1use std::path::Path;
4
5use hdf5_pure::{DType, File};
6use modewise_signals::{MultichannelSeries, SamplingRate, SignalError};
7use ndarray::Array2;
8use thiserror::Error;
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
12pub enum Layout {
13 #[default]
14 ChannelsByTime,
15 TimeByChannels,
16}
17
18#[derive(Debug, Error)]
19pub enum Hdf5Error {
20 #[error("cannot open {path}: {message}")]
21 Open { path: String, message: String },
22 #[error("cannot read dataset {dataset:?}: {message}")]
23 Dataset { dataset: String, message: String },
24 #[error("dataset {dataset:?} must be 2D (channels and time), got shape {shape:?}")]
25 NotTwoDimensional { dataset: String, shape: Vec<u64> },
26 #[error("dataset {dataset:?} is not numeric ({dtype})")]
27 NotNumeric { dataset: String, dtype: String },
28 #[error(transparent)]
29 Signal(#[from] SignalError),
30}
31
32pub fn read_series(
35 path: &Path,
36 dataset: &str,
37 layout: Layout,
38 sampling_rate: SamplingRate,
39 labels: Option<Vec<String>>,
40) -> Result<MultichannelSeries, Hdf5Error> {
41 let file = File::open(path).map_err(|e| Hdf5Error::Open {
42 path: path.display().to_string(),
43 message: e.to_string(),
44 })?;
45 let dataset_error = |message: String| Hdf5Error::Dataset {
46 dataset: dataset.to_string(),
47 message,
48 };
49 let ds = file
50 .dataset(dataset)
51 .map_err(|e| dataset_error(e.to_string()))?;
52
53 let dtype = ds.dtype().map_err(|e| dataset_error(e.to_string()))?;
54 if !is_numeric(&dtype) {
55 return Err(Hdf5Error::NotNumeric {
56 dataset: dataset.to_string(),
57 dtype: format!("{dtype:?}"),
58 });
59 }
60 let shape = ds.shape().map_err(|e| dataset_error(e.to_string()))?;
61 let [rows, cols] = shape[..] else {
62 return Err(Hdf5Error::NotTwoDimensional {
63 dataset: dataset.to_string(),
64 shape,
65 });
66 };
67 let values = ds.read_f64().map_err(|e| dataset_error(e.to_string()))?;
68 let stored = Array2::from_shape_vec((rows as usize, cols as usize), values)
69 .map_err(|e| dataset_error(e.to_string()))?;
70 let data = match layout {
71 Layout::ChannelsByTime => stored,
72 Layout::TimeByChannels => stored.reversed_axes().as_standard_layout().into_owned(),
73 };
74
75 let labels = labels.unwrap_or_else(|| (0..data.nrows()).map(|i| format!("ch{i}")).collect());
76 Ok(MultichannelSeries::new(data, sampling_rate, labels)?)
77}
78
79fn is_numeric(dtype: &DType) -> bool {
80 use DType::*;
81 matches!(
82 dtype,
83 F32 | F64 | I8 | I16 | I32 | I64 | U8 | U16 | U32 | U64
84 )
85}