pub(super) mod arena;
pub(super) mod fft;
pub(super) mod interpolation;
pub(super) mod morton;
pub(super) mod vptree;
use std::{
iter::Sum,
ops::{Add, AddAssign, DivAssign, MulAssign, SubAssign},
};
use rayon::{
iter::{
IndexedParallelIterator, IntoParallelRefIterator, IntoParallelRefMutIterator,
ParallelIterator,
},
slice::{ParallelSlice, ParallelSliceMut},
};
use rand_distr::{Distribution, Normal};
use num_traits::{AsPrimitive, Float};
use rustfft::FftNum;
const GAIN_INCREMENT: f64 = 0.2;
const GAIN_DECAY: f64 = 0.8;
const MIN_GAIN: f64 = 0.01;
pub(super) struct GradientStep<T> {
pub learning_rate: T,
pub momentum: T,
pub inverse_norm: T,
}
#[inline]
pub(super) fn check_perplexity<T: Float + AsPrimitive<usize>>(perplexity: &T, n_samples: &usize) {
if n_samples - 1 < 3 * perplexity.as_() {
panic!("error: the provided perplexity is too large for the number of data points.\n");
}
}
pub(super) fn prepare_buffers<T: Float + Send + Sync>(
y: &mut Vec<T>,
dy: &mut Vec<T>,
uy: &mut Vec<T>,
gains: &mut Vec<T>,
grad_entries: usize,
) {
y.resize(grad_entries, T::zero()); dy.resize(grad_entries, T::zero()); uy.resize(grad_entries, T::zero()); gains.resize(grad_entries, T::one()); }
pub(super) fn clear_buffers<T: Float + Send + Sync>(
dy: &mut Vec<T>,
uy: &mut Vec<T>,
gains: &mut Vec<T>,
) {
*dy = Vec::new(); *uy = Vec::new(); *gains = Vec::new(); }
#[cfg(any(
not(all(target_arch = "wasm32", target_os = "unknown")),
feature = "wasm_js"
))]
pub(super) fn make_rng() -> impl rand::Rng {
rand::rng()
}
#[cfg(all(
target_arch = "wasm32",
target_os = "unknown",
not(feature = "wasm_js")
))]
pub(super) fn make_rng() -> impl rand::Rng {
use rand::SeedableRng;
rand::rngs::SmallRng::seed_from_u64(0x6268_7473_6e65)
}
pub(super) fn random_init<T: Float + Send + Sync + Copy>(y: &mut [T]) {
let distr = Normal::new(0.0, 1e-4).unwrap();
let mut rng = make_rng();
y.iter_mut()
.for_each(|el| *el = T::from(distr.sample(&mut rng)).unwrap());
}
pub(super) fn compute_pairwise_distance_matrix<'a, T, U, F, G>(
distances: &mut [T],
f: F,
g: G,
n_samples: usize,
) where
T: Float + Send + Sync,
U: 'a + Send + Sync + ?Sized,
F: Fn(&U, &U) -> T + Sync + Send,
G: Fn(&usize) -> &'a U + Sync + Send,
{
distances
.par_chunks_mut(n_samples)
.enumerate()
.for_each(|(i, distances_row)| {
let ith = g(&i);
for (j, d) in distances_row.iter_mut().enumerate().skip(i + 1) {
*d = f(ith, g(&j));
}
});
for i in 0..n_samples {
for j in (i + 1)..n_samples {
distances[j * n_samples + i] = distances[i * n_samples + j];
}
}
}
pub(super) fn search_beta<T>(p_values_row: &mut [T], distances_row: &[T], perplexity: &T)
where
T: Send + Sync + Copy + Float + Sum + MulAssign + DivAssign,
{
let mut found = false;
let mut beta: T = T::one();
let mut min_beta: T = -T::max_value();
let mut max_beta: T = T::max_value();
let tolerance: T = T::from(1e-5).unwrap();
let mut iteration = 0;
let mut p_values_row_sum: T = T::zero();
let two = T::from(2.0).unwrap();
debug_assert_eq!(p_values_row.len(), distances_row.len());
while !found && iteration < 200 {
p_values_row
.iter_mut()
.zip(distances_row.iter())
.for_each(|(p, d)| {
*p = (-beta * (*d * *d)).exp();
});
p_values_row_sum = p_values_row.iter().copied().sum::<T>() + T::min_positive_value();
let mut entropy = p_values_row
.iter()
.zip(distances_row.iter())
.fold(T::zero(), |acc, (p, d)| acc + beta * *p * (*d * *d));
entropy = entropy / p_values_row_sum + p_values_row_sum.ln();
let entropy_difference = entropy - perplexity.ln();
if entropy_difference < tolerance && -entropy_difference < tolerance {
found = true;
} else {
if entropy_difference > T::zero() {
min_beta = beta;
if max_beta == T::max_value() || max_beta == -T::max_value() {
beta *= two;
} else {
beta = (beta + max_beta) / two;
}
} else {
max_beta = beta;
if min_beta == -T::max_value() || min_beta == T::max_value() {
beta /= two;
} else {
beta = (beta + min_beta) / two;
}
}
if beta.is_infinite() && beta.is_sign_positive() {
beta = T::max_value()
}
if beta.is_infinite() && beta.is_sign_negative() {
beta = -T::max_value()
}
}
iteration += 1;
}
p_values_row
.iter_mut()
.for_each(|p| *p /= p_values_row_sum + T::epsilon());
}
pub(super) fn normalize_p_values<T: Float + Send + Sync + MulAssign + Sum>(
p_values: &mut [T],
early_exaggeration: T,
) {
let p_values_sum: T = p_values.par_iter().copied().sum::<T>();
let scale = early_exaggeration / (p_values_sum + T::epsilon());
p_values.par_iter_mut().for_each(|p| *p *= scale);
}
pub(super) fn symmetrize_sparse_matrix<T>(
sym_p_rows: &mut Vec<usize>,
sym_p_columns: &mut Vec<u32>,
mut p_columns: Vec<u32>,
p_values: &mut Vec<T>,
n_samples: usize,
n_neighbors: &usize,
) where
T: Float + Add + DivAssign + Send + Sync + MulAssign,
{
p_columns
.par_chunks_mut(*n_neighbors)
.zip(p_values.par_chunks_mut(*n_neighbors))
.for_each_init(Vec::<(u32, T)>::new, |row, (cols, vals)| {
row.clear();
row.extend(cols.iter().copied().zip(vals.iter().copied()));
row.sort_unstable_by_key(|(c, _)| *c);
for (j, &(col, val)) in row.iter().enumerate() {
cols[j] = col;
vals[j] = val;
}
});
let col = |i: usize| p_columns[i] as usize;
let mut row_counts: Vec<usize> = vec![0; n_samples];
let p_rows = |i| i * n_neighbors;
for n in 0..n_samples {
for i in p_rows(n)..p_rows(n + 1) {
row_counts[n] += 1;
let neighbor_row = &p_columns[p_rows(col(i))..p_rows(col(i) + 1)];
if neighbor_row.binary_search(&(n as u32)).is_err() {
row_counts[col(i)] += 1;
}
}
}
let total: usize = row_counts.iter().sum();
let mut sym_row_p: Vec<usize> = vec![0; n_samples + 1];
let mut sym_col_p: Vec<u32> = vec![0; total];
let mut sym_val_p: Vec<T> = vec![T::zero(); total];
sym_row_p[0] = 0;
for _n in 0..n_samples {
sym_row_p[_n + 1] = sym_row_p[_n] + row_counts[_n];
}
let mut offset: Vec<usize> = vec![0; n_samples];
for _n in 0..n_samples {
for i in p_rows(_n)..p_rows(_n + 1) {
let neighbor_row = &p_columns[p_rows(col(i))..p_rows(col(i) + 1)];
let present = neighbor_row.binary_search(&(_n as u32));
if let Ok(m_offset) = present {
if _n <= col(i) {
sym_col_p[sym_row_p[_n] + offset[_n]] = p_columns[i];
sym_col_p[sym_row_p[col(i)] + offset[col(i)]] = _n as u32;
let m = p_rows(col(i)) + m_offset;
sym_val_p[sym_row_p[_n] + offset[_n]] = p_values[i] + p_values[m];
sym_val_p[sym_row_p[col(i)] + offset[col(i)]] = p_values[i] + p_values[m];
}
} else {
sym_col_p[sym_row_p[_n] + offset[_n]] = p_columns[i];
sym_col_p[sym_row_p[col(i)] + offset[col(i)]] = _n as u32;
sym_val_p[sym_row_p[_n] + offset[_n]] = p_values[i];
sym_val_p[sym_row_p[col(i)] + offset[col(i)]] = p_values[i];
}
if present.is_err() || _n <= col(i) {
offset[_n] += 1;
if col(i) != _n {
offset[col(i)] += 1;
}
}
}
}
let zero_point_five = T::from(0.5).unwrap();
sym_val_p.iter_mut().for_each(|p| *p *= zero_point_five);
*p_values = sym_val_p;
*sym_p_rows = sym_row_p;
*sym_p_columns = sym_col_p;
}
pub(super) fn update_solution<T>(
y: &mut [T],
dy: &[T],
uy: &mut [T],
gains: &mut [T],
learning_rate: &T,
momentum: &T,
) where
T: Float + Send + Sync + AddAssign,
{
let (gain_increment, gain_decay, min_gain) = gain_constants::<T>();
y.par_iter_mut()
.zip(dy.par_iter())
.zip(uy.par_iter_mut())
.zip(gains.par_iter_mut())
.for_each(|(((y_el, dy_el), uy_el), gains_el)| {
*gains_el = if dy_el.signum() != uy_el.signum() {
*gains_el + gain_increment
} else {
*gains_el * gain_decay
};
if *gains_el < min_gain {
*gains_el = min_gain;
}
*uy_el = *momentum * *uy_el - *learning_rate * *gains_el * *dy_el;
*y_el += *uy_el
});
}
pub(super) fn gradient_descent_step<T, const D: usize>(
y: &mut [T],
positive_forces: &[T],
negative_forces: &[T],
uy: &mut [T],
gains: &mut [T],
step: GradientStep<T>,
) where
T: Float + Send + Sync + AddAssign,
{
let GradientStep {
learning_rate,
momentum,
inverse_norm,
} = step;
let (gain_increment, gain_decay, min_gain) = gain_constants::<T>();
y.par_chunks_mut(D)
.with_min_len(crate::PARALLEL_CODE_THRESHOLD)
.zip(positive_forces.par_chunks(D))
.zip(negative_forces.par_chunks(D))
.zip(uy.par_chunks_mut(D))
.zip(gains.par_chunks_mut(D))
.for_each(
|((((y_sample, positive), negative), uy_sample), gains_sample)| {
y_sample
.iter_mut()
.zip(positive.iter())
.zip(negative.iter())
.zip(uy_sample.iter_mut())
.zip(gains_sample.iter_mut())
.for_each(|((((y_el, pf), nf), uy_el), gain)| {
let gradient = *pf - *nf * inverse_norm;
*gain = if gradient.signum() != uy_el.signum() {
*gain + gain_increment
} else {
*gain * gain_decay
};
if *gain < min_gain {
*gain = min_gain;
}
*uy_el = momentum * *uy_el - learning_rate * *gain * gradient;
*y_el += *uy_el;
});
},
);
}
pub(super) fn stop_lying<T: Float + Send + Sync + MulAssign>(
p_values: &mut [T],
early_exaggeration: T,
) {
let scale = early_exaggeration.recip();
p_values.par_iter_mut().for_each(|p| *p *= scale);
}
pub(super) fn zero_mean<T, const D: usize>(y: &mut [T], n_samples: usize)
where
T: Float + Send + Sync + Copy + AddAssign + DivAssign + SubAssign,
{
let mut means = y
.par_chunks_exact(D)
.fold(
|| [T::zero(); D],
|mut totals, sample| {
totals
.iter_mut()
.zip(sample.iter())
.for_each(|(total, el)| *total += *el);
totals
},
)
.reduce(
|| [T::zero(); D],
|mut left, right| {
left.iter_mut()
.zip(right.iter())
.for_each(|(total, partial)| *total += *partial);
left
},
);
let n_samples = T::from(n_samples).unwrap();
means.iter_mut().for_each(|mean| *mean /= n_samples);
y.par_chunks_mut(D).for_each(|sample| {
sample
.iter_mut()
.zip(means.iter())
.for_each(|(el, mean)| *el -= *mean);
});
}
pub(crate) fn evaluate_error<T, const D: usize>(p_values: &[T], y: &[T], n_samples: usize) -> T
where
T: Float + Send + Sync + AddAssign + Add + DivAssign + Sum,
{
let mut distances: Vec<T> = vec![T::zero(); n_samples * n_samples];
let (points, _) = y.as_chunks::<D>();
compute_pairwise_distance_matrix(
&mut distances,
|a: &[T; D], b: &[T; D]| {
a.iter()
.zip(b.iter())
.map(|(aa, bb)| (*aa - *bb).powi(2))
.sum::<T>()
},
|i| &points[*i],
n_samples,
);
let inverse_q_sum = distances
.par_iter()
.map(|d| (T::one() + *d).recip())
.sum::<T>()
.recip();
p_values
.par_iter()
.zip(distances.par_iter())
.fold(
|| T::zero(),
|c, (p, d)| {
let q = (T::one() + *d).recip() * inverse_q_sum;
c + *p * ((*p + T::min_positive_value()) / (q + T::min_positive_value())).ln()
},
)
.sum::<T>()
}
pub(crate) fn evaluate_error_approximately<T, const D: usize>(
p_rows: &[usize],
p_columns: &[u32],
p_values: &[T],
y: &[T],
n_samples: usize,
theta: T,
) -> T
where
T: Float + Send + Sync + Sum + AddAssign + SubAssign + MulAssign + DivAssign,
morton::Dim<D>: morton::Morton<D>,
{
let q_sum = {
let arena = arena::Arena::<T, D>::new(y, n_samples);
let theta_sq = theta * theta;
let mut q_sums: Vec<T> = vec![T::zero(); n_samples];
q_sums.par_iter_mut().enumerate().for_each(|(index, sum)| {
let mut negative_forces = [T::zero(); D];
let mut stack = [0u32; arena::STACK_CAP];
arena.compute_non_edge_forces(
index,
theta_sq,
y,
&mut negative_forces,
sum,
&mut stack,
);
});
q_sums.par_iter().map(|sum| *sum).sum::<T>()
};
sparse_kl_divergence::<T, D>(p_rows, p_columns, p_values, y, n_samples, q_sum.recip())
}
pub(crate) fn evaluate_error_interpolated<T, const D: usize>(
p_rows: &[usize],
p_columns: &[u32],
p_values: &[T],
y: &[T],
n_samples: usize,
) -> T
where
T: Float
+ FftNum
+ Send
+ Sync
+ Sum
+ AsPrimitive<usize>
+ AddAssign
+ SubAssign
+ MulAssign
+ DivAssign,
{
let q_sum = {
let mut interpolant = interpolation::Interpolant::<T, D>::new();
let mut scratch_forces = vec![T::zero(); n_samples * D];
let mut q_sum = T::zero();
interpolant.repulsive_forces(y, n_samples, &mut scratch_forces, &mut q_sum);
q_sum
};
sparse_kl_divergence::<T, D>(p_rows, p_columns, p_values, y, n_samples, q_sum.recip())
}
fn sparse_kl_divergence<T, const D: usize>(
p_rows: &[usize],
p_columns: &[u32],
p_values: &[T],
y: &[T],
n_samples: usize,
inverse_q_sum: T,
) -> T
where
T: Float + Send + Sync + Sum + AddAssign,
{
let mut partials: Vec<T> = vec![T::zero(); n_samples];
partials
.par_iter_mut()
.enumerate()
.for_each(|(index, cost)| {
let sample_a = &y[index * D..(index + 1) * D];
for &column in &p_columns[p_rows[index]..p_rows[index + 1]] {
let column = column as usize;
let sample_b = &y[column * D..(column + 1) * D];
let mut q = sample_a
.iter()
.zip(sample_b.iter())
.map(|(a, b)| (*a - *b).powi(2))
.sum::<T>();
q = (T::one() + q).recip() * inverse_q_sum;
*cost += p_values[index]
* ((p_values[index] + T::min_positive_value()) / (q + T::min_positive_value()))
.ln();
}
});
partials.par_iter().map(|partial| *partial).sum::<T>()
}
#[inline]
fn gain_constants<T: Float>() -> (T, T, T) {
(
T::from(GAIN_INCREMENT).unwrap(),
T::from(GAIN_DECAY).unwrap(),
T::from(MIN_GAIN).unwrap(),
)
}