dspk_synapse/features/
conduction.rs1use serde::{Deserialize, Serialize};
8use dspk_ops::math::{cross_correlation, peak_lag};
9
10const MAX_LAG_FRACTION: usize = 3;
12const MIN_PAIR_ENERGY: f32 = 1e-8;
14const MIN_PAIR_CORRELATION: f32 = 0.2;
16const MIN_DELAY_SAMPLES: f32 = 1e-3;
18
19impl ConductionVelocityEstimate {
20 pub const UNDEFINED: Self = Self { velocity_m_per_s: f32::NAN, mean_delay_ms: f32::NAN, correlation_r: f32::NAN };
22}
23
24#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
26pub struct ConductionVelocityEstimate {
27 pub velocity_m_per_s: f32,
29 pub mean_delay_ms: f32,
31 pub correlation_r: f32,
33}
34
35pub fn estimate_hdemg_conduction_velocity(
39 waveform: &[f32],
40 rows: usize,
41 cols: usize,
42 samples: usize,
43 sample_rate_hz: f64,
44 pitch_um: f32,
45) -> ConductionVelocityEstimate {
46 assert_eq!(waveform.len(), rows * cols * samples);
47 if rows == 0 || cols < 3 || samples < 5 {
48 return ConductionVelocityEstimate::UNDEFINED;
49 }
50
51 let mut best_row = 0usize;
53 let mut best_energy = -1.0f32;
54 for r in 0..rows {
55 let mut e = 0.0f32;
56 for c in 0..cols {
57 let ch = r * cols + c;
58 let slice = &waveform[ch * samples..(ch + 1) * samples];
59 e += slice.iter().map(|v| v * v).sum::<f32>();
60 }
61 if e > best_energy {
62 best_energy = e;
63 best_row = r;
64 }
65 }
66
67 let num_sd = cols - 1;
69 let mut sd = vec![0.0f32; num_sd * samples];
70 for c in 0..num_sd {
71 let ch0 = best_row * cols + c;
72 let ch1 = best_row * cols + c + 1;
73 for t in 0..samples {
74 sd[c * samples + t] = waveform[ch1 * samples + t] - waveform[ch0 * samples + t];
75 }
76 }
77
78 let max_lag = (samples / MAX_LAG_FRACTION).max(2);
80 let mut weighted_delay_samples = 0.0f32;
81 let mut weight_sum = 0.0f32;
82 let mut corr_sum = 0.0f32;
83 let mut pair_count = 0usize;
84
85 for c in 0..(num_sd - 1) {
86 let x = &sd[c * samples..(c + 1) * samples];
87 let y = &sd[(c + 1) * samples..(c + 2) * samples];
88 let ex: f32 = x.iter().map(|v| v * v).sum();
89 let ey: f32 = y.iter().map(|v| v * v).sum();
90 let denom = (ex * ey).sqrt();
91 if denom < MIN_PAIR_ENERGY {
92 continue;
93 }
94
95 let corrs: Vec<f32> = cross_correlation(x, y, max_lag).into_iter().map(|c| c / denom).collect();
96 let Some(peak) = peak_lag(&corrs, max_lag) else { continue };
97 let best_r = peak.value;
98 let lag_samples = peak.fractional_lag().abs();
99 if best_r > MIN_PAIR_CORRELATION && lag_samples > MIN_DELAY_SAMPLES {
100 let w = best_r * denom;
101 weighted_delay_samples += w * lag_samples;
102 weight_sum += w;
103 corr_sum += best_r;
104 pair_count += 1;
105 }
106 }
107
108 if weight_sum <= MIN_PAIR_ENERGY || pair_count == 0 {
109 return ConductionVelocityEstimate::UNDEFINED;
110 }
111
112 let mean_lag_samples = weighted_delay_samples / weight_sum;
113 let delay_sec = (mean_lag_samples as f64) / sample_rate_hz.max(1.0);
114 let pitch_m = (pitch_um as f64) * 1e-6;
115 let velocity_m_per_s = (pitch_m / delay_sec) as f32;
117
118 ConductionVelocityEstimate {
119 velocity_m_per_s,
120 mean_delay_ms: (delay_sec * 1e3) as f32,
121 correlation_r: corr_sum / (pair_count as f32),
122 }
123}
124
125#[cfg(test)]
126mod tests {
127 use super::*;
128
129 #[test]
130 fn test_hdemg_conduction_velocity_estimation() {
131 let (rows, cols, samples) = (4, 8, 96);
134 let pitch_um = 8000.0f32;
135 let fs = 4000.0f64;
136 let mut wave = vec![0.0f32; rows * cols * samples];
137
138 for c in 0..cols {
139 let ch = 1 * cols + c;
140 let center = 20.0 + (c as f32) * 8.0;
141 for t in 0..samples {
142 let u = (t as f32 - center) / 3.5;
143 wave[ch * samples + t] = -100.0 * u * (-0.5 * u * u).exp() * (c as f32 * 0.05 + 1.0);
145 }
146 }
147
148 let est = estimate_hdemg_conduction_velocity(&wave, rows, cols, samples, fs, pitch_um);
149 assert!(
150 (est.velocity_m_per_s - 4.0).abs() < 0.3,
151 "est velocity = {} m/s, expected 4.0 m/s",
152 est.velocity_m_per_s
153 );
154 assert!(est.correlation_r > 0.9);
155 }
156}