sc_neurocore_engine/analysis/
waveform.rs1pub fn waveform_width(waveform: &[f64], dt: f64) -> f64 {
11 if waveform.is_empty() {
12 return f64::NAN;
13 }
14 let trough = waveform
15 .iter()
16 .enumerate()
17 .min_by(|a, b| a.1.partial_cmp(b.1).unwrap())
18 .unwrap()
19 .0;
20 if trough >= waveform.len() - 1 {
21 return f64::NAN;
22 }
23 let peak = trough
24 + waveform[trough..]
25 .iter()
26 .enumerate()
27 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
28 .unwrap()
29 .0;
30 (peak - trough) as f64 * dt
31}
32
33pub fn waveform_amplitude(waveform: &[f64]) -> f64 {
35 if waveform.is_empty() {
36 return 0.0;
37 }
38 let max = waveform.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
39 let min = waveform.iter().cloned().fold(f64::INFINITY, f64::min);
40 max - min
41}
42
43pub fn waveform_repolarization_slope(waveform: &[f64], dt: f64) -> f64 {
45 if waveform.is_empty() {
46 return f64::NAN;
47 }
48 let trough = waveform
49 .iter()
50 .enumerate()
51 .min_by(|a, b| a.1.partial_cmp(b.1).unwrap())
52 .unwrap()
53 .0;
54 if trough >= waveform.len() - 2 {
55 return f64::NAN;
56 }
57 let post = &waveform[trough..];
58 let mut max_dv = f64::NEG_INFINITY;
59 for i in 0..post.len() - 1 {
60 let dv = (post[i + 1] - post[i]) / dt;
61 if dv > max_dv {
62 max_dv = dv;
63 }
64 }
65 max_dv
66}
67
68pub fn waveform_recovery_slope(waveform: &[f64], dt: f64) -> f64 {
70 if waveform.is_empty() {
71 return f64::NAN;
72 }
73 let trough = waveform
74 .iter()
75 .enumerate()
76 .min_by(|a, b| a.1.partial_cmp(b.1).unwrap())
77 .unwrap()
78 .0;
79 if trough >= waveform.len() - 1 {
80 return f64::NAN;
81 }
82 let peak = trough
83 + waveform[trough..]
84 .iter()
85 .enumerate()
86 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
87 .unwrap()
88 .0;
89 if peak >= waveform.len() - 2 {
90 return f64::NAN;
91 }
92 let post = &waveform[peak..];
93 let mut min_dv = f64::INFINITY;
94 for i in 0..post.len() - 1 {
95 let dv = (post[i + 1] - post[i]) / dt;
96 if dv < min_dv {
97 min_dv = dv;
98 }
99 }
100 min_dv
101}
102
103pub fn waveform_halfwidth(waveform: &[f64], dt: f64) -> f64 {
105 if waveform.is_empty() {
106 return f64::NAN;
107 }
108 let trough_val = waveform.iter().cloned().fold(f64::INFINITY, f64::min);
109 let half_val = trough_val / 2.0;
110 let below: Vec<usize> = waveform
111 .iter()
112 .enumerate()
113 .filter(|(_, &v)| v < half_val)
114 .map(|(i, _)| i)
115 .collect();
116 if below.len() < 2 {
117 return f64::NAN;
118 }
119 (below[below.len() - 1] - below[0]) as f64 * dt
120}
121
122pub fn waveform_pt_ratio(waveform: &[f64]) -> f64 {
124 if waveform.is_empty() {
125 return f64::NAN;
126 }
127 let trough = waveform
128 .iter()
129 .enumerate()
130 .min_by(|a, b| a.1.partial_cmp(b.1).unwrap())
131 .unwrap()
132 .0;
133 let trough_val = waveform[trough].abs();
134 if trough >= waveform.len() - 1 || trough_val < 1e-30 {
135 return f64::NAN;
136 }
137 let peak_val = waveform[trough..]
138 .iter()
139 .cloned()
140 .fold(f64::NEG_INFINITY, f64::max);
141 peak_val.abs() / trough_val
142}
143
144#[cfg(test)]
145mod tests {
146 use super::*;
147
148 fn typical_waveform() -> Vec<f64> {
149 vec![
151 0.0, 0.1, 0.0, -0.3, -0.8, -1.0, -0.7, -0.2, 0.3, 0.5, 0.4, 0.2, 0.1, 0.0,
152 ]
153 }
154
155 #[test]
156 fn test_waveform_width() {
157 let wf = typical_waveform();
158 let dt = 1.0 / 30000.0;
159 let w = waveform_width(&wf, dt);
160 assert!((w - 4.0 * dt).abs() < 1e-12);
162 }
163
164 #[test]
165 fn test_waveform_width_empty() {
166 assert!(waveform_width(&[], 1.0 / 30000.0).is_nan());
167 }
168
169 #[test]
170 fn test_waveform_amplitude() {
171 let wf = typical_waveform();
172 let amp = waveform_amplitude(&wf);
173 assert!((amp - 1.5).abs() < 1e-12);
175 }
176
177 #[test]
178 fn test_waveform_amplitude_empty() {
179 assert_eq!(waveform_amplitude(&[]), 0.0);
180 }
181
182 #[test]
183 fn test_repolarization_slope() {
184 let wf = typical_waveform();
185 let dt = 1.0 / 30000.0;
186 let slope = waveform_repolarization_slope(&wf, dt);
187 assert!(slope > 0.0);
188 let expected = 0.5 / dt;
191 assert!((slope - expected).abs() < 1e-6);
192 }
193
194 #[test]
195 fn test_recovery_slope() {
196 let wf = typical_waveform();
197 let dt = 1.0 / 30000.0;
198 let slope = waveform_recovery_slope(&wf, dt);
199 assert!(slope < 0.0);
201 }
202
203 #[test]
204 fn test_halfwidth() {
205 let wf = typical_waveform();
206 let dt = 1.0 / 30000.0;
207 let hw = waveform_halfwidth(&wf, dt);
208 assert!((hw - 2.0 * dt).abs() < 1e-12);
211 }
212
213 #[test]
214 fn test_pt_ratio() {
215 let wf = typical_waveform();
216 let ratio = waveform_pt_ratio(&wf);
217 assert!((ratio - 0.5).abs() < 1e-12);
219 }
220
221 #[test]
222 fn test_pt_ratio_no_peak() {
223 let wf = vec![0.0, -1.0];
224 assert!(waveform_pt_ratio(&wf).is_nan());
225 }
226
227 #[test]
228 fn test_flat_waveform() {
229 let wf = vec![0.5; 10];
230 let w = waveform_width(&wf, 1.0 / 30000.0);
234 assert!(w.abs() < 1e-6 || w >= 0.0, "Width for flat waveform: {w}");
236 }
237
238 #[test]
239 fn test_single_sample() {
240 assert!(waveform_width(&[1.0], 0.001).is_nan());
241 assert!((waveform_amplitude(&[5.0])).abs() < 1e-12);
242 }
243
244 #[test]
245 fn test_negative_only_waveform() {
246 let wf = vec![-0.5, -1.0, -0.3];
247 let dt = 1.0 / 30000.0;
248 let w = waveform_width(&wf, dt);
249 assert!((w - dt).abs() < 1e-12);
251 }
252}