1use crate::sparse_io::*;
2
3#[cfg(feature = "hdf5")]
4use hdf5::types::FixedAscii;
5#[cfg(feature = "hdf5")]
6use hdf5::types::FixedUnicode;
7#[cfg(feature = "hdf5")]
8use hdf5::types::TypeDescriptor;
9#[cfg(feature = "hdf5")]
10use hdf5::types::VarLenUnicode;
11
12#[cfg(feature = "hdf5")]
14pub struct H5adDataFrame {
15 pub col_names: Vec<Box<str>>,
16 pub col_data: Vec<Vec<Box<str>>>,
17}
18
19pub fn strip_backend_suffix(path: &str) -> &str {
22 path.strip_suffix(".zarr.zip")
23 .or_else(|| path.strip_suffix(".zarr"))
24 .or_else(|| path.strip_suffix(".h5"))
25 .unwrap_or(path)
26}
27
28pub fn resolve_backend_file(
40 file_path: &str,
41 backend: Option<SparseIoBackend>,
42) -> anyhow::Result<(SparseIoBackend, Box<str>)> {
43 use legume_numeric::matrix::common_io::file_ext;
44 let ext = file_ext(file_path).unwrap_or(Box::<str>::from(""));
45
46 if let Some(backend) = backend {
47 let mut resolved_backend = backend;
48 let mut backend_file = file_path.to_string();
49
50 match ext.as_ref() {
52 "zarr" => {
53 resolved_backend = SparseIoBackend::Zarr;
54 }
55 "h5" => {
56 resolved_backend = SparseIoBackend::HDF5;
57 }
58 "zip" if file_path.ends_with(".zarr.zip") => {
59 resolved_backend = SparseIoBackend::Zarr;
60 backend_file = file_path
62 .strip_suffix(".zip")
63 .unwrap_or(file_path)
64 .to_string();
65 }
66 _ => {
67 backend_file = match resolved_backend {
69 SparseIoBackend::HDF5 => format!("{}.h5", file_path),
70 SparseIoBackend::Zarr => format!("{}.zarr", file_path),
71 }
72 }
73 };
74
75 #[cfg(not(feature = "hdf5"))]
81 if resolved_backend == SparseIoBackend::HDF5 {
82 let stripped = strip_backend_suffix(&backend_file);
83 let new_path = format!("{}.zarr", stripped);
84 log::warn!(
85 "HDF5 output requested but this binary was built without the \
86 `hdf5` feature; writing Zarr instead ({} -> {}). Pass \
87 `--zip=true` (or use a `.zarr.zip` output path) for a zipped \
88 archive.",
89 &backend_file,
90 &new_path
91 );
92 return Ok((SparseIoBackend::Zarr, new_path.into_boxed_str()));
93 }
94
95 Ok((resolved_backend, backend_file.into_boxed_str()))
96 } else {
97 let resolved_backend = match ext.as_ref() {
99 "zarr" => SparseIoBackend::Zarr,
100 "h5" => SparseIoBackend::HDF5,
101 "zip" if file_path.ends_with(".zarr.zip") => SparseIoBackend::Zarr,
102 _ => return Err(anyhow::anyhow!("Unknown file format: {}", file_path)),
103 };
104
105 let backend_file = file_path.to_string();
108
109 Ok((resolved_backend, backend_file.into_boxed_str()))
110 }
111}
112
113#[cfg(feature = "hdf5")]
121pub fn read_h5ad_column(group: &hdf5::Group, col_name: &str) -> anyhow::Result<Vec<Box<str>>> {
122 if let Ok(col_group) = group.group(col_name) {
124 let categories = read_hdf5_strings(col_group.dataset("categories")?)?;
125 let codes_ds = col_group.dataset("codes")?;
126 let dtype = codes_ds.dtype()?;
127 let desc = dtype.to_descriptor()?;
128
129 let codes: Vec<i32> = match desc {
130 TypeDescriptor::Integer(sz) => match sz {
131 hdf5::types::IntSize::U1 => codes_ds
132 .read_1d::<i8>()?
133 .iter()
134 .map(|&x| x as i32)
135 .collect(),
136 hdf5::types::IntSize::U2 => codes_ds
137 .read_1d::<i16>()?
138 .iter()
139 .map(|&x| x as i32)
140 .collect(),
141 hdf5::types::IntSize::U4 => codes_ds.read_1d::<i32>()?.to_vec(),
142 hdf5::types::IntSize::U8 => codes_ds
143 .read_1d::<i64>()?
144 .iter()
145 .map(|&x| x as i32)
146 .collect(),
147 },
148 TypeDescriptor::Unsigned(sz) => match sz {
149 hdf5::types::IntSize::U1 => codes_ds
150 .read_1d::<u8>()?
151 .iter()
152 .map(|&x| x as i32)
153 .collect(),
154 hdf5::types::IntSize::U2 => codes_ds
155 .read_1d::<u16>()?
156 .iter()
157 .map(|&x| x as i32)
158 .collect(),
159 hdf5::types::IntSize::U4 => codes_ds
160 .read_1d::<u32>()?
161 .iter()
162 .map(|&x| x as i32)
163 .collect(),
164 hdf5::types::IntSize::U8 => codes_ds
165 .read_1d::<u64>()?
166 .iter()
167 .map(|&x| x as i32)
168 .collect(),
169 },
170 _ => {
171 return Err(anyhow::anyhow!(
172 "unsupported codes dtype for categorical '{}'",
173 col_name
174 ));
175 }
176 };
177
178 let result: Vec<Box<str>> = codes
179 .iter()
180 .map(|&c| {
181 if c < 0 {
182 "NA".to_string().into_boxed_str()
183 } else {
184 categories[c as usize].clone()
185 }
186 })
187 .collect();
188
189 return Ok(result);
190 }
191
192 let ds = group.dataset(col_name)?;
194 let dtype = ds.dtype()?;
195 let desc = dtype.to_descriptor()?;
196
197 match desc {
198 TypeDescriptor::VarLenUnicode
199 | TypeDescriptor::FixedAscii(_)
200 | TypeDescriptor::FixedUnicode(_) => read_hdf5_strings(ds),
201 TypeDescriptor::Boolean => {
202 let data = ds.read_1d::<bool>()?;
203 Ok(data
204 .iter()
205 .map(|&b| if b { "true" } else { "false" }.into())
206 .collect())
207 }
208 TypeDescriptor::Integer(_) => {
209 let data = ds.read_1d::<i64>()?;
210 Ok(data
211 .iter()
212 .map(|x| x.to_string().into_boxed_str())
213 .collect())
214 }
215 TypeDescriptor::Unsigned(_) => {
216 let data = ds.read_1d::<u64>()?;
217 Ok(data
218 .iter()
219 .map(|x| x.to_string().into_boxed_str())
220 .collect())
221 }
222 TypeDescriptor::Float(sz) => match sz {
223 hdf5::types::FloatSize::U4 => {
224 let data = ds.read_1d::<f32>()?;
225 Ok(data
226 .iter()
227 .map(|x| x.to_string().into_boxed_str())
228 .collect())
229 }
230 hdf5::types::FloatSize::U8 => {
231 let data = ds.read_1d::<f64>()?;
232 Ok(data
233 .iter()
234 .map(|x| x.to_string().into_boxed_str())
235 .collect())
236 }
237 },
238 _ => Err(anyhow::anyhow!(
239 "unsupported dtype for column '{}'",
240 col_name
241 )),
242 }
243}
244
245#[cfg(feature = "hdf5")]
251pub fn resolve_h5ad_field(
252 group: &hdf5::Group,
253 fields: &[Box<str>],
254 label: &str,
255) -> Option<Vec<Box<str>>> {
256 for field in fields {
257 let field = field.trim();
258 if field.is_empty() {
259 continue;
260 }
261 if let Ok(v) = read_h5ad_column(group, field) {
262 log::info!("Using '{}' for {} ({} entries)", field, label, v.len());
263 return Some(v);
264 }
265 }
266 None
267}
268
269#[cfg(feature = "hdf5")]
278pub fn read_h5ad_dataframe(group: &hdf5::Group) -> anyhow::Result<H5adDataFrame> {
279 let col_order: Vec<String> = match group.attr("column-order") {
280 Ok(attr) => match attr.read_1d::<VarLenUnicode>() {
281 Ok(arr) => arr.iter().map(|x| x.to_string()).collect(),
282 Err(e) => {
283 log::warn!("Failed to read column-order attribute: {}", e);
284 vec![]
285 }
286 },
287 Err(_) => {
288 log::warn!("No column-order attribute found; returning empty dataframe");
289 vec![]
290 }
291 };
292
293 let mut col_names = Vec::new();
294 let mut col_data = Vec::new();
295
296 for col_name in &col_order {
297 match read_h5ad_column(group, col_name) {
298 Ok(data) => {
299 col_names.push(col_name.clone().into_boxed_str());
300 col_data.push(data);
301 }
302 Err(e) => {
303 log::warn!("Skipping obs column '{}': {}", col_name, e);
304 }
305 }
306 }
307
308 Ok(H5adDataFrame {
309 col_names,
310 col_data,
311 })
312}
313
314#[cfg(feature = "hdf5")]
316pub fn read_hdf5_strings(data: hdf5::dataset::Dataset) -> anyhow::Result<Vec<Box<str>>> {
317 let dtype = data.dtype()?;
318 let desc = dtype.to_descriptor()?;
319
320 let ret: Vec<Box<str>> = match desc {
321 TypeDescriptor::VarLenUnicode => data
322 .read_1d::<VarLenUnicode>()?
323 .map(|x| x.to_string().into_boxed_str())
324 .into_iter()
325 .collect(),
326 TypeDescriptor::FixedAscii(n) => {
327 if n < 24 {
328 data.read_1d::<FixedAscii<24>>()?
329 .map(|x| x.to_string().into_boxed_str())
330 .into_iter()
331 .collect()
332 } else if n < 128 {
333 data.read_1d::<FixedAscii<128>>()?
334 .map(|x| x.to_string().into_boxed_str())
335 .into_iter()
336 .collect()
337 } else {
338 data.read_1d::<FixedAscii<1024>>()?
339 .map(|x| x.to_string().into_boxed_str())
340 .into_iter()
341 .collect()
342 }
343 }
344 TypeDescriptor::FixedUnicode(n) => {
345 if n < 24 {
346 data.read_1d::<FixedUnicode<24>>()?
347 .map(|x| x.to_string().into_boxed_str())
348 .into_iter()
349 .collect()
350 } else if n < 128 {
351 data.read_1d::<FixedUnicode<128>>()?
352 .map(|x| x.to_string().into_boxed_str())
353 .into_iter()
354 .collect()
355 } else {
356 data.read_1d::<FixedUnicode<1024>>()?
357 .map(|x| x.to_string().into_boxed_str())
358 .into_iter()
359 .collect()
360 }
361 }
362 _ => {
363 return Err(anyhow::anyhow!("unsupported string"));
364 }
365 };
366
367 Ok(ret)
368}
369
370