use crate::algorithms::decomposition::{count_to_f64, mean_center, pca};
use crate::rng::SplitMix64;
const ITERATIONS: usize = 1000;
const EXAGGERATION_ITERS: usize = 100;
const EXAGGERATION: f64 = 4.0;
const LEARNING_RATE: f64 = 8.0;
const EPS: f64 = 1e-12;
#[must_use]
pub fn tsne(data: &[Vec<f64>], n_components: usize, perplexity: f64, seed: u64) -> Vec<Vec<f64>> {
let n = data.len();
if n == 0 || n_components == 0 {
return Vec::new();
}
let affinities = joint_affinities(data, perplexity);
let mut embedding = pca_init(data, n_components, seed);
optimize(&mut embedding, &affinities, n, n_components);
embedding
}
fn joint_affinities(data: &[Vec<f64>], perplexity: f64) -> Vec<f64> {
let n = data.len();
let dist = squared_distances(data, n);
let mut p = vec![0.0_f64; n * n];
for i in 0..n {
let beta = tune_beta(&dist, n, i, perplexity);
let mut sum = 0.0_f64;
for j in 0..n {
if i != j {
let value = (-beta * at(&dist, n, i, j)).exp();
put(&mut p, n, i, j, value);
sum += value;
}
}
if sum > 0.0 {
for j in 0..n {
let v = at(&p, n, i, j);
put(&mut p, n, i, j, v / sum);
}
}
}
let norm = 2.0 * count_to_f64(n);
let mut joint = vec![0.0_f64; n * n];
for i in 0..n {
for j in 0..n {
let value = (at(&p, n, i, j) + at(&p, n, j, i)) / norm;
put(&mut joint, n, i, j, value.max(EPS));
}
}
joint
}
fn tune_beta(dist: &[f64], n: usize, i: usize, perplexity: f64) -> f64 {
let target = perplexity.ln();
let mut beta = 1.0_f64;
let mut lo = f64::MIN_POSITIVE;
let mut hi = f64::INFINITY;
for _ in 0..50 {
let (entropy, _) = entropy_and_sum(dist, n, i, beta);
let diff = entropy - target;
if diff.abs() < 1e-5 {
break;
}
if diff > 0.0 {
lo = beta;
beta = if hi.is_infinite() {
beta * 2.0
} else {
f64::midpoint(beta, hi)
};
} else {
hi = beta;
beta = f64::midpoint(beta, lo);
}
}
beta
}
fn entropy_and_sum(dist: &[f64], n: usize, i: usize, beta: f64) -> (f64, f64) {
let mut sum = 0.0_f64;
let mut dsum = 0.0_f64;
for j in 0..n {
if i != j {
let d = at(dist, n, i, j);
let w = (-beta * d).exp();
sum += w;
dsum = d.mul_add(w, dsum);
}
}
if sum <= 0.0 {
return (0.0, 0.0);
}
let entropy = beta * dsum / sum + sum.ln();
(entropy, sum)
}
fn squared_distances(data: &[Vec<f64>], n: usize) -> Vec<f64> {
let mut dist = vec![0.0_f64; n * n];
for i in 0..n {
for j in (i + 1)..n {
let d = match (data.get(i), data.get(j)) {
(Some(a), Some(b)) => a
.iter()
.zip(b)
.map(|(&x, &y)| {
let delta = x - y;
delta * delta
})
.sum(),
_ => 0.0,
};
put(&mut dist, n, i, j, d);
put(&mut dist, n, j, i, d);
}
}
dist
}
fn pca_init(data: &[Vec<f64>], n_components: usize, seed: u64) -> Vec<Vec<f64>> {
let dim = data.first().map_or(0, Vec::len);
let (centered, _means) = mean_center(data, dim);
let result = pca(data, n_components);
let mut rng = SplitMix64::new(seed);
let mut embedding: Vec<Vec<f64>> = centered
.iter()
.map(|row| {
result
.components
.iter()
.map(|component| {
let score: f64 = row.iter().zip(component).map(|(&x, &c)| x * c).sum();
1e-4_f64.mul_add(rng.standard_normal(), score)
})
.collect()
})
.collect();
rescale_unit_std(&mut embedding, n_components);
embedding
}
fn rescale_unit_std(embedding: &mut [Vec<f64>], dim: usize) {
let n = count_to_f64(embedding.len()).max(1.0);
for d in 0..dim {
let mean: f64 = embedding
.iter()
.map(|r| r.get(d).copied().unwrap_or(0.0))
.sum::<f64>()
/ n;
let var: f64 = embedding
.iter()
.map(|r| {
let v = r.get(d).copied().unwrap_or(0.0) - mean;
v * v
})
.sum::<f64>()
/ n;
let std = var.sqrt().max(1e-12);
for row in embedding.iter_mut() {
if let Some(slot) = row.get_mut(d) {
*slot = (*slot - mean) / std;
}
}
}
}
fn optimize(embedding: &mut [Vec<f64>], affinities: &[f64], n: usize, dim: usize) {
let mut velocity = vec![vec![0.0_f64; dim]; n];
for iter in 0..ITERATIONS {
let exaggeration = if iter < EXAGGERATION_ITERS {
EXAGGERATION
} else {
1.0
};
let momentum = if iter < EXAGGERATION_ITERS { 0.5 } else { 0.8 };
let (q_unnorm, q_sum) = student_t_affinities(embedding, n, dim);
for i in 0..n {
let mut grad = vec![0.0_f64; dim];
for j in 0..n {
if i == j {
continue;
}
let p = exaggeration * at(affinities, n, i, j);
let q = (at(&q_unnorm, n, i, j) / q_sum).max(EPS);
let mult = 4.0 * (p - q) * at(&q_unnorm, n, i, j);
for d in 0..dim {
let yi = coord(embedding, i, d);
let yj = coord(embedding, j, d);
if let Some(g) = grad.get_mut(d) {
*g += mult * (yi - yj);
}
}
}
for d in 0..dim {
let v = LEARNING_RATE.mul_add(
-grad.get(d).copied().unwrap_or(0.0),
momentum * coord_vel(&velocity, i, d),
);
set_vel(&mut velocity, i, d, v);
let cur = coord(embedding, i, d);
set_coord(embedding, i, d, cur + v);
}
}
}
}
fn student_t_affinities(embedding: &[Vec<f64>], n: usize, dim: usize) -> (Vec<f64>, f64) {
let mut q = vec![0.0_f64; n * n];
let mut sum = 0.0_f64;
for i in 0..n {
for j in (i + 1)..n {
let mut d = 0.0_f64;
for k in 0..dim {
let delta = coord(embedding, i, k) - coord(embedding, j, k);
d = delta.mul_add(delta, d);
}
let value = 1.0 / (1.0 + d);
put(&mut q, n, i, j, value);
put(&mut q, n, j, i, value);
sum = 2.0f64.mul_add(value, sum);
}
}
(q, sum.max(EPS))
}
fn coord(embedding: &[Vec<f64>], point: usize, dim: usize) -> f64 {
embedding
.get(point)
.and_then(|r| r.get(dim))
.copied()
.unwrap_or(0.0)
}
fn set_coord(embedding: &mut [Vec<f64>], point: usize, dim: usize, value: f64) {
if let Some(slot) = embedding.get_mut(point).and_then(|r| r.get_mut(dim)) {
*slot = value;
}
}
fn coord_vel(velocity: &[Vec<f64>], point: usize, dim: usize) -> f64 {
velocity
.get(point)
.and_then(|r| r.get(dim))
.copied()
.unwrap_or(0.0)
}
fn set_vel(velocity: &mut [Vec<f64>], point: usize, dim: usize, value: f64) {
if let Some(slot) = velocity.get_mut(point).and_then(|r| r.get_mut(dim)) {
*slot = value;
}
}
fn at(matrix: &[f64], n: usize, i: usize, j: usize) -> f64 {
matrix.get(i * n + j).copied().unwrap_or(0.0)
}
fn put(matrix: &mut [f64], n: usize, i: usize, j: usize, value: f64) {
if let Some(slot) = matrix.get_mut(i * n + j) {
*slot = value;
}
}
#[cfg(test)]
mod tests {
use super::*;
fn two_clusters() -> Vec<Vec<f64>> {
(0_usize..20)
.map(|i| {
let base = if i < 10 { 0.0 } else { 20.0 };
let jitter = count_to_f64(i % 10) * 0.05;
vec![base + jitter, jitter, 0.0]
})
.collect()
}
#[test]
fn keeps_clusters_separated() {
let data = two_clusters();
let embedding = tsne(&data, 2, 5.0, 7);
assert_eq!(embedding.len(), 20, "embedding row count");
let within = pair_distance(&embedding, 0, 1);
let across = pair_distance(&embedding, 0, 15);
assert!(within < across, "within {within} not < across {across}");
}
#[test]
fn deterministic_for_fixed_seed() {
let data = two_clusters();
let a = tsne(&data, 2, 5.0, 3);
let b = tsne(&data, 2, 5.0, 3);
let fa = a.first().and_then(|r| r.first()).copied().unwrap_or(0.0);
let fb = b.first().and_then(|r| r.first()).copied().unwrap_or(0.0);
assert_eq!(fa.to_bits(), fb.to_bits(), "non-deterministic embedding");
}
#[test]
fn empty_input_is_empty() {
assert!(tsne(&[], 2, 5.0, 0).is_empty());
}
fn pair_distance(embedding: &[Vec<f64>], left: usize, right: usize) -> f64 {
match (embedding.get(left), embedding.get(right)) {
(Some(pa), Some(pb)) => pa
.iter()
.zip(pb)
.map(|(&x, &y)| {
let delta = x - y;
delta * delta
})
.sum::<f64>()
.sqrt(),
_ => 0.0,
}
}
}