use std::collections::HashMap;
#[derive(Clone, Debug)]
pub struct Peak {
pub time: f64,
pub frequency: f64,
pub magnitude: f64,
pub bin: usize,
}
impl Peak {
#[must_use]
pub const fn new(time: f64, frequency: f64, magnitude: f64, bin: usize) -> Self {
Self {
time,
frequency,
magnitude,
bin,
}
}
#[must_use]
pub const fn quantized_frequency(&self) -> u32 {
self.bin as u32
}
#[must_use]
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
pub fn quantized_time(&self) -> u32 {
(self.time * 1000.0) as u32
}
#[must_use]
pub fn time_distance(&self, other: &Self) -> f64 {
(self.time - other.time).abs()
}
#[must_use]
pub fn frequency_distance(&self, other: &Self) -> f64 {
(self.frequency - other.frequency).abs()
}
#[must_use]
pub fn distance(&self, other: &Self, time_weight: f64, freq_weight: f64) -> f64 {
let time_dist = self.time_distance(other);
let freq_dist = self.frequency_distance(other);
(time_weight * time_dist * time_dist + freq_weight * freq_dist * freq_dist).sqrt()
}
}
#[derive(Clone, Debug)]
pub struct ConstellationMap {
pub peaks: Vec<Peak>,
pub sample_rate: u32,
pub duration: f64,
time_index: HashMap<u32, Vec<usize>>,
}
impl ConstellationMap {
#[must_use]
pub fn new(peaks: Vec<Peak>, sample_rate: u32, duration: f64) -> Self {
let time_index = Self::build_time_index(&peaks);
Self {
peaks,
sample_rate,
duration,
time_index,
}
}
fn build_time_index(peaks: &[Peak]) -> HashMap<u32, Vec<usize>> {
let mut index = HashMap::new();
for (i, peak) in peaks.iter().enumerate() {
let time_bin = peak.quantized_time() / 100; index.entry(time_bin).or_insert_with(Vec::new).push(i);
}
index
}
#[must_use]
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
pub fn peaks_in_time_range(&self, start_time: f64, end_time: f64) -> Vec<&Peak> {
let start_bin = ((start_time * 1000.0) as u32) / 100;
let end_bin = ((end_time * 1000.0) as u32) / 100;
let mut result = Vec::new();
for bin in start_bin..=end_bin {
if let Some(indices) = self.time_index.get(&bin) {
for &idx in indices {
if let Some(peak) = self.peaks.get(idx) {
if peak.time >= start_time && peak.time <= end_time {
result.push(peak);
}
}
}
}
}
result
}
#[must_use]
pub fn peaks_in_frequency_range(&self, min_freq: f64, max_freq: f64) -> Vec<&Peak> {
self.peaks
.iter()
.filter(|p| p.frequency >= min_freq && p.frequency <= max_freq)
.collect()
}
#[must_use]
pub fn nearest_peaks(
&self,
anchor: &Peak,
time_range: (f64, f64),
max_count: usize,
) -> Vec<&Peak> {
let start_time = anchor.time + time_range.0;
let end_time = anchor.time + time_range.1;
let mut candidates = self.peaks_in_time_range(start_time, end_time);
candidates.sort_by(|a, b| {
let dist_a = anchor.distance(a, 1.0, 0.1);
let dist_b = anchor.distance(b, 1.0, 0.1);
dist_a
.partial_cmp(&dist_b)
.unwrap_or(std::cmp::Ordering::Equal)
});
candidates.truncate(max_count);
candidates
}
#[must_use]
pub fn peak_count(&self) -> usize {
self.peaks.len()
}
#[must_use]
pub fn peak_density(&self) -> f64 {
if self.duration > 0.0 {
self.peaks.len() as f64 / self.duration
} else {
0.0
}
}
#[must_use]
pub fn frequency_range(&self) -> (f64, f64) {
if self.peaks.is_empty() {
return (0.0, 0.0);
}
let min_freq = self
.peaks
.iter()
.map(|p| p.frequency)
.min_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
.unwrap_or(0.0);
let max_freq = self
.peaks
.iter()
.map(|p| p.frequency)
.max_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
.unwrap_or(0.0);
(min_freq, max_freq)
}
#[must_use]
pub fn filter_by_magnitude(&self, threshold: f64) -> Self {
let filtered_peaks: Vec<Peak> = self
.peaks
.iter()
.filter(|p| p.magnitude >= threshold)
.cloned()
.collect();
Self::new(filtered_peaks, self.sample_rate, self.duration)
}
#[must_use]
pub fn filter_by_frequency(&self, min_freq: f64, max_freq: f64) -> Self {
let filtered_peaks: Vec<Peak> = self
.peaks
.iter()
.filter(|p| p.frequency >= min_freq && p.frequency <= max_freq)
.cloned()
.collect();
Self::new(filtered_peaks, self.sample_rate, self.duration)
}
#[must_use]
pub fn statistics(&self) -> ConstellationStatistics {
if self.peaks.is_empty() {
return ConstellationStatistics::default();
}
let total_magnitude: f64 = self.peaks.iter().map(|p| p.magnitude).sum();
let avg_magnitude = total_magnitude / self.peaks.len() as f64;
let max_magnitude = self
.peaks
.iter()
.map(|p| p.magnitude)
.max_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
.unwrap_or(0.0);
let min_magnitude = self
.peaks
.iter()
.map(|p| p.magnitude)
.min_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
.unwrap_or(0.0);
let (min_freq, max_freq) = self.frequency_range();
ConstellationStatistics {
peak_count: self.peaks.len(),
density: self.peak_density(),
avg_magnitude,
max_magnitude,
min_magnitude,
frequency_range: (min_freq, max_freq),
}
}
#[must_use]
pub fn subsample(&self, factor: usize) -> Self {
if factor <= 1 {
return self.clone();
}
let subsampled_peaks: Vec<Peak> = self.peaks.iter().step_by(factor).cloned().collect();
Self::new(subsampled_peaks, self.sample_rate, self.duration)
}
#[must_use]
pub fn merge(&self, other: &Self, time_offset: f64) -> Self {
let mut merged_peaks = self.peaks.clone();
for peak in &other.peaks {
let mut shifted_peak = peak.clone();
shifted_peak.time += time_offset;
merged_peaks.push(shifted_peak);
}
let total_duration = self.duration.max(other.duration + time_offset);
Self::new(merged_peaks, self.sample_rate, total_duration)
}
}
#[derive(Clone, Debug, Default)]
pub struct ConstellationStatistics {
pub peak_count: usize,
pub density: f64,
pub avg_magnitude: f64,
pub max_magnitude: f64,
pub min_magnitude: f64,
pub frequency_range: (f64, f64),
}
impl ConstellationStatistics {
#[must_use]
pub fn is_valid(&self) -> bool {
self.peak_count > 0 && self.density > 0.0 && self.max_magnitude > 0.0
}
#[must_use]
pub fn quality_score(&self) -> f64 {
if !self.is_valid() {
return 0.0;
}
let density_score = if self.density < 10.0 {
self.density / 10.0
} else if self.density > 50.0 {
50.0 / self.density
} else {
1.0
};
let dynamic_range = if self.min_magnitude > 0.0 {
(self.max_magnitude / self.min_magnitude).ln() / 10.0
} else {
0.5
};
(density_score * 0.7 + dynamic_range.min(1.0) * 0.3).min(1.0)
}
}