1use crate::sparse_io::SparseIoBackend;
9use legume_numeric::matrix::traits::MatWithNames;
10use log::info;
11use nalgebra::DMatrix;
12use rand_distr::num_traits::FromPrimitive;
13use std::sync::Arc;
14use zarrs::array::{data_type, Array as ZArray};
15use zarrs::config::MetadataRetrieveVersion;
16use zarrs::filesystem::FilesystemStore;
17use zarrs::storage::ReadableListableStorageTraits as ZReadStorageTraits;
18use zarrs_zip::ZipStorageAdapter;
19
20pub type ZarrStoreRw = (Arc<dyn ZReadStorageTraits>, Option<Arc<FilesystemStore>>);
24
25fn detect_zip_zarr_prefix(zip_path: &std::path::Path) -> anyhow::Result<Box<str>> {
31 let filename = zip_path
32 .file_name()
33 .and_then(|f| f.to_str())
34 .ok_or_else(|| anyhow::anyhow!("invalid zip path: {:?}", zip_path))?;
35 let stem = filename.strip_suffix(".zip").unwrap_or(filename);
36 let new_prefix = format!("{}/", stem.strip_suffix(".zarr").unwrap_or(stem));
37 let legacy_prefix = format!("{}/", stem);
38
39 let file = std::fs::File::open(zip_path)?;
40 let archive = zip::ZipArchive::new(std::io::BufReader::new(file))?;
41 if archive.file_names().any(|n| n.starts_with(&new_prefix)) {
42 Ok(new_prefix.into())
43 } else if new_prefix != legacy_prefix
44 && archive.file_names().any(|n| n.starts_with(&legacy_prefix))
45 {
46 Ok(legacy_prefix.into())
47 } else {
48 Ok("".into())
49 }
50}
51
52pub fn open_zarr_store_rw(path: &str) -> anyhow::Result<ZarrStoreRw> {
58 let p = std::path::Path::new(path);
59 let is_zip = p.extension().is_some_and(|e| e == "zip");
60
61 if is_zip {
62 let parent = p
63 .parent()
64 .ok_or_else(|| anyhow::anyhow!("no parent directory for {}", path))?;
65 let filename = p
66 .file_name()
67 .ok_or_else(|| anyhow::anyhow!("no filename for {}", path))?
68 .to_str()
69 .ok_or_else(|| anyhow::anyhow!("non-UTF8 filename in {}", path))?;
70 let zarr_prefix = detect_zip_zarr_prefix(p)?;
71 let fs = Arc::new(FilesystemStore::new(parent)?);
72 let key = zarrs::storage::StoreKey::new(filename)?;
73 info!(
74 "Opening zarr zip store: {} (prefix: {:?})",
75 path, zarr_prefix
76 );
77 Ok((
78 Arc::new(ZipStorageAdapter::new_with_path(fs, key, &*zarr_prefix)?),
79 None,
80 ))
81 } else {
82 let fs = Arc::new(FilesystemStore::new(path)?);
83 Ok((fs.clone(), Some(fs)))
84 }
85}
86
87pub fn open_zarr_store(path: &str) -> anyhow::Result<Arc<dyn ZReadStorageTraits>> {
89 open_zarr_store_rw(path).map(|(r, _)| r)
90}
91
92pub fn apply_zip_flag(output: &str, zip: bool, backend: &SparseIoBackend) -> Box<str> {
101 let target_is_hdf5 = matches!(backend, SparseIoBackend::HDF5)
106 || output.ends_with(".h5")
107 || output.ends_with(".hdf5");
108 if zip && !target_is_hdf5 && !output.ends_with(".zarr.zip") {
109 let base = crate::hdf5_io::strip_backend_suffix(output);
110 format!("{}.zarr.zip", base).into()
111 } else {
112 output.into()
113 }
114}
115
116pub fn prepare_output(
122 output: &str,
123 backend: SparseIoBackend,
124 zip: bool,
125) -> anyhow::Result<(Box<str>, SparseIoBackend, Box<str>)> {
126 use crate::hdf5_io::resolve_backend_file;
127 use legume_numeric::matrix::common_io::remove_file;
128
129 let effective_output = apply_zip_flag(output, zip, &backend);
130 let (backend, working_file) = resolve_backend_file(&effective_output, Some(backend))?;
131 if std::path::Path::new(working_file.as_ref()).exists() {
132 remove_file(&working_file)?;
133 }
134 Ok((effective_output, backend, working_file))
135}
136
137pub fn finalize_output<'a>(
141 working_file: &'a str,
142 effective_output: &'a str,
143) -> anyhow::Result<&'a str> {
144 finalize_zarr_output(working_file, effective_output)?;
145 Ok(if effective_output.ends_with(".zarr.zip") {
146 effective_output
147 } else {
148 working_file
149 })
150}
151
152pub fn extract_zarr_zip(zip_path: &str, target_dir: &str) -> anyhow::Result<()> {
156 let zip_file = std::fs::File::open(zip_path)?;
157 let mut archive = zip::ZipArchive::new(std::io::BufReader::new(zip_file))?;
158
159 let filename = std::path::Path::new(zip_path)
163 .file_name()
164 .and_then(|f| f.to_str())
165 .ok_or_else(|| anyhow::anyhow!("invalid zip path: {}", zip_path))?;
166 let stem = filename.strip_suffix(".zip").unwrap_or(filename);
167 let new_prefix = format!("{}/", stem.strip_suffix(".zarr").unwrap_or(stem));
168 let legacy_prefix = format!("{}/", stem);
169 let prefix: &str = if archive.file_names().any(|n| n.starts_with(&new_prefix)) {
170 &new_prefix
171 } else if new_prefix != legacy_prefix
172 && archive.file_names().any(|n| n.starts_with(&legacy_prefix))
173 {
174 &legacy_prefix
175 } else {
176 ""
177 };
178
179 std::fs::create_dir_all(target_dir)?;
180 let target = std::path::Path::new(target_dir);
181
182 for i in 0..archive.len() {
183 let mut file = archive.by_index(i)?;
184 let rel = file.name().strip_prefix(prefix).unwrap_or(file.name());
185 if rel.is_empty() {
186 continue;
187 }
188 let out_path = target.join(rel);
189 if file.is_dir() {
190 std::fs::create_dir_all(&out_path)?;
191 } else {
192 if let Some(parent) = out_path.parent() {
193 std::fs::create_dir_all(parent)?;
194 }
195 let mut outfile = std::fs::File::create(&out_path)?;
196 std::io::copy(&mut file, &mut outfile)?;
197 }
198 }
199 Ok(())
200}
201
202pub fn materialize_writable_backend(src: &str, dst: &str) -> anyhow::Result<()> {
206 if src.ends_with(".zarr.zip") {
207 info!("extracting {} → {}", src, dst);
208 extract_zarr_zip(src, dst)
209 } else {
210 legume_numeric::matrix::common_io::recursive_copy(src, dst)
211 }
212}
213
214pub fn finalize_zarr_output(zarr_dir: &str, target_path: &str) -> anyhow::Result<()> {
224 if !target_path.ends_with(".zarr.zip") {
225 return Ok(());
226 }
227
228 let zarr_path = std::path::Path::new(zarr_dir);
229 let stem = zarr_path
230 .file_name()
231 .and_then(|s| s.to_str())
232 .map(|s| s.strip_suffix(".zarr").unwrap_or(s))
233 .ok_or_else(|| anyhow::anyhow!("invalid zarr_dir: {}", zarr_dir))?
234 .to_string();
235
236 let tmp_target = format!("{}.tmp", target_path);
241 info!("Zipping zarr output: {} → {}", zarr_dir, target_path);
242 if let Err(e) =
243 legume_numeric::matrix::common_io::zip_dir_as(zarr_dir, &tmp_target, Some(&stem))
244 {
245 let _ = std::fs::remove_file(&tmp_target);
246 return Err(e);
247 }
248 if let Err(e) = std::fs::rename(&tmp_target, target_path) {
249 let _ = std::fs::remove_file(&tmp_target);
250 return Err(e.into());
251 }
252 std::fs::remove_dir_all(zarr_dir)?;
253 Ok(())
254}
255
256pub fn read_zarr_array_attr<V: serde::de::DeserializeOwned>(
264 store: Arc<dyn ZReadStorageTraits>,
265 array_path: &str,
266 attr_name: &str,
267) -> anyhow::Result<V> {
268 let arr = ZArray::open_opt(store, array_path, &MetadataRetrieveVersion::Default)?;
269 let attr = arr.attributes().get(attr_name).ok_or_else(|| {
270 anyhow::anyhow!(
271 "attribute '{}' not found on array '{}'",
272 attr_name,
273 array_path
274 )
275 })?;
276 Ok(serde_json::from_value(attr.clone())?)
277}
278
279pub fn read_zarr_flat_f32(
285 store: Arc<dyn ZReadStorageTraits>,
286 key: &str,
287) -> anyhow::Result<(Vec<f32>, Vec<u64>)> {
288 let arr = ZArray::open_opt(store, key, &MetadataRetrieveVersion::Default)?;
289 let shape = arr.shape().to_vec();
290 let subset = arr.subset_all();
291
292 let dt = arr.data_type();
293 let data = if *dt == data_type::float32() {
294 arr.retrieve_array_subset::<Vec<f32>>(&subset)?
295 } else if *dt == data_type::float64() {
296 arr.retrieve_array_subset::<Vec<f64>>(&subset)?
297 .into_iter()
298 .map(|x| x as f32)
299 .collect()
300 } else if *dt == data_type::uint32() {
301 arr.retrieve_array_subset::<Vec<u32>>(&subset)?
302 .into_iter()
303 .map(|x| x as f32)
304 .collect()
305 } else if *dt == data_type::uint64() {
306 arr.retrieve_array_subset::<Vec<u64>>(&subset)?
307 .into_iter()
308 .map(|x| x as f32)
309 .collect()
310 } else {
311 anyhow::bail!("unsupported zarr data type: {:?}", dt)
312 };
313
314 Ok((data, shape))
315}
316
317pub fn read_zarr_flat_u32(
319 store: Arc<dyn ZReadStorageTraits>,
320 key: &str,
321) -> anyhow::Result<(Vec<u32>, Vec<u64>)> {
322 let arr = ZArray::open_opt(store, key, &MetadataRetrieveVersion::Default)?;
323 let shape = arr.shape().to_vec();
324 let subset = arr.subset_all();
325
326 let dt = arr.data_type();
327 let data = if *dt == data_type::uint32() {
328 arr.retrieve_array_subset::<Vec<u32>>(&subset)?
329 } else if *dt == data_type::uint64() {
330 arr.retrieve_array_subset::<Vec<u64>>(&subset)?
331 .into_iter()
332 .map(|x| x as u32)
333 .collect()
334 } else {
335 anyhow::bail!("unsupported zarr data type for u32: {:?}", dt)
336 };
337
338 Ok((data, shape))
339}
340
341#[cfg(feature = "ndarray")]
344pub fn read_zarr_ndarray<T>(
346 store: Arc<dyn ZReadStorageTraits>,
347 key_name: &str,
348) -> anyhow::Result<ndarray::ArrayD<T>>
349where
350 T: zarrs::array::ElementOwned + FromPrimitive,
351{
352 let arr = ZArray::open_opt(store, key_name, &MetadataRetrieveVersion::Default)?;
353
354 let dt = arr.data_type();
355 if *dt == data_type::float32() {
356 let array: ndarray::ArrayD<f32> =
357 arr.retrieve_array_subset::<ndarray::ArrayD<f32>>(&arr.subset_all())?;
358 Ok(array.mapv(|x| T::from_f32(x).unwrap()))
359 } else if *dt == data_type::float64() {
360 let array: ndarray::ArrayD<f64> =
361 arr.retrieve_array_subset::<ndarray::ArrayD<f64>>(&arr.subset_all())?;
362 Ok(array.mapv(|x| T::from_f64(x).unwrap()))
363 } else if *dt == data_type::uint32() {
364 let array: ndarray::ArrayD<u32> =
365 arr.retrieve_array_subset::<ndarray::ArrayD<u32>>(&arr.subset_all())?;
366 Ok(array.mapv(|x| T::from_u32(x).unwrap()))
367 } else if *dt == data_type::uint64() {
368 let array: ndarray::ArrayD<u64> =
369 arr.retrieve_array_subset::<ndarray::ArrayD<u64>>(&arr.subset_all())?;
370 Ok(array.mapv(|x| T::from_u64(x).unwrap()))
371 } else {
372 anyhow::bail!("unsupported zarr data type: {:?}", dt)
373 }
374}
375
376pub fn read_zarr_numerics<T>(
378 store: Arc<dyn ZReadStorageTraits>,
379 key_name: &str,
380) -> anyhow::Result<Vec<T>>
381where
382 T: zarrs::array::ElementOwned + FromPrimitive,
383{
384 let arr = ZArray::open_opt(store, key_name, &MetadataRetrieveVersion::Default)?;
385
386 let dt = arr.data_type();
387 let ret = if *dt == data_type::float32() {
388 arr.retrieve_array_subset::<Vec<f32>>(&arr.subset_all())?
389 .into_iter()
390 .map(|x| T::from_f32(x).unwrap())
391 .collect()
392 } else if *dt == data_type::float64() {
393 arr.retrieve_array_subset::<Vec<f64>>(&arr.subset_all())?
394 .into_iter()
395 .map(|x| T::from_f64(x).unwrap())
396 .collect()
397 } else if *dt == data_type::uint32() {
398 arr.retrieve_array_subset::<Vec<u32>>(&arr.subset_all())?
399 .into_iter()
400 .map(|x| T::from_u32(x).unwrap())
401 .collect()
402 } else if *dt == data_type::uint64() {
403 arr.retrieve_array_subset::<Vec<u64>>(&arr.subset_all())?
404 .into_iter()
405 .map(|x| T::from_u64(x).unwrap())
406 .collect()
407 } else {
408 anyhow::bail!("unsupported zarr data type: {:?}", dt);
409 };
410
411 Ok(ret)
412}
413
414pub fn read_zarr_group_attr<V>(
419 store: Arc<dyn ZReadStorageTraits>,
420 key_name: &str,
421) -> anyhow::Result<V>
422where
423 V: serde::de::DeserializeOwned,
424{
425 use anyhow::Context;
426
427 fn parse_key_name(key_name: &str) -> (Box<str>, Box<str>) {
428 let trimmed = key_name.strip_prefix('/').unwrap_or(key_name);
429 match trimmed.rsplit_once('/') {
430 Some((left, right)) => (
431 format!("/{}", left).into_boxed_str(),
432 right.to_string().into_boxed_str(),
433 ),
434 None => (
435 "/".to_string().into_boxed_str(),
436 trimmed.to_string().into_boxed_str(),
437 ),
438 }
439 }
440
441 let (group_name, attr_name) = parse_key_name(key_name);
442
443 let group = zarrs::group::Group::open_opt(
444 store,
445 group_name.as_ref(),
446 &MetadataRetrieveVersion::Default,
447 )
448 .with_context(|| format!("Failed to open group '{}'", group_name))?;
449
450 let attr_value = group
451 .attributes()
452 .get(attr_name.as_ref())
453 .with_context(|| {
454 format!(
455 "Attribute '{}' not found in group '{}'",
456 attr_name, group_name
457 )
458 })?;
459
460 Ok(serde_json::from_value(attr_value.clone())?)
461}
462
463pub fn read_zarr_strings(
465 store: Arc<dyn ZReadStorageTraits>,
466 key_name: &str,
467) -> anyhow::Result<Vec<Box<str>>> {
468 let arr = ZArray::open_opt(store, key_name, &MetadataRetrieveVersion::Default)?;
469
470 Ok(arr
471 .retrieve_array_subset::<Vec<String>>(&arr.subset_all())?
472 .into_iter()
473 .map(|x| x.into_boxed_str())
474 .collect())
475}
476
477fn hex_to_shifted_lookup() -> [Option<char>; 256] {
481 let mut lookup = [None; 256];
482 for (i, ch) in "0123456789abcdef".chars().enumerate() {
483 lookup[ch as usize] = Some(
484 [
485 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p',
486 ][i],
487 );
488 }
489 lookup
490}
491
492fn encode_10x_cell_id(
494 lookup: &[Option<char>; 256],
495 barcode: u32,
496 suffix: u32,
497) -> anyhow::Result<Box<str>> {
498 let barcode: String = format!("{:08x}", barcode)
499 .chars()
500 .map(|ch| lookup[ch as usize].ok_or_else(|| anyhow::anyhow!("invalid hex char: {}", ch)))
501 .collect::<anyhow::Result<String>>()?;
502 Ok(format!("{}-{}", barcode, suffix).into_boxed_str())
503}
504
505#[cfg(feature = "ndarray")]
506pub fn parse_10x_cell_id(
510 input: ndarray::ArrayView<u32, ndarray::IxDyn>,
511) -> anyhow::Result<Vec<Box<str>>> {
512 anyhow::ensure!(
513 input.ndim() == 2 && input.shape()[1] == 2,
514 "Must be 2D with shape [N, 2]"
515 );
516 let lookup = hex_to_shifted_lookup();
517 input
518 .outer_iter()
519 .map(|row| encode_10x_cell_id(&lookup, row[0], row[1]))
520 .collect()
521}
522
523pub fn parse_10x_cell_id_flat(data: &[u32], nrows: usize) -> anyhow::Result<Vec<Box<str>>> {
527 anyhow::ensure!(data.len() == nrows * 2, "cell_id buffer size mismatch");
528 let lookup = hex_to_shifted_lookup();
529 (0..nrows)
530 .map(|i| encode_10x_cell_id(&lookup, data[i * 2], data[i * 2 + 1]))
531 .collect()
532}
533
534fn select_columns_to_dmatrix(
539 data: &[f32],
540 nrows: usize,
541 ncols_total: usize,
542 selected: &[usize],
543) -> anyhow::Result<DMatrix<f32>> {
544 let k = selected.len();
545 let mut col_major = vec![0.0f32; nrows * k];
546 for (col_out, &col_in) in selected.iter().enumerate() {
547 anyhow::ensure!(col_in < ncols_total, "column index {} out of range", col_in);
548 for row in 0..nrows {
549 col_major[row + col_out * nrows] = data[row * ncols_total + col_in];
550 }
551 }
552 Ok(DMatrix::from_vec(nrows, k, col_major))
553}
554
555fn resolve_columns(
557 column_indices: &[usize],
558 column_names: &[Box<str>],
559 all_col_names: &[Box<str>],
560 default_cols: &[usize],
561) -> anyhow::Result<Vec<usize>> {
562 if !column_indices.is_empty() {
563 Ok(column_indices.to_vec())
564 } else if !column_names.is_empty() {
565 let matched: Vec<usize> = column_names
568 .iter()
569 .filter_map(|name| all_col_names.iter().position(|c| c == name))
570 .collect();
571 if matched.is_empty() {
572 anyhow::bail!(
573 "none of the requested columns {:?} found (available: {:?})",
574 column_names,
575 all_col_names
576 );
577 }
578 Ok(matched)
579 } else {
580 Ok(default_cols.to_vec())
581 }
582}
583
584pub fn read_zarr_matrix(
597 file_path: &str,
598 data_array: &str,
599 col_names_attr: Option<&str>,
600 row_names_array: Option<&str>,
601 row_names_10x: bool,
602 column_indices: &[usize],
603 column_names: &[Box<str>],
604) -> anyhow::Result<MatWithNames<DMatrix<f32>>> {
605 let store = open_zarr_store(file_path)?;
606
607 let (flat, shape) = read_zarr_flat_f32(store.clone(), data_array)?;
609 let nrows = shape[0] as usize;
610 let ncols_total = if shape.len() > 1 {
611 shape[1] as usize
612 } else {
613 1
614 };
615
616 let all_col_names: Vec<Box<str>> = if let Some(attr) = col_names_attr {
618 read_zarr_array_attr(store.clone(), data_array, attr).unwrap_or_else(|_| {
619 (0..ncols_total)
620 .map(|i| i.to_string().into_boxed_str())
621 .collect()
622 })
623 } else {
624 (0..ncols_total)
625 .map(|i| i.to_string().into_boxed_str())
626 .collect()
627 };
628
629 let selected = resolve_columns(column_indices, column_names, &all_col_names, &[0, 1])?;
631 let sel_names: Vec<Box<str>> = selected
632 .iter()
633 .map(|&i| {
634 all_col_names
635 .get(i)
636 .cloned()
637 .unwrap_or_else(|| i.to_string().into_boxed_str())
638 })
639 .collect();
640
641 let mat = select_columns_to_dmatrix(&flat, nrows, ncols_total, &selected)?;
643
644 let row_names = if let Some(rn_path) = row_names_array {
646 if row_names_10x {
647 let (id_data, id_shape) = read_zarr_flat_u32(store.clone(), rn_path)?;
648 anyhow::ensure!(
649 id_shape[0] as usize == nrows,
650 "row names array rows ({}) != data rows ({})",
651 id_shape[0],
652 nrows
653 );
654 parse_10x_cell_id_flat(&id_data, nrows)?
655 } else {
656 read_zarr_strings(store.clone(), rn_path)?
657 }
658 } else {
659 (0..nrows).map(|i| i.to_string().into_boxed_str()).collect()
660 };
661
662 info!(
663 "Read {} x {} from zarr {}{}: {:?}",
664 nrows,
665 selected.len(),
666 file_path,
667 data_array,
668 sel_names
669 );
670
671 Ok(MatWithNames {
672 rows: row_names,
673 cols: sel_names,
674 mat,
675 })
676}
677
678pub fn read_zarr_coordinates(
687 file_path: &str,
688 column_indices: &[usize],
689 column_names: &[Box<str>],
690) -> anyhow::Result<MatWithNames<DMatrix<f32>>> {
691 read_zarr_matrix(
692 file_path,
693 "/cell_summary",
694 Some("column_names"),
695 Some("/cell_id"),
696 true,
697 column_indices,
698 column_names,
699 )
700}
701
702#[cfg(test)]
703mod tests {
704 use super::*;
705
706 fn xenium_path() -> Option<std::path::PathBuf> {
707 let p = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
708 .parent()
709 .unwrap()
710 .join("docs/temp/cells.zarr.zip");
711 p.exists().then_some(p)
712 }
713
714 #[test]
715 fn test_parse_10x_cell_id_flat() {
716 let data = vec![16844u32, 1, 22527, 1];
717 let ids = parse_10x_cell_id_flat(&data, 2).unwrap();
718 assert_eq!(ids.len(), 2);
719 assert!(ids[0].ends_with("-1"), "got: {}", ids[0]);
720 assert!(ids[1].ends_with("-1"), "got: {}", ids[1]);
721 assert_eq!(ids[0].len(), 10);
722 }
723
724 #[test]
725 fn test_parse_10x_cell_id_flat_mismatch() {
726 assert!(parse_10x_cell_id_flat(&[1, 2, 3], 2).is_err());
727 }
728
729 #[test]
730 fn test_read_zarr_coordinates_by_name() {
731 let Some(p) = xenium_path() else { return };
732 let result = read_zarr_coordinates(
733 p.to_str().unwrap(),
734 &[],
735 &["cell_centroid_x".into(), "cell_centroid_y".into()],
736 )
737 .unwrap();
738
739 assert_eq!(result.mat.ncols(), 2);
740 assert!(result.mat.nrows() > 0);
741 assert_eq!(result.rows.len(), result.mat.nrows());
742 assert_eq!(result.cols[0].as_ref(), "cell_centroid_x");
743 assert_eq!(result.cols[1].as_ref(), "cell_centroid_y");
744 assert!(result.mat.min() >= 0.0);
745 }
746
747 #[test]
748 fn test_read_zarr_coordinates_by_index() {
749 let Some(p) = xenium_path() else { return };
750 let result = read_zarr_coordinates(p.to_str().unwrap(), &[0, 1], &[]).unwrap();
751 assert_eq!(result.mat.ncols(), 2);
752 assert_eq!(result.cols[0].as_ref(), "cell_centroid_x");
753 }
754
755 #[test]
756 fn test_read_zarr_coordinates_default() {
757 let Some(p) = xenium_path() else { return };
758 let result = read_zarr_coordinates(p.to_str().unwrap(), &[], &[]).unwrap();
759 assert_eq!(result.mat.ncols(), 2);
760 }
761
762 #[test]
763 fn test_read_zarr_coordinates_bad_column() {
764 let Some(p) = xenium_path() else { return };
765 let result = read_zarr_coordinates(
766 p.to_str().unwrap(),
767 &[],
768 &["nonexistent".to_string().into_boxed_str()],
769 );
770 assert!(result.is_err());
771 }
772
773 #[test]
774 fn test_read_zarr_coordinates_mixed_defaults() {
775 let Some(p) = xenium_path() else { return };
778 let result = read_zarr_coordinates(
779 p.to_str().unwrap(),
780 &[],
781 &[
782 "pxl_row_in_fullres".into(),
783 "pxl_col_in_fullres".into(),
784 "cell_centroid_x".into(),
785 "cell_centroid_y".into(),
786 ],
787 )
788 .unwrap();
789 assert_eq!(result.mat.ncols(), 2);
790 assert_eq!(result.cols[0].as_ref(), "cell_centroid_x");
791 assert_eq!(result.cols[1].as_ref(), "cell_centroid_y");
792 assert!(result.mat.nrows() > 0);
793 }
794
795 #[test]
796 fn test_read_zarr_array_attr() {
797 let Some(p) = xenium_path() else { return };
798 let store = open_zarr_store(p.to_str().unwrap()).unwrap();
799 let names: Vec<Box<str>> =
800 read_zarr_array_attr(store, "/cell_summary", "column_names").unwrap();
801 assert!(!names.is_empty());
802 assert!(names.contains(&"cell_centroid_x".to_string().into_boxed_str()));
803 }
804
805 #[test]
806 fn test_read_zarr_matrix_generic() {
807 let Some(p) = xenium_path() else { return };
808 let result = read_zarr_matrix(
810 p.to_str().unwrap(),
811 "/cell_summary",
812 Some("column_names"),
813 Some("/cell_id"),
814 true,
815 &[2, 3],
816 &[],
817 )
818 .unwrap();
819 assert_eq!(result.mat.ncols(), 2);
820 assert_eq!(result.cols[0].as_ref(), "cell_area");
821 assert_eq!(result.cols[1].as_ref(), "nucleus_centroid_x");
822 }
823}