use crate::sparse_io::*;
#[cfg(feature = "hdf5")]
use hdf5::types::FixedAscii;
#[cfg(feature = "hdf5")]
use hdf5::types::FixedUnicode;
#[cfg(feature = "hdf5")]
use hdf5::types::TypeDescriptor;
#[cfg(feature = "hdf5")]
use hdf5::types::VarLenUnicode;
#[cfg(feature = "hdf5")]
pub struct H5adDataFrame {
pub col_names: Vec<Box<str>>,
pub col_data: Vec<Vec<Box<str>>>,
}
pub fn strip_backend_suffix(path: &str) -> &str {
path.strip_suffix(".zarr.zip")
.or_else(|| path.strip_suffix(".zarr"))
.or_else(|| path.strip_suffix(".h5"))
.unwrap_or(path)
}
pub fn resolve_backend_file(
file_path: &str,
backend: Option<SparseIoBackend>,
) -> anyhow::Result<(SparseIoBackend, Box<str>)> {
use legume_numeric::matrix::common_io::file_ext;
let ext = file_ext(file_path).unwrap_or(Box::<str>::from(""));
if let Some(backend) = backend {
let mut resolved_backend = backend;
let mut backend_file = file_path.to_string();
match ext.as_ref() {
"zarr" => {
resolved_backend = SparseIoBackend::Zarr;
}
"h5" => {
resolved_backend = SparseIoBackend::HDF5;
}
"zip" if file_path.ends_with(".zarr.zip") => {
resolved_backend = SparseIoBackend::Zarr;
backend_file = file_path
.strip_suffix(".zip")
.unwrap_or(file_path)
.to_string();
}
_ => {
backend_file = match resolved_backend {
SparseIoBackend::HDF5 => format!("{}.h5", file_path),
SparseIoBackend::Zarr => format!("{}.zarr", file_path),
}
}
};
#[cfg(not(feature = "hdf5"))]
if resolved_backend == SparseIoBackend::HDF5 {
let stripped = strip_backend_suffix(&backend_file);
let new_path = format!("{}.zarr", stripped);
log::warn!(
"HDF5 output requested but this binary was built without the \
`hdf5` feature; writing Zarr instead ({} -> {}). Pass \
`--zip=true` (or use a `.zarr.zip` output path) for a zipped \
archive.",
&backend_file,
&new_path
);
return Ok((SparseIoBackend::Zarr, new_path.into_boxed_str()));
}
Ok((resolved_backend, backend_file.into_boxed_str()))
} else {
let resolved_backend = match ext.as_ref() {
"zarr" => SparseIoBackend::Zarr,
"h5" => SparseIoBackend::HDF5,
"zip" if file_path.ends_with(".zarr.zip") => SparseIoBackend::Zarr,
_ => return Err(anyhow::anyhow!("Unknown file format: {}", file_path)),
};
let backend_file = file_path.to_string();
Ok((resolved_backend, backend_file.into_boxed_str()))
}
}
#[cfg(feature = "hdf5")]
pub fn read_h5ad_column(group: &hdf5::Group, col_name: &str) -> anyhow::Result<Vec<Box<str>>> {
if let Ok(col_group) = group.group(col_name) {
let categories = read_hdf5_strings(col_group.dataset("categories")?)?;
let codes_ds = col_group.dataset("codes")?;
let dtype = codes_ds.dtype()?;
let desc = dtype.to_descriptor()?;
let codes: Vec<i32> = match desc {
TypeDescriptor::Integer(sz) => match sz {
hdf5::types::IntSize::U1 => codes_ds
.read_1d::<i8>()?
.iter()
.map(|&x| x as i32)
.collect(),
hdf5::types::IntSize::U2 => codes_ds
.read_1d::<i16>()?
.iter()
.map(|&x| x as i32)
.collect(),
hdf5::types::IntSize::U4 => codes_ds.read_1d::<i32>()?.to_vec(),
hdf5::types::IntSize::U8 => codes_ds
.read_1d::<i64>()?
.iter()
.map(|&x| x as i32)
.collect(),
},
TypeDescriptor::Unsigned(sz) => match sz {
hdf5::types::IntSize::U1 => codes_ds
.read_1d::<u8>()?
.iter()
.map(|&x| x as i32)
.collect(),
hdf5::types::IntSize::U2 => codes_ds
.read_1d::<u16>()?
.iter()
.map(|&x| x as i32)
.collect(),
hdf5::types::IntSize::U4 => codes_ds
.read_1d::<u32>()?
.iter()
.map(|&x| x as i32)
.collect(),
hdf5::types::IntSize::U8 => codes_ds
.read_1d::<u64>()?
.iter()
.map(|&x| x as i32)
.collect(),
},
_ => {
return Err(anyhow::anyhow!(
"unsupported codes dtype for categorical '{}'",
col_name
));
}
};
let result: Vec<Box<str>> = codes
.iter()
.map(|&c| {
if c < 0 {
"NA".to_string().into_boxed_str()
} else {
categories[c as usize].clone()
}
})
.collect();
return Ok(result);
}
let ds = group.dataset(col_name)?;
let dtype = ds.dtype()?;
let desc = dtype.to_descriptor()?;
match desc {
TypeDescriptor::VarLenUnicode
| TypeDescriptor::FixedAscii(_)
| TypeDescriptor::FixedUnicode(_) => read_hdf5_strings(ds),
TypeDescriptor::Boolean => {
let data = ds.read_1d::<bool>()?;
Ok(data
.iter()
.map(|&b| if b { "true" } else { "false" }.into())
.collect())
}
TypeDescriptor::Integer(_) => {
let data = ds.read_1d::<i64>()?;
Ok(data
.iter()
.map(|x| x.to_string().into_boxed_str())
.collect())
}
TypeDescriptor::Unsigned(_) => {
let data = ds.read_1d::<u64>()?;
Ok(data
.iter()
.map(|x| x.to_string().into_boxed_str())
.collect())
}
TypeDescriptor::Float(sz) => match sz {
hdf5::types::FloatSize::U4 => {
let data = ds.read_1d::<f32>()?;
Ok(data
.iter()
.map(|x| x.to_string().into_boxed_str())
.collect())
}
hdf5::types::FloatSize::U8 => {
let data = ds.read_1d::<f64>()?;
Ok(data
.iter()
.map(|x| x.to_string().into_boxed_str())
.collect())
}
},
_ => Err(anyhow::anyhow!(
"unsupported dtype for column '{}'",
col_name
)),
}
}
#[cfg(feature = "hdf5")]
pub fn resolve_h5ad_field(
group: &hdf5::Group,
fields: &[Box<str>],
label: &str,
) -> Option<Vec<Box<str>>> {
for field in fields {
let field = field.trim();
if field.is_empty() {
continue;
}
if let Ok(v) = read_h5ad_column(group, field) {
log::info!("Using '{}' for {} ({} entries)", field, label, v.len());
return Some(v);
}
}
None
}
#[cfg(feature = "hdf5")]
pub fn read_h5ad_dataframe(group: &hdf5::Group) -> anyhow::Result<H5adDataFrame> {
let col_order: Vec<String> = match group.attr("column-order") {
Ok(attr) => match attr.read_1d::<VarLenUnicode>() {
Ok(arr) => arr.iter().map(|x| x.to_string()).collect(),
Err(e) => {
log::warn!("Failed to read column-order attribute: {}", e);
vec![]
}
},
Err(_) => {
log::warn!("No column-order attribute found; returning empty dataframe");
vec![]
}
};
let mut col_names = Vec::new();
let mut col_data = Vec::new();
for col_name in &col_order {
match read_h5ad_column(group, col_name) {
Ok(data) => {
col_names.push(col_name.clone().into_boxed_str());
col_data.push(data);
}
Err(e) => {
log::warn!("Skipping obs column '{}': {}", col_name, e);
}
}
}
Ok(H5adDataFrame {
col_names,
col_data,
})
}
#[cfg(feature = "hdf5")]
pub fn read_hdf5_strings(data: hdf5::dataset::Dataset) -> anyhow::Result<Vec<Box<str>>> {
let dtype = data.dtype()?;
let desc = dtype.to_descriptor()?;
let ret: Vec<Box<str>> = match desc {
TypeDescriptor::VarLenUnicode => data
.read_1d::<VarLenUnicode>()?
.map(|x| x.to_string().into_boxed_str())
.into_iter()
.collect(),
TypeDescriptor::FixedAscii(n) => {
if n < 24 {
data.read_1d::<FixedAscii<24>>()?
.map(|x| x.to_string().into_boxed_str())
.into_iter()
.collect()
} else if n < 128 {
data.read_1d::<FixedAscii<128>>()?
.map(|x| x.to_string().into_boxed_str())
.into_iter()
.collect()
} else {
data.read_1d::<FixedAscii<1024>>()?
.map(|x| x.to_string().into_boxed_str())
.into_iter()
.collect()
}
}
TypeDescriptor::FixedUnicode(n) => {
if n < 24 {
data.read_1d::<FixedUnicode<24>>()?
.map(|x| x.to_string().into_boxed_str())
.into_iter()
.collect()
} else if n < 128 {
data.read_1d::<FixedUnicode<128>>()?
.map(|x| x.to_string().into_boxed_str())
.into_iter()
.collect()
} else {
data.read_1d::<FixedUnicode<1024>>()?
.map(|x| x.to_string().into_boxed_str())
.into_iter()
.collect()
}
}
_ => {
return Err(anyhow::anyhow!("unsupported string"));
}
};
Ok(ret)
}