modewise_signals/
series.rs

1use std::collections::HashSet;
2
3use ndarray::{Array2, ArrayView2};
4
5use crate::{SamplingRate, SignalError};
6
7/// Equally sampled signals, laid out as channels × samples.
8#[derive(Debug, Clone, PartialEq)]
9pub struct MultichannelSeries {
10    data: Array2<f64>,
11    sampling_rate: SamplingRate,
12    labels: Vec<String>,
13}
14
15impl MultichannelSeries {
16    pub fn new(
17        data: Array2<f64>,
18        sampling_rate: SamplingRate,
19        labels: Vec<String>,
20    ) -> Result<Self, SignalError> {
21        let (n_channels, n_samples) = data.dim();
22        if n_channels == 0 {
23            return Err(SignalError::NoChannels);
24        }
25        if n_samples < 2 {
26            return Err(SignalError::TooFewSamples(n_samples));
27        }
28        if labels.len() != n_channels {
29            return Err(SignalError::LabelCountMismatch {
30                expected: n_channels,
31                actual: labels.len(),
32            });
33        }
34        let mut seen = HashSet::new();
35        if let Some(duplicate) = labels.iter().find(|label| !seen.insert(*label)) {
36            return Err(SignalError::DuplicateLabel(duplicate.clone()));
37        }
38        if let Some(((channel, sample), _)) = data.indexed_iter().find(|(_, v)| !v.is_finite()) {
39            return Err(SignalError::NonFiniteValue { channel, sample });
40        }
41        Ok(Self {
42            data,
43            sampling_rate,
44            labels,
45        })
46    }
47
48    pub fn data(&self) -> ArrayView2<'_, f64> {
49        self.data.view()
50    }
51
52    pub fn sampling_rate(&self) -> SamplingRate {
53        self.sampling_rate
54    }
55
56    pub fn labels(&self) -> &[String] {
57        &self.labels
58    }
59
60    pub fn n_channels(&self) -> usize {
61        self.data.nrows()
62    }
63
64    pub fn n_samples(&self) -> usize {
65        self.data.ncols()
66    }
67
68    pub fn duration_s(&self) -> f64 {
69        self.n_samples() as f64 * self.sampling_rate.period_s()
70    }
71}
72
73#[cfg(test)]
74mod tests {
75    use ndarray::array;
76
77    use super::*;
78
79    fn fs() -> SamplingRate {
80        SamplingRate::new(2.0).unwrap()
81    }
82
83    fn labels(names: &[&str]) -> Vec<String> {
84        names.iter().map(|s| s.to_string()).collect()
85    }
86
87    #[test]
88    fn valid_series_exposes_shape_and_duration() {
89        let data = array![[1.0, 2.0, 3.0, 4.0], [5.0, 6.0, 7.0, 8.0]];
90        let series = MultichannelSeries::new(data, fs(), labels(&["a", "b"])).unwrap();
91        assert_eq!(series.n_channels(), 2);
92        assert_eq!(series.n_samples(), 4);
93        assert_eq!(series.duration_s(), 2.0);
94        assert_eq!(series.labels(), ["a", "b"]);
95        assert_eq!(series.data()[[1, 2]], 7.0);
96    }
97
98    #[test]
99    fn rejects_no_channels() {
100        let data = Array2::<f64>::zeros((0, 4));
101        let err = MultichannelSeries::new(data, fs(), vec![]).unwrap_err();
102        assert_eq!(err, SignalError::NoChannels);
103    }
104
105    #[test]
106    fn rejects_too_few_samples() {
107        let err = MultichannelSeries::new(array![[1.0]], fs(), labels(&["a"])).unwrap_err();
108        assert_eq!(err, SignalError::TooFewSamples(1));
109    }
110
111    #[test]
112    fn rejects_label_count_mismatch() {
113        let err =
114            MultichannelSeries::new(array![[1.0, 2.0]], fs(), labels(&["a", "b"])).unwrap_err();
115        assert_eq!(
116            err,
117            SignalError::LabelCountMismatch {
118                expected: 1,
119                actual: 2
120            }
121        );
122    }
123
124    #[test]
125    fn rejects_duplicate_labels() {
126        let data = array![[1.0, 2.0], [3.0, 4.0]];
127        let err = MultichannelSeries::new(data, fs(), labels(&["a", "a"])).unwrap_err();
128        assert_eq!(err, SignalError::DuplicateLabel("a".into()));
129    }
130
131    #[test]
132    fn rejects_non_finite_values() {
133        let data = array![[1.0, 2.0], [3.0, f64::NAN]];
134        let err = MultichannelSeries::new(data, fs(), labels(&["a", "b"])).unwrap_err();
135        assert_eq!(
136            err,
137            SignalError::NonFiniteValue {
138                channel: 1,
139                sample: 1
140            }
141        );
142    }
143}