1use std::f64::consts::PI;
4
5pub const SPEED_OF_SOUND: f64 = 343.0;
7
8#[derive(Debug, Clone, Copy)]
10pub struct Echo {
11 pub sample_idx: usize,
13 pub rtt: f64,
15 pub distance: f64,
17 pub amplitude: f64,
19 pub confidence: f64,
21}
22
23impl Echo {
24 pub fn from_rtt(rtt: f64, amplitude: f64) -> Self {
25 Self {
26 sample_idx: 0,
27 rtt,
28 distance: rtt * SPEED_OF_SOUND / 2.0,
29 amplitude,
30 confidence: amplitude,
31 }
32 }
33}
34
35#[derive(Debug, Clone, Copy)]
37pub struct SpatialPoint {
38 pub x: f64,
39 pub y: f64,
40 pub z: f64,
41 pub intensity: f64,
42}
43
44#[derive(Debug, Clone, Copy)]
46pub struct PulseConfig {
47 pub frequency: f64, pub duration: f64, pub sample_rate: f64, pub amplitude: f64, }
52
53impl Default for PulseConfig {
54 fn default() -> Self {
55 Self {
56 frequency: 40_000.0,
57 duration: 0.001,
58 sample_rate: 1_000_000.0,
59 amplitude: 1.0,
60 }
61 }
62}
63
64pub fn generate_pulse(config: &PulseConfig) -> Vec<f64> {
66 let n_samples = (config.duration * config.sample_rate) as usize;
67 (0..n_samples)
68 .map(|i| {
69 let t = i as f64 / config.sample_rate;
70 config.amplitude * (2.0 * PI * config.frequency * t).sin()
71 })
72 .collect()
73}
74
75pub fn detect_echoes(
77 signal: &[f64],
78 sample_rate: f64,
79 threshold: f64,
80 min_separation: f64,
81) -> Vec<Echo> {
82 let min_sep_samples = (min_separation * sample_rate) as usize;
83 let mut echoes = Vec::new();
84 let mut last_peak = 0isize;
85
86 for i in 1..signal.len().saturating_sub(1) {
87 if signal[i] > threshold
88 && signal[i] > signal[i - 1]
89 && signal[i] > signal[i + 1]
90 && (last_peak < 0 || (i as isize - last_peak) as usize >= min_sep_samples)
91 {
92 let rtt = i as f64 / sample_rate;
93 echoes.push(Echo {
94 sample_idx: i,
95 rtt,
96 distance: rtt * SPEED_OF_SOUND / 2.0,
97 amplitude: signal[i],
98 confidence: signal[i],
99 });
100 last_peak = i as isize;
101 }
102 }
103 echoes
104}
105
106pub fn beamform(signals: &[Vec<f64>], delays: &[f64], sample_rate: f64) -> Vec<f64> {
108 if signals.is_empty() || delays.len() != signals.len() {
109 return vec![];
110 }
111 let max_len = signals.iter().map(|s| s.len()).max().unwrap_or(0);
112 if max_len == 0 {
113 return vec![];
114 }
115
116 let mut output = vec![0.0f64; max_len];
117 let mut counts = vec![0usize; max_len];
118
119 for (signal, &delay) in signals.iter().zip(delays.iter()) {
120 let delay_samples = (delay * sample_rate).round() as isize;
121 for (i, &val) in signal.iter().enumerate() {
122 let out_idx = i as isize + delay_samples;
123 if out_idx >= 0 && (out_idx as usize) < max_len {
124 output[out_idx as usize] += val;
125 counts[out_idx as usize] += 1;
126 }
127 }
128 }
129
130 for i in 0..max_len {
131 if counts[i] > 0 {
132 output[i] /= counts[i] as f64;
133 }
134 }
135 output
136}
137
138pub fn echo_to_spatial(echo: &Echo, angle: f64) -> SpatialPoint {
140 SpatialPoint {
141 x: echo.distance * angle.cos(),
142 y: echo.distance * angle.sin(),
143 z: 0.0,
144 intensity: echo.amplitude,
145 }
146}
147
148pub fn build_spatial_map(sweeps: &[(f64, Vec<Echo>)]) -> Vec<SpatialPoint> {
150 sweeps
151 .iter()
152 .flat_map(|(angle, echoes)| echoes.iter().map(|e| echo_to_spatial(e, *angle)))
153 .collect()
154}
155
156pub fn compute_snr(signal: &[f64]) -> f64 {
158 if signal.len() < 2 {
159 return 0.0;
160 }
161 let mean: f64 = signal.iter().sum::<f64>() / signal.len() as f64;
162 let signal_power: f64 = signal.iter().map(|x| x.powi(2)).sum::<f64>() / signal.len() as f64;
163 let noise_power = (signal_power - mean * mean).max(0.0);
164 if noise_power == 0.0 {
165 return f64::INFINITY;
166 }
167 10.0 * (signal_power / noise_power).log10()
168}
169
170pub fn matched_filter(signal: &[f64], template: &[f64]) -> Vec<f64> {
172 if signal.len() < template.len() {
173 return vec![];
174 }
175 let out_len = signal.len() - template.len() + 1;
176 (0..out_len)
177 .map(|i| {
178 signal[i..i + template.len()]
179 .iter()
180 .zip(template.iter())
181 .map(|(s, t)| s * t)
182 .sum()
183 })
184 .collect()
185}
186
187#[cfg(test)]
188mod tests {
189 use super::*;
190
191 #[test]
192 fn test_generate_pulse() {
193 let config = PulseConfig::default();
194 let pulse = generate_pulse(&config);
195 assert!(!pulse.is_empty());
196 assert!(pulse.iter().all(|&s| s.abs() <= 1.0));
197 }
198
199 #[test]
200 fn test_detect_echoes() {
201 let mut signal = vec![0.0; 1000];
203 signal[100] = 0.8;
204 signal[500] = 0.6;
205 let echoes = detect_echoes(&signal, 1_000_000.0, 0.5, 0.0001);
206 assert_eq!(echoes.len(), 2);
207 assert!((echoes[0].distance - 0.01715).abs() < 0.001);
208 }
209
210 #[test]
211 fn test_echo_from_rtt() {
212 let echo = Echo::from_rtt(0.01, 0.9);
213 assert!((echo.distance - 1.715).abs() < 0.01);
214 }
215
216 #[test]
217 fn test_beamform() {
218 let signals = vec![vec![1.0, 2.0, 3.0, 0.0], vec![0.0, 1.0, 2.0, 3.0]];
219 let delays = vec![0.0, 0.000001];
220 let output = beamform(&signals, &delays, 1_000_000.0);
221 assert!(!output.is_empty());
222 }
223
224 #[test]
225 fn test_echo_to_spatial() {
226 let echo = Echo::from_rtt(0.01, 0.9);
227 let point = echo_to_spatial(&echo, 0.0); assert!((point.x - 1.715).abs() < 0.01);
229 assert!(point.y.abs() < 0.01);
230 }
231
232 #[test]
233 fn test_spatial_map() {
234 let sweeps = vec![
235 (0.0, vec![Echo::from_rtt(0.01, 0.9)]),
236 (PI / 4.0, vec![Echo::from_rtt(0.02, 0.7)]),
237 ];
238 let map = build_spatial_map(&sweeps);
239 assert_eq!(map.len(), 2);
240 }
241
242 #[test]
243 fn test_matched_filter() {
244 let template = vec![1.0, 1.0, 1.0];
245 let signal = vec![0.0, 0.0, 1.0, 1.0, 1.0, 0.0, 0.0];
246 let result = matched_filter(&signal, &template);
247 assert_eq!(result.len(), 5);
248 assert!((result[2] - 3.0).abs() < 0.001);
249 }
250
251 #[test]
252 fn test_snr() {
253 let signal = vec![1.0, 1.0, 1.0, 1.0];
254 let snr = compute_snr(&signal);
255 assert!(snr.is_infinite() || snr > 30.0); }
257}