modewise_signals/
series.rs1use std::collections::HashSet;
2
3use ndarray::{Array2, ArrayView2};
4
5use crate::{SamplingRate, SignalError};
6
7#[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}