use std::f64::consts::PI;
pub const SPEED_OF_SOUND: f64 = 343.0;
#[derive(Debug, Clone, Copy)]
pub struct Echo {
pub sample_idx: usize,
pub rtt: f64,
pub distance: f64,
pub amplitude: f64,
pub confidence: f64,
}
impl Echo {
pub fn from_rtt(rtt: f64, amplitude: f64) -> Self {
Self {
sample_idx: 0,
rtt,
distance: rtt * SPEED_OF_SOUND / 2.0,
amplitude,
confidence: amplitude,
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct SpatialPoint {
pub x: f64,
pub y: f64,
pub z: f64,
pub intensity: f64,
}
#[derive(Debug, Clone, Copy)]
pub struct PulseConfig {
pub frequency: f64, pub duration: f64, pub sample_rate: f64, pub amplitude: f64, }
impl Default for PulseConfig {
fn default() -> Self {
Self {
frequency: 40_000.0,
duration: 0.001,
sample_rate: 1_000_000.0,
amplitude: 1.0,
}
}
}
pub fn generate_pulse(config: &PulseConfig) -> Vec<f64> {
let n_samples = (config.duration * config.sample_rate) as usize;
(0..n_samples)
.map(|i| {
let t = i as f64 / config.sample_rate;
config.amplitude * (2.0 * PI * config.frequency * t).sin()
})
.collect()
}
pub fn detect_echoes(
signal: &[f64],
sample_rate: f64,
threshold: f64,
min_separation: f64,
) -> Vec<Echo> {
let min_sep_samples = (min_separation * sample_rate) as usize;
let mut echoes = Vec::new();
let mut last_peak = 0isize;
for i in 1..signal.len().saturating_sub(1) {
if signal[i] > threshold
&& signal[i] > signal[i - 1]
&& signal[i] > signal[i + 1]
&& (last_peak < 0 || (i as isize - last_peak) as usize >= min_sep_samples)
{
let rtt = i as f64 / sample_rate;
echoes.push(Echo {
sample_idx: i,
rtt,
distance: rtt * SPEED_OF_SOUND / 2.0,
amplitude: signal[i],
confidence: signal[i],
});
last_peak = i as isize;
}
}
echoes
}
pub fn beamform(signals: &[Vec<f64>], delays: &[f64], sample_rate: f64) -> Vec<f64> {
if signals.is_empty() || delays.len() != signals.len() {
return vec![];
}
let max_len = signals.iter().map(|s| s.len()).max().unwrap_or(0);
if max_len == 0 {
return vec![];
}
let mut output = vec![0.0f64; max_len];
let mut counts = vec![0usize; max_len];
for (signal, &delay) in signals.iter().zip(delays.iter()) {
let delay_samples = (delay * sample_rate).round() as isize;
for (i, &val) in signal.iter().enumerate() {
let out_idx = i as isize + delay_samples;
if out_idx >= 0 && (out_idx as usize) < max_len {
output[out_idx as usize] += val;
counts[out_idx as usize] += 1;
}
}
}
for i in 0..max_len {
if counts[i] > 0 {
output[i] /= counts[i] as f64;
}
}
output
}
pub fn echo_to_spatial(echo: &Echo, angle: f64) -> SpatialPoint {
SpatialPoint {
x: echo.distance * angle.cos(),
y: echo.distance * angle.sin(),
z: 0.0,
intensity: echo.amplitude,
}
}
pub fn build_spatial_map(sweeps: &[(f64, Vec<Echo>)]) -> Vec<SpatialPoint> {
sweeps
.iter()
.flat_map(|(angle, echoes)| echoes.iter().map(|e| echo_to_spatial(e, *angle)))
.collect()
}
pub fn compute_snr(signal: &[f64]) -> f64 {
if signal.len() < 2 {
return 0.0;
}
let mean: f64 = signal.iter().sum::<f64>() / signal.len() as f64;
let signal_power: f64 = signal.iter().map(|x| x.powi(2)).sum::<f64>() / signal.len() as f64;
let noise_power = (signal_power - mean * mean).max(0.0);
if noise_power == 0.0 {
return f64::INFINITY;
}
10.0 * (signal_power / noise_power).log10()
}
pub fn matched_filter(signal: &[f64], template: &[f64]) -> Vec<f64> {
if signal.len() < template.len() {
return vec![];
}
let out_len = signal.len() - template.len() + 1;
(0..out_len)
.map(|i| {
signal[i..i + template.len()]
.iter()
.zip(template.iter())
.map(|(s, t)| s * t)
.sum()
})
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_generate_pulse() {
let config = PulseConfig::default();
let pulse = generate_pulse(&config);
assert!(!pulse.is_empty());
assert!(pulse.iter().all(|&s| s.abs() <= 1.0));
}
#[test]
fn test_detect_echoes() {
let mut signal = vec![0.0; 1000];
signal[100] = 0.8;
signal[500] = 0.6;
let echoes = detect_echoes(&signal, 1_000_000.0, 0.5, 0.0001);
assert_eq!(echoes.len(), 2);
assert!((echoes[0].distance - 0.01715).abs() < 0.001);
}
#[test]
fn test_echo_from_rtt() {
let echo = Echo::from_rtt(0.01, 0.9);
assert!((echo.distance - 1.715).abs() < 0.01);
}
#[test]
fn test_beamform() {
let signals = vec![vec![1.0, 2.0, 3.0, 0.0], vec![0.0, 1.0, 2.0, 3.0]];
let delays = vec![0.0, 0.000001];
let output = beamform(&signals, &delays, 1_000_000.0);
assert!(!output.is_empty());
}
#[test]
fn test_echo_to_spatial() {
let echo = Echo::from_rtt(0.01, 0.9);
let point = echo_to_spatial(&echo, 0.0); assert!((point.x - 1.715).abs() < 0.01);
assert!(point.y.abs() < 0.01);
}
#[test]
fn test_spatial_map() {
let sweeps = vec![
(0.0, vec![Echo::from_rtt(0.01, 0.9)]),
(PI / 4.0, vec![Echo::from_rtt(0.02, 0.7)]),
];
let map = build_spatial_map(&sweeps);
assert_eq!(map.len(), 2);
}
#[test]
fn test_matched_filter() {
let template = vec![1.0, 1.0, 1.0];
let signal = vec![0.0, 0.0, 1.0, 1.0, 1.0, 0.0, 0.0];
let result = matched_filter(&signal, &template);
assert_eq!(result.len(), 5);
assert!((result[2] - 3.0).abs() < 0.001);
}
#[test]
fn test_snr() {
let signal = vec![1.0, 1.0, 1.0, 1.0];
let snr = compute_snr(&signal);
assert!(snr.is_infinite() || snr > 30.0); }
}