use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Correlogram {
pub bin_centers_ms: Vec<f32>,
pub counts: Vec<u64>,
pub bin_size_ms: f32,
pub window_ms: f32,
}
const MIN_BIN_MS: f32 = 0.05;
pub fn compute_autocorrelogram(
sorted_samples: &[u64],
sample_rate_hz: f64,
bin_size_ms: f32,
window_ms: f32,
) -> Correlogram {
let bin_ms = bin_size_ms.max(MIN_BIN_MS);
let half_bins = ((window_ms.max(bin_ms) / bin_ms).round() as usize).max(1);
let num_bins = 2 * half_bins + 1;
let max_lag_ms = (half_bins as f32 + 0.5) * bin_ms;
let mut bin_centers_ms = Vec::with_capacity(num_bins);
for b in 0..num_bins {
bin_centers_ms.push(((b as isize - half_bins as isize) as f32) * bin_ms);
}
let mut counts = vec![0u64; num_bins];
let n = sorted_samples.len();
if n < 2 || sample_rate_hz <= 0.0 {
return Correlogram {
bin_centers_ms,
counts,
bin_size_ms: bin_ms,
window_ms: (half_bins as f32) * bin_ms,
};
}
let ms_per_sample = 1000.0 / sample_rate_hz;
for i in 0..n {
let t_i = sorted_samples[i];
for j in (i + 1)..n {
let dt_ms = ((sorted_samples[j].saturating_sub(t_i)) as f64 * ms_per_sample) as f32;
if dt_ms >= max_lag_ms {
break;
}
let offset_bins = ((dt_ms / bin_ms).round() as usize).min(half_bins);
counts[half_bins + offset_bins] += 1;
counts[half_bins - offset_bins] += 1;
}
}
Correlogram {
bin_centers_ms,
counts,
bin_size_ms: bin_ms,
window_ms: (half_bins as f32) * bin_ms,
}
}
pub fn compute_crosscorrelogram(
sorted_samples_a: &[u64],
sorted_samples_b: &[u64],
sample_rate_hz: f64,
bin_size_ms: f32,
window_ms: f32,
) -> Correlogram {
let bin_ms = bin_size_ms.max(MIN_BIN_MS);
let half_bins = ((window_ms.max(bin_ms) / bin_ms).round() as usize).max(1);
let num_bins = 2 * half_bins + 1;
let max_lag_ms = (half_bins as f32 + 0.5) * bin_ms;
let mut bin_centers_ms = Vec::with_capacity(num_bins);
for b in 0..num_bins {
bin_centers_ms.push(((b as isize - half_bins as isize) as f32) * bin_ms);
}
let mut counts = vec![0u64; num_bins];
if sorted_samples_a.is_empty() || sorted_samples_b.is_empty() || sample_rate_hz <= 0.0 {
return Correlogram {
bin_centers_ms,
counts,
bin_size_ms: bin_ms,
window_ms: (half_bins as f32) * bin_ms,
};
}
let ms_per_sample = 1000.0 / sample_rate_hz;
let mut left_b = 0usize;
let n_b = sorted_samples_b.len();
for &t_a in sorted_samples_a {
while left_b < n_b {
let dt_ms = ((sorted_samples_b[left_b] as f64 - t_a as f64) * ms_per_sample) as f32;
if dt_ms < -max_lag_ms {
left_b += 1;
} else {
break;
}
}
for j in left_b..n_b {
let dt_ms = ((sorted_samples_b[j] as f64 - t_a as f64) * ms_per_sample) as f32;
if dt_ms >= max_lag_ms {
break;
}
let signed_bin = (dt_ms / bin_ms).round() as isize;
let bin_idx = (half_bins as isize + signed_bin).clamp(0, (num_bins - 1) as isize) as usize;
counts[bin_idx] += 1;
}
}
Correlogram {
bin_centers_ms,
counts,
bin_size_ms: bin_ms,
window_ms: (half_bins as f32) * bin_ms,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_acg_and_ccg_refractory_dip_and_lag_peak() {
let train_a: Vec<u64> = (0..20).map(|i| i * 10).collect();
let acg = compute_autocorrelogram(&train_a, 1000.0, 1.0, 25.0);
let center = acg.counts.len() / 2;
for lag in 0..9 {
assert_eq!(acg.counts[center + lag], 0);
assert_eq!(acg.counts[center - lag], 0);
}
assert!(acg.counts[center + 10] > 0);
let train_b: Vec<u64> = train_a.iter().map(|&t| t + 3).collect();
let ccg = compute_crosscorrelogram(&train_a, &train_b, 1000.0, 1.0, 25.0);
assert_eq!(ccg.counts[center + 3], 20);
}
#[test]
fn acg_counts_ordered_pairs_and_matches_self_ccg() {
let train = [100u64, 100, 130, 400, 415, 1_000];
let acg = compute_autocorrelogram(&train, 30_000.0, 0.5, 5.0);
let ccg = compute_crosscorrelogram(&train, &train, 30_000.0, 0.5, 5.0);
let centre = acg.counts.len() / 2;
assert_eq!(ccg.counts[centre] - acg.counts[centre], 6);
assert_eq!(acg.counts[centre], 2);
let mut rest = ccg.counts.clone();
rest[centre] -= 6;
assert_eq!(rest, acg.counts);
assert!(acg.counts.iter().eq(acg.counts.iter().rev()));
}
}