use super::{log_feature_type_histogram, Output, Staged};
use crate::hdf5_io::*;
use crate::sparse_io::*;
use crate::sparse_util::*;
use crate::utilities::name_matching::{
colon_peak_names, compose_id_name, filter_row_indices_by_type, make_names_unique,
};
use legume_numeric::matrix::common_io::*;
use log::{info, warn};
#[derive(Clone, Debug)]
pub struct H5adImport {
pub h5ad_file: Box<str>,
pub row_id_field: Vec<Box<str>>,
pub row_name_field: Vec<Box<str>>,
pub col_name_field: Vec<Box<str>>,
pub select_row_type: Box<str>,
pub remove_row_type: Box<str>,
}
impl H5adImport {
pub fn new(h5ad_file: &str) -> Self {
let fields = |xs: &[&str]| xs.iter().map(|&x| Box::from(x)).collect();
Self {
h5ad_file: h5ad_file.into(),
row_id_field: fields(&["_index", "gene_id"]),
row_name_field: fields(&["feature_name", "gene_name"]),
col_name_field: fields(&["_index", "cell"]),
select_row_type: "".into(),
remove_row_type: "".into(),
}
}
}
pub fn from_h5ad(h5ad: &H5adImport, out: &Output) -> anyhow::Result<Box<str>> {
stage_h5ad(h5ad, out)?.finish()
}
pub fn stage_h5ad(args: &H5adImport, out: &Output) -> anyhow::Result<Staged> {
let file = hdf5::File::open(args.h5ad_file.to_string())?;
info!("Opened AnnData h5ad file: {}", args.h5ad_file);
let (staged, backend) = out.stage()?;
let backend_file = staged.working();
let effective_output = &staged.effective;
let (x_path, var_path) = if file.group("raw/X").is_ok() {
info!("Using raw/X (raw counts)");
("raw/X", "raw/var")
} else {
info!("Using X");
("X", "var")
};
let x_group = file.group(x_path)?;
let var_group = file.group(var_path)?;
let obs_group = file.group("obs")?;
let row_ids = resolve_h5ad_field(
&var_group,
&index_candidates(&var_group, &args.row_id_field),
"feature IDs",
);
let row_names = resolve_h5ad_field(&var_group, &args.row_name_field, "gene symbols");
let typed = read_h5ad_column(&var_group, "feature_type")
.or_else(|_| read_h5ad_column(&var_group, "feature_types"))
.ok();
let column_names = resolve_h5ad_field(
&obs_group,
&index_candidates(&obs_group, &args.col_name_field),
"barcodes",
);
let (n_obs, n_var) = declared_shape(&x_group).unzip();
let pointer_type = sparse_pointer_type(
&x_group,
x_path,
n_obs.or(column_names.as_ref().map(Vec::len)),
n_var.or(row_ids.as_ref().map(Vec::len)),
)?;
let CooTripletsShape { triplets, shape } = {
let values = x_group.dataset("data")?.read_1d::<f32>()?;
let indices = x_group.dataset("indices")?.read_1d::<u64>()?;
let indptr = x_group.dataset("indptr")?.read_1d::<u64>()?;
ValuesIndicesPointers {
values: values.as_slice().expect("values not contiguous"),
indices: indices.as_slice().expect("indices not contiguous"),
indptr: indptr.as_slice().expect("indptr not contiguous"),
}
.to_coo(pointer_type)?
};
let nnz = shape.nnz;
info!(
"Read {} non-zero elements reaching {} x {}",
nnz, shape.nrows, shape.ncols
);
let nrows = crate::convert::axis_len(
n_var,
row_ids.as_ref().map(Vec::len),
shape.nrows,
pointer_type == IndexPointerType::Row,
"features",
)?;
let ncols = crate::convert::axis_len(
n_obs,
column_names.as_ref().map(Vec::len),
shape.ncols,
pointer_type == IndexPointerType::Column,
"cells",
)?;
info!("Matrix: {} x {}", nrows, ncols);
let listed = |what: &str, names: &[Box<str>], n: usize| {
anyhow::ensure!(
names.len() == n,
"{} lists {} {what} but {x_path} has {n}",
args.h5ad_file,
names.len()
);
Ok(())
};
let index =
|n: usize| -> Vec<Box<str>> { (0..n).map(|x| x.to_string().into_boxed_str()).collect() };
let mut row_ids = row_ids.unwrap_or_else(|| {
warn!("No feature ID column found in {var_path}/; using numeric IDs");
index(nrows)
});
listed("feature IDs", &row_ids, nrows)?;
let mut row_names = row_names.unwrap_or_else(|| {
info!("No gene symbol column found in {var_path}/; using index only");
vec![Box::from(""); nrows]
});
listed("gene symbols", &row_names, nrows)?;
let has_types = typed.is_some();
let row_types: Vec<Box<str>> = typed.unwrap_or_else(|| vec![Box::from(""); nrows]);
listed("feature types", &row_types, nrows)?;
let n_peaks = colon_peak_names(
&mut row_ids,
&mut row_names,
has_types.then_some(&row_types[..]),
);
if n_peaks > 0 {
info!("{n_peaks} peak names rewritten in chr:start-end form");
}
let mut row_ids = compose_id_name(row_ids, row_names);
make_names_unique(&mut row_ids);
let column_names = column_names.unwrap_or_else(|| {
warn!("No barcode column found in obs/; using numeric IDs");
index(ncols)
});
listed("barcodes", &column_names, ncols)?;
info!("Read {} barcodes", column_names.len());
log_feature_type_histogram(args.h5ad_file.as_ref(), &row_types);
let select_rows =
filter_row_indices_by_type(&row_types, &args.select_row_type, &args.remove_row_type);
crate::convert::ensure_rows_kept(
select_rows.len(),
nrows,
has_types.then_some(&row_types[..]),
(&args.select_row_type, &args.remove_row_type),
None,
)?;
let mut data = create_sparse_from_triplets_owned(
triplets,
(nrows, ncols, nnz),
Some(backend_file),
Some(&backend),
)?;
info!("created sparse matrix: {}", backend_file);
data.register_row_names_vec(&row_ids);
let obs_df = read_h5ad_dataframe(&obs_group)?;
info!(
"Read {} obs columns: {:?}",
obs_df.col_names.len(),
obs_df.col_names
);
let output_stem = crate::hdf5_io::strip_backend_suffix(effective_output);
let donor_idx = obs_df
.col_names
.iter()
.position(|c| c.as_ref() == "donor_id");
if let Some(di) = donor_idx {
let donors = &obs_df.col_data[di];
let new_col_names: Vec<Box<str>> = column_names
.iter()
.zip(donors.iter())
.map(|(b, d)| format!("{}@{}", b, d).into_boxed_str())
.collect();
data.register_column_names_vec(&new_col_names);
info!("Column names set to barcode@donor_id");
let donor_file = format!("{}.barcode_to_donor.tsv.gz", output_stem);
let mut lines: Vec<Box<str>> = Vec::with_capacity(column_names.len() + 1);
lines.push("barcode\tdonor_id".into());
for (b, d) in column_names.iter().zip(donors.iter()) {
lines.push(format!("{}\t{}", b, d).into());
}
write_lines(&lines, &donor_file)?;
info!("Wrote {}", donor_file);
let sample_file = format!("{}.sample_metadata.tsv.gz", output_stem);
write_sample_metadata(&sample_file, &obs_df.col_names, &obs_df.col_data, di)?;
info!("Wrote {}", sample_file);
} else {
data.register_column_names_vec(&column_names);
}
if select_rows.len() < nrows {
info!(
"filtering features: {} -> {} of `{}` type",
nrows,
select_rows.len(),
args.select_row_type
);
data.subset_columns_rows(None, Some(&select_rows))?;
}
let meta_file = format!("{}.cell_metadata.tsv.gz", output_stem);
let mut lines: Vec<Box<str>> = Vec::with_capacity(column_names.len() + 1);
let header = std::iter::once("barcode")
.chain(obs_df.col_names.iter().map(|s| s.as_ref()))
.collect::<Vec<&str>>()
.join("\t");
lines.push(header.into());
for i in 0..column_names.len() {
let row = std::iter::once(column_names[i].as_ref())
.chain(obs_df.col_data.iter().map(|col| col[i].as_ref()))
.collect::<Vec<&str>>()
.join("\t");
lines.push(row.into());
}
write_lines(&lines, &meta_file)?;
info!("Wrote {}", meta_file);
drop(data);
Ok(staged)
}
fn str_attr(loc: &hdf5::Location, name: &str) -> Option<String> {
use hdf5::types::{FixedAscii, FixedUnicode, VarLenAscii, VarLenUnicode};
let attr = loc.attr(name).ok()?;
attr.read_scalar::<VarLenUnicode>()
.map(|s| s.to_string())
.or_else(|_| attr.read_scalar::<VarLenAscii>().map(|s| s.to_string()))
.or_else(|_| {
attr.read_scalar::<FixedUnicode<64>>()
.map(|s| s.to_string())
})
.or_else(|_| attr.read_scalar::<FixedAscii<64>>().map(|s| s.to_string()))
.ok()
}
fn sparse_pointer_type(
x: &hdf5::Group,
x_path: &str,
n_obs: Option<usize>,
n_var: Option<usize>,
) -> anyhow::Result<IndexPointerType> {
let named = str_attr(x, "encoding-type").or_else(|| str_attr(x, "h5sparse_format"));
let format = match named {
Some(format) => format.to_ascii_lowercase(),
None => {
let vectors = x.dataset("indptr")?.size().checked_sub(1);
let over = |n: Option<usize>| vectors.is_some() && vectors == n;
match (over(n_obs), over(n_var)) {
(true, false) => "csr".into(),
(false, true) => "csc".into(),
_ => anyhow::bail!(
"{x_path} has no `encoding-type` or `h5sparse_format` attribute to tell \
CSR from CSC, and its {} compressed vectors do not tell either for \
{} cells and {} features",
vectors.map_or("no".into(), |n| n.to_string()),
n_obs.map_or("unknown".into(), |n| n.to_string()),
n_var.map_or("unknown".into(), |n| n.to_string()),
),
}
}
};
if format.contains("csr") {
info!("Sparse format: CSR -> transposing to (features x cells)");
Ok(IndexPointerType::Column)
} else if format.contains("csc") {
info!("Sparse format: CSC -> transposing to (features x cells)");
Ok(IndexPointerType::Row)
} else {
anyhow::bail!("{x_path} is stored as `{format}`, neither CSR nor CSC")
}
}
fn declared_shape(x: &hdf5::Group) -> Option<(usize, usize)> {
["shape", "h5sparse_shape"].iter().find_map(|name| {
let shape: Vec<u64> = x.attr(name).ok()?.read_raw().ok()?;
(shape.len() == 2).then(|| (shape[0] as usize, shape[1] as usize))
})
}
fn index_candidates(group: &hdf5::Group, fields: &[Box<str>]) -> Vec<Box<str>> {
let named = str_attr(group, "_index").filter(|name| name != "_index");
fields
.iter()
.flat_map(|field| {
let index = named
.as_deref()
.filter(|_| field.trim() == "_index")
.map(Box::from);
index.into_iter().chain([field.clone()])
})
.collect()
}
fn write_sample_metadata(
path: &str,
col_names: &[Box<str>],
col_data: &[Vec<Box<str>>],
donor_col_idx: usize,
) -> anyhow::Result<()> {
use std::collections::BTreeMap;
let donors = &col_data[donor_col_idx];
let mut donor_first_row: BTreeMap<&str, usize> = BTreeMap::new();
for (i, d) in donors.iter().enumerate() {
donor_first_row.entry(d.as_ref()).or_insert(i);
}
let mut lines: Vec<Box<str>> = Vec::with_capacity(donor_first_row.len() + 1);
let header = std::iter::once("donor_id")
.chain(
col_names
.iter()
.enumerate()
.filter(|&(j, _)| j != donor_col_idx)
.map(|(_, s)| s.as_ref()),
)
.collect::<Vec<&str>>()
.join("\t");
lines.push(header.into());
for (donor_id, &row_i) in &donor_first_row {
let row = std::iter::once(*donor_id)
.chain(
col_data
.iter()
.enumerate()
.filter(|&(j, _)| j != donor_col_idx)
.map(|(_, col)| col[row_i].as_ref()),
)
.collect::<Vec<&str>>()
.join("\t");
lines.push(row.into());
}
write_lines(&lines, path)?;
Ok(())
}