use rayon::prelude::*;
use std::f64::consts::PI;
pub fn population_vector_decode(
trains: &[&[i32]],
preferred_directions: &[f64],
window: usize,
) -> Vec<f64> {
if trains.is_empty() || window == 0 {
return vec![];
}
let min_len = trains.iter().map(|t| t.len()).min().unwrap_or(0);
let n_bins = min_len / window;
if n_bins == 0 {
return vec![];
}
let dirs_cos: Vec<f64> = preferred_directions.iter().map(|&d| d.cos()).collect();
let dirs_sin: Vec<f64> = preferred_directions.iter().map(|&d| d.sin()).collect();
let decoded: Vec<f64> = (0..n_bins)
.into_par_iter()
.map(|b| {
let mut sx = 0.0_f64;
let mut sy = 0.0_f64;
let start = b * window;
let end = (b + 1) * window;
for (i, t) in trains.iter().enumerate() {
let count: i64 = t[start..end].iter().map(|&v| v as i64).sum();
let c = dirs_cos.get(i).copied().unwrap_or(1.0);
let s = dirs_sin.get(i).copied().unwrap_or(0.0);
sx += count as f64 * c;
sy += count as f64 * s;
}
sy.atan2(sx)
})
.collect();
decoded
}
pub fn bayesian_decode(
spike_counts: &[f64],
tuning_rates: &[f64],
n_stimuli: usize,
n_neurons: usize,
prior: &[f64],
) -> usize {
if n_stimuli == 0 || n_neurons == 0 {
return 0;
}
let use_uniform = prior.is_empty();
let log_prior_uniform = -(n_stimuli as f64).ln();
let (best_s, _best_lp) = (0..n_stimuli)
.into_par_iter()
.map(|s| {
let mut lp = if use_uniform {
log_prior_uniform
} else {
(prior.get(s).copied().unwrap_or(1e-30) + 1e-30).ln()
};
let row_rates = &tuning_rates[s * n_neurons..(s + 1) * n_neurons];
let mut j = 0;
while j + 3 < n_neurons {
let lam0 = row_rates[j].max(1e-10);
let lam1 = row_rates[j + 1].max(1e-10);
let lam2 = row_rates[j + 2].max(1e-10);
let lam3 = row_rates[j + 3].max(1e-10);
lp += spike_counts[j] * lam0.ln() - lam0;
lp += spike_counts[j + 1] * lam1.ln() - lam1;
lp += spike_counts[j + 2] * lam2.ln() - lam2;
lp += spike_counts[j + 3] * lam3.ln() - lam3;
j += 4;
}
while j < n_neurons {
let lam = row_rates[j].max(1e-10);
lp += spike_counts[j] * lam.ln() - lam;
j += 1;
}
(s, lp)
})
.max_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal))
.unwrap_or((0, f64::NEG_INFINITY));
best_s
}
pub fn maximum_likelihood_decode(
spike_counts: &[f64],
tuning_rates: &[f64],
n_stimuli: usize,
n_neurons: usize,
) -> usize {
bayesian_decode(spike_counts, tuning_rates, n_stimuli, n_neurons, &[])
}
pub fn linear_discriminant_decode(
train_data: &[f64],
n_samples: usize,
n_features: usize,
labels: &[i64],
test_point: &[f64],
) -> i64 {
if n_samples == 0 || n_features == 0 {
return 0;
}
let mut classes: Vec<i64> = labels[..n_samples].to_vec();
classes.sort();
classes.dedup();
if classes.len() < 2 {
return classes.first().copied().unwrap_or(0);
}
let (class_means, class_indices): (Vec<Vec<f64>>, Vec<Vec<usize>>) = classes
.par_iter()
.map(|&c| {
let indices: Vec<usize> = (0..n_samples).filter(|&i| labels[i] == c).collect();
let mut mean = vec![0.0_f64; n_features];
for &idx in &indices {
let row = &train_data[idx * n_features..(idx + 1) * n_features];
for f in 0..n_features {
mean[f] += row[f];
}
}
let n_c = indices.len() as f64;
for v in &mut mean {
*v /= n_c;
}
(mean, indices)
})
.unzip();
let nf = n_features;
let mut s_w = vec![0.0_f64; nf * nf];
for (ci, indices) in class_indices.iter().enumerate() {
let mean = &class_means[ci];
for &idx in indices {
for i in 0..nf {
let di = train_data[idx * nf + i] - mean[i];
for j in 0..nf {
let dj = train_data[idx * nf + j] - mean[j];
s_w[i * nf + j] += di * dj;
}
}
}
}
for i in 0..nf {
s_w[i * nf + i] += 1e-8;
}
let mut overall_mean = vec![0.0_f64; nf];
for i in 0..n_samples {
for f in 0..nf {
overall_mean[f] += train_data[i * nf + f];
}
}
for v in &mut overall_mean {
*v /= n_samples as f64;
}
let mut best_class = classes[0];
let mut best_score = f64::NEG_INFINITY;
for (ci, &c) in classes.iter().enumerate() {
let diff: Vec<f64> = (0..nf)
.map(|f| class_means[ci][f] - overall_mean[f])
.collect();
let w = solve_linear(&s_w, &diff, nf);
let score: f64 = (0..nf).map(|f| w[f] * test_point[f]).sum();
if score > best_score {
best_score = score;
best_class = c;
}
}
best_class
}
pub fn naive_bayes_decode(
train_data: &[f64],
n_samples: usize,
n_features: usize,
labels: &[i64],
test_point: &[f64],
) -> i64 {
if n_samples == 0 || n_features == 0 {
return 0;
}
let mut classes: Vec<i64> = labels[..n_samples].to_vec();
classes.sort();
classes.dedup();
let mut best_class = classes.first().copied().unwrap_or(0);
let mut best_log_p = f64::NEG_INFINITY;
for &c in &classes {
let indices: Vec<usize> = (0..n_samples).filter(|&i| labels[i] == c).collect();
let n_c = indices.len() as f64;
let log_prior = (n_c / n_samples as f64).ln();
let mut log_likelihood = 0.0_f64;
for f in 0..n_features {
let vals: Vec<f64> = indices
.iter()
.map(|&i| train_data[i * n_features + f])
.collect();
let mu: f64 = vals.iter().sum::<f64>() / n_c;
let var: f64 = vals.iter().map(|&v| (v - mu).powi(2)).sum::<f64>() / n_c + 1e-10;
let x = test_point[f];
log_likelihood += -0.5 * ((2.0 * PI * var).ln() + (x - mu).powi(2) / var);
}
let log_p = log_prior + log_likelihood;
if log_p > best_log_p {
best_log_p = log_p;
best_class = c;
}
}
best_class
}
fn solve_linear(a: &[f64], b: &[f64], n: usize) -> Vec<f64> {
let mut aug = vec![0.0_f64; n * (n + 1)];
for i in 0..n {
for j in 0..n {
aug[i * (n + 1) + j] = a[i * n + j];
}
aug[i * (n + 1) + n] = b[i];
}
let stride = n + 1;
for col in 0..n {
let mut max_row = col;
let mut max_val = aug[col * stride + col].abs();
for row in (col + 1)..n {
let v = aug[row * stride + col].abs();
if v > max_val {
max_val = v;
max_row = row;
}
}
if max_row != col {
for j in 0..stride {
aug.swap(col * stride + j, max_row * stride + j);
}
}
let pivot = aug[col * stride + col];
if pivot.abs() < 1e-30 {
continue;
}
for row in (col + 1)..n {
let factor = aug[row * stride + col] / pivot;
for j in col..stride {
aug[row * stride + j] -= factor * aug[col * stride + j];
}
}
}
let mut x = vec![0.0_f64; n];
for i in (0..n).rev() {
let mut sum = aug[i * stride + n];
for j in (i + 1)..n {
sum -= aug[i * stride + j] * x[j];
}
let diag = aug[i * stride + i];
x[i] = if diag.abs() > 1e-30 { sum / diag } else { 0.0 };
}
x
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_pv_single_neuron_right() {
let train = vec![1i32; 100];
let trains: Vec<&[i32]> = vec![&train];
let dirs = vec![0.0_f64]; let decoded = population_vector_decode(&trains, &dirs, 50);
assert_eq!(decoded.len(), 2);
assert!((decoded[0] - 0.0).abs() < 1e-10, "should decode to 0 rad");
}
#[test]
fn test_pv_two_neurons_45deg() {
let train = vec![1i32; 100];
let trains: Vec<&[i32]> = vec![&train, &train];
let dirs = vec![0.0, PI / 2.0];
let decoded = population_vector_decode(&trains, &dirs, 100);
assert_eq!(decoded.len(), 1);
assert!(
(decoded[0] - PI / 4.0).abs() < 1e-10,
"equal firing at 0 and π/2 → π/4, got {}",
decoded[0]
);
}
#[test]
fn test_pv_empty() {
let decoded = population_vector_decode(&[], &[], 50);
assert!(decoded.is_empty());
}
#[test]
fn test_pv_no_bins() {
let train = vec![1i32; 10];
let trains: Vec<&[i32]> = vec![&train];
let decoded = population_vector_decode(&trains, &[0.0], 100);
assert!(decoded.is_empty(), "train shorter than window → empty");
}
#[test]
fn test_bayesian_obvious() {
let tuning = vec![10.0, 0.1, 0.1, 10.0]; let counts = vec![8.0, 0.0]; let s = bayesian_decode(&counts, &tuning, 2, 2, &[]);
assert_eq!(s, 0, "high neuron 0 firing → stimulus 0");
}
#[test]
fn test_bayesian_with_prior() {
let tuning = vec![5.0, 5.0, 5.0, 5.0]; let counts = vec![5.0, 5.0];
let prior = vec![0.1, 0.9]; let s = bayesian_decode(&counts, &tuning, 2, 2, &prior);
assert_eq!(s, 1, "equal evidence + strong prior → stimulus 1");
}
#[test]
fn test_bayesian_empty() {
assert_eq!(bayesian_decode(&[], &[], 0, 0, &[]), 0);
}
#[test]
fn test_ml_matches_bayesian_uniform() {
let tuning = vec![10.0, 0.1, 0.1, 10.0];
let counts = vec![0.0, 8.0]; let s_ml = maximum_likelihood_decode(&counts, &tuning, 2, 2);
let s_bay = bayesian_decode(&counts, &tuning, 2, 2, &[]);
assert_eq!(s_ml, s_bay);
assert_eq!(s_ml, 1);
}
#[test]
fn test_lda_separable() {
#[rustfmt::skip]
let data = vec![
0.0, 0.0,
0.1, 0.1,
-0.1, 0.1,
10.0, 10.0,
10.1, 9.9,
9.9, 10.1,
];
let labels = vec![0_i64, 0, 0, 1, 1, 1];
let test_1 = vec![10.0, 10.0];
assert_eq!(linear_discriminant_decode(&data, 6, 2, &labels, &test_1), 1);
let r0 = linear_discriminant_decode(&data, 6, 2, &labels, &[-5.0, -5.0]);
let r1 = linear_discriminant_decode(&data, 6, 2, &labels, &[15.0, 15.0]);
assert_ne!(r0, r1, "distant points should decode to different classes");
}
#[test]
fn test_lda_single_class() {
let data = vec![1.0, 2.0, 3.0, 4.0];
let labels = vec![5_i64, 5];
let test = vec![2.0, 3.0];
assert_eq!(linear_discriminant_decode(&data, 2, 2, &labels, &test), 5);
}
#[test]
fn test_lda_empty() {
assert_eq!(linear_discriminant_decode(&[], 0, 0, &[], &[]), 0);
}
#[test]
fn test_nb_separable() {
#[rustfmt::skip]
let data = vec![
0.0, 0.0,
0.1, 0.1,
-0.1, -0.1,
10.0, 10.0,
10.1, 10.1,
9.9, 9.9,
];
let labels = vec![0_i64, 0, 0, 1, 1, 1];
let test_0 = vec![0.2, 0.2];
let test_1 = vec![9.8, 9.8];
assert_eq!(naive_bayes_decode(&data, 6, 2, &labels, &test_0), 0);
assert_eq!(naive_bayes_decode(&data, 6, 2, &labels, &test_1), 1);
}
#[test]
fn test_nb_single_class() {
let data = vec![1.0, 2.0];
let labels = vec![7_i64];
assert_eq!(naive_bayes_decode(&data, 1, 2, &labels, &[1.0, 2.0]), 7);
}
#[test]
fn test_nb_agrees_with_lda_simple() {
#[rustfmt::skip]
let data = vec![
-5.0, -5.0,
-4.9, -5.1,
5.0, 5.0,
5.1, 4.9,
];
let labels = vec![0_i64, 0, 1, 1];
let test = vec![4.0, 4.0];
let lda = linear_discriminant_decode(&data, 4, 2, &labels, &test);
let nb = naive_bayes_decode(&data, 4, 2, &labels, &test);
assert_eq!(lda, nb, "well-separated → both predict same class");
}
#[test]
fn test_solve_2x2() {
let a = vec![2.0, 1.0, 1.0, 3.0];
let b = vec![5.0, 10.0];
let x = solve_linear(&a, &b, 2);
assert!((x[0] - 1.0).abs() < 1e-10);
assert!((x[1] - 3.0).abs() < 1e-10);
}
}