pub(super) mod sptree;
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};
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_iter_mut()
.enumerate()
.map(|(index, d)| {
let row_index = index / n_samples;
let column_index = index % n_samples;
(row_index, column_index, d)
})
.filter(|(row_index, column_index, _)| row_index < column_index)
.for_each(|(i, j, d)| {
*d = f(g(&i), 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.powi(2)).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.powi(2));
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]) {
let p_values_sum: T = p_values.par_iter().copied().sum::<T>();
let scale = T::from(12.0).unwrap() / (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<usize>,
p_columns: Vec<usize>,
p_values: &mut Vec<T>,
n_samples: usize,
n_neighbors: &usize,
) where
T: Float + Add + DivAssign + Send + Sync + MulAssign,
{
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;
if !p_columns[p_rows(p_columns[i])..p_rows(p_columns[i] + 1)].contains(&n) {
row_counts[p_columns[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<usize> = 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 mut present: bool = false;
for m in p_rows(p_columns[i])..p_rows(p_columns[i] + 1) {
if p_columns[m] == _n {
present = true;
if _n <= p_columns[i] {
sym_col_p[sym_row_p[_n] + offset[_n]] = p_columns[i];
sym_col_p[sym_row_p[p_columns[i]] + offset[p_columns[i]]] = _n;
sym_val_p[sym_row_p[_n] + offset[_n]] = p_values[i] + p_values[m];
sym_val_p[sym_row_p[p_columns[i]] + offset[p_columns[i]]] =
p_values[i] + p_values[m];
}
}
}
if !present {
sym_col_p[sym_row_p[_n] + offset[_n]] = p_columns[i];
sym_col_p[sym_row_p[p_columns[i]] + offset[p_columns[i]]] = _n;
sym_val_p[sym_row_p[_n] + offset[_n]] = p_values[i];
sym_val_p[sym_row_p[p_columns[i]] + offset[p_columns[i]]] = p_values[i];
}
if !present || _n <= p_columns[i] {
offset[_n] += 1;
if p_columns[i] != _n {
offset[p_columns[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 zero_point_two = T::from(0.2).unwrap();
let zero_point_eight = T::from(0.8).unwrap();
let zero_point_zero_one = T::from(0.01).unwrap();
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 + zero_point_two
} else {
*gains_el * zero_point_eight
};
if *gains_el < zero_point_zero_one {
*gains_el = zero_point_zero_one;
}
*uy_el = *momentum * *uy_el - *learning_rate * *gains_el * *dy_el;
*y_el += *uy_el
});
}
pub(super) fn stop_lying<T: Float + Send + Sync + MulAssign>(p_values: &mut [T]) {
let scale = T::one() / T::from(12.0).unwrap();
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 mut q_values: Vec<T> = vec![T::zero(); n_samples * n_samples];
q_values
.par_iter_mut()
.zip(distances.par_iter())
.for_each(|(q, d)| *q = T::one() / (T::one() + *d));
let q_sum = q_values.par_iter().map(|q| *q).sum::<T>();
let inverse_q_sum = T::one() / q_sum;
q_values
.par_iter_mut()
.for_each(|q| *q = *q * inverse_q_sum);
p_values
.par_iter()
.zip(q_values.par_iter())
.fold(
|| T::zero(),
|c, (p, q)| {
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: &[usize],
p_values: &[T],
y: &[T],
n_samples: usize,
theta: T,
) -> T
where
T: Float + Send + Sync + Sum + AddAssign + SubAssign + MulAssign + DivAssign,
{
let q_sum = {
let tree = sptree::SPTree::<T, D>::new(y, n_samples);
let mut q_sums: Vec<T> = vec![T::zero(); n_samples];
q_sums.par_iter_mut().enumerate().for_each(|(index, sum)| {
let mut buffer = [T::zero(); D];
let mut negative_forces = [T::zero(); D];
tree.compute_non_edge_forces(index, theta, &mut negative_forces, &mut buffer, sum);
});
q_sums.par_iter().map(|sum| *sum).sum::<T>()
};
let inverse_q_sum = T::one() / q_sum;
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 n in p_rows[index]..p_rows[index + 1] {
let sample_b = &y[p_columns[n] * D..(p_columns[n] + 1) * D];
let mut q = sample_a
.iter()
.zip(sample_b.iter())
.map(|(a, b)| (*a - *b).powi(2))
.sum::<T>();
q = (T::one() / (T::one() + q)) * 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>()
}