use super::correlogram::{compute_autocorrelogram, compute_crosscorrelogram, Correlogram};
const POISSON_EPSILON: f64 = 1e-10;
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct RefractoryOptions {
pub bin_ms: f32,
pub window_ms: f32,
pub max_central_bins: usize,
pub ignored_central_bins: usize,
pub shoulder_start_ms: f32,
pub ccg_ratio: f64,
pub ccg_probability: f64,
pub acg_ratio: f64,
pub acg_probability: f64,
}
impl Default for RefractoryOptions {
fn default() -> Self {
Self {
bin_ms: 1.0,
window_ms: 500.0,
max_central_bins: 10,
ignored_central_bins: 1,
shoulder_start_ms: 250.0,
ccg_ratio: 0.25,
ccg_probability: 0.05,
acg_ratio: 0.2,
acg_probability: 0.2,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct RefractoryStats {
pub ratio: f64,
pub probability: f64,
pub baseline: f64,
}
impl RefractoryStats {
pub fn is_refractory(&self, ratio: f64, probability: f64) -> bool {
self.ratio < ratio && self.probability < probability
}
}
pub fn compute_refractory_stats(correlogram: &Correlogram, options: &RefractoryOptions) -> RefractoryStats {
let counts = &correlogram.counts;
let half = counts.len() / 2;
let shoulder = ((options.shoulder_start_ms / correlogram.bin_size_ms).round() as usize).clamp(1, half);
let mean = |bins: &[u64]| if bins.is_empty() { 0.0 } else { bins.iter().sum::<u64>() as f64 / bins.len() as f64 };
let left = mean(&counts[..=half - shoulder]);
let right = mean(&counts[half + shoulder..]);
let baseline = left.max(right);
if baseline <= 0.0 {
return RefractoryStats { ratio: f64::INFINITY, probability: 1.0, baseline };
}
let mut ratio = f64::INFINITY;
let mut probability = 1.0f64;
let skip = options.ignored_central_bins;
let mut n_k = 0.0f64;
let mut bins = 0usize;
for k in skip..=options.max_central_bins.min(half.saturating_sub(1)) {
if k == 0 {
n_k += counts[half] as f64;
bins += 1;
} else {
n_k += (counts[half - k] + counts[half + k]) as f64;
bins += 2;
}
let lambda = bins as f64 * baseline;
ratio = ratio.min(n_k / lambda);
let p = 0.5 * (1.0 + erf((n_k - lambda) / (POISSON_EPSILON + 2.0 * lambda).sqrt()));
probability = probability.min(p);
}
RefractoryStats { ratio, probability, baseline }
}
pub fn compute_ccg_refractory(sorted_a: &[u64], sorted_b: &[u64], sample_rate_hz: f64, options: &RefractoryOptions) -> (RefractoryStats, bool) {
let ccg = compute_crosscorrelogram(sorted_a, sorted_b, sample_rate_hz, options.bin_ms, options.window_ms);
let stats = compute_refractory_stats(&ccg, options);
(stats, stats.is_refractory(options.ccg_ratio, options.ccg_probability))
}
pub fn compute_acg_refractory(sorted: &[u64], sample_rate_hz: f64, options: &RefractoryOptions) -> (RefractoryStats, bool) {
let acg = compute_autocorrelogram(sorted, sample_rate_hz, options.bin_ms, options.window_ms);
let stats = compute_refractory_stats(&acg, options);
(stats, stats.is_refractory(options.acg_ratio, options.acg_probability))
}
fn erf(x: f64) -> f64 {
const P: f64 = 0.327_591_1;
const A: [f64; 5] = [0.254_829_592, -0.284_496_736, 1.421_413_741, -1.453_152_027, 1.061_405_429];
let sign = x.signum();
let x = x.abs();
let t = 1.0 / (1.0 + P * x);
let poly = A.iter().rev().fold(0.0, |acc, &a| acc * t + a) * t;
sign * (1.0 - poly * (-x * x).exp())
}
#[cfg(test)]
mod tests {
use super::*;
const FS: f64 = 30_000.0;
const DURATION: u64 = 600 * 30_000;
struct Rng(u64);
impl Rng {
fn uniform(&mut self) -> f64 {
self.0 = self.0.wrapping_add(0x9e37_79b9_7f4a_7c15);
let mut z = self.0;
z = (z ^ (z >> 30)).wrapping_mul(0xbf58_476d_1ce4_e5b9);
z = (z ^ (z >> 27)).wrapping_mul(0x94d0_49bb_1331_11eb);
((z ^ (z >> 31)) >> 11) as f64 / (1u64 << 53) as f64
}
}
fn train(seed: u64, rate_hz: f64, dead_ms: f64) -> Vec<u64> {
let mut rng = Rng(seed);
let dead = dead_ms * FS / 1000.0;
let mut t = 0.0f64;
let mut out = Vec::new();
loop {
t += dead - (1.0 - rng.uniform()).ln() * FS / rate_hz;
if t >= DURATION as f64 {
return out;
}
out.push(t as u64);
}
}
fn merge(a: &[u64], b: &[u64]) -> Vec<u64> {
let mut m: Vec<u64> = a.iter().chain(b).copied().collect();
m.sort_unstable();
m
}
#[test]
fn erf_matches_known_values() {
for (x, e) in [(0.0, 0.0), (0.5, 0.520_499_877_8), (1.0, 0.842_700_792_9), (-2.0, -0.995_322_265)] {
assert!((erf(x) - e).abs() < 2e-7, "erf({x})");
}
}
#[test]
fn poisson_train_is_not_refractory() {
let t = train(1, 10.0, 0.0);
let (stats, refractory) = compute_acg_refractory(&t, FS, &RefractoryOptions::default());
assert!(!refractory, "{stats:?}");
assert!(stats.ratio > 0.5, "{stats:?}");
}
#[test]
fn an_empty_zero_lag_bin_alone_is_not_refractory() {
let t = train(6, 30.0, 0.5);
let (stats, refractory) = compute_acg_refractory(&t, FS, &RefractoryOptions::default());
assert!(!refractory, "{stats:?}");
let counting_centre = RefractoryOptions { ignored_central_bins: 0, ..Default::default() };
assert!(compute_acg_refractory(&t, FS, &counting_centre).1, "with the centre counted it looks refractory");
}
#[test]
fn train_with_dead_time_is_refractory() {
let t = train(2, 10.0, 3.0);
let (stats, refractory) = compute_acg_refractory(&t, FS, &RefractoryOptions::default());
assert!(refractory, "{stats:?}");
}
#[test]
fn halves_of_one_neuron_have_a_refractory_ccg() {
let t = train(3, 20.0, 3.0);
let (a, b): (Vec<u64>, Vec<u64>) = t.iter().enumerate().fold((Vec::new(), Vec::new()), |(mut a, mut b), (i, &s)| {
if i % 2 == 0 { a.push(s) } else { b.push(s) }
(a, b)
});
let (stats, refractory) = compute_ccg_refractory(&a, &b, FS, &RefractoryOptions::default());
assert!(refractory, "{stats:?}");
}
#[test]
fn independent_neurons_have_a_flat_ccg() {
let (a, b) = (train(4, 10.0, 3.0), train(5, 10.0, 3.0));
let (stats, refractory) = compute_ccg_refractory(&a, &b, FS, &RefractoryOptions::default());
assert!(!refractory, "{stats:?}");
let (stats, refractory) = compute_acg_refractory(&merge(&a, &b), FS, &RefractoryOptions::default());
assert!(!refractory, "{stats:?}");
}
#[test]
fn empty_or_sparse_trains_are_not_refractory() {
let options = RefractoryOptions::default();
assert!(!compute_acg_refractory(&[], FS, &options).1);
assert!(!compute_ccg_refractory(&[100], &[], FS, &options).1);
}
}