use crate::matrix::kmeans::kmeans_centroids_seeded;
use crate::matrix::pca::{random_init_2d, INIT_SCALE};
use nalgebra::DMatrix;
use rand::{rngs::SmallRng, RngExt, SeedableRng};
use rayon::prelude::*;
type Mat = DMatrix<f32>;
pub const N_LANDMARKS: usize = 300;
pub const MIN_LINKS: usize = 3;
const KMEANS_ITER: usize = 30;
const COINCIDENT: f32 = 1e-3;
#[must_use]
pub fn landmark_init_2d(
features: &Mat,
edges: &[(usize, usize, f32)],
n_landmarks: usize,
seed: u64,
) -> Vec<f32> {
let n = features.nrows();
let (centroids, labels) =
kmeans_centroids_seeded(features, n_landmarks.min(n), KMEANS_ITER, seed);
let (centroids, labels, weights) = drop_empty(¢roids, &labels);
let k = centroids.nrows();
if k < 2 {
return random_init_2d(n, seed);
}
let geodesics = landmark_geodesics(¢roids, &labels, edges);
let mut pos = weighted_mds_2d(&geodesics, &weights);
let reach = pos.iter().flatten().fold(0.0_f32, |m, &v| m.max(v.abs()));
if reach <= 0.0 || !reach.is_finite() {
return random_init_2d(n, seed);
}
pos.iter_mut()
.flatten()
.for_each(|v| *v = *v * INIT_SCALE / reach);
let spacing = (0..k)
.map(|a| {
(0..k)
.map(|b| (pos[a][0] - pos[b][0]).hypot(pos[a][1] - pos[b][1]))
.filter(|&d| d > COINCIDENT * INIT_SCALE)
.fold(f32::INFINITY, f32::min)
})
.sum::<f32>()
/ k as f32;
let half = 0.5 * spacing;
let mut rng = SmallRng::seed_from_u64(seed ^ 0x1a4d_3a2c_5eed_0001);
labels
.iter()
.flat_map(|&l| pos[l])
.map(|v| v + rng.random_range(-0.5..0.5) * half)
.collect()
}
fn drop_empty(centroids: &Mat, labels: &[usize]) -> (Mat, Vec<usize>, Vec<f32>) {
let mut count = vec![0_usize; centroids.nrows()];
for &l in labels {
count[l] += 1;
}
let kept: Vec<usize> = (0..count.len()).filter(|&l| count[l] > 0).collect();
let mut renumber = vec![usize::MAX; count.len()];
for (new, &old) in kept.iter().enumerate() {
renumber[old] = new;
}
let centroids = centroids.select_rows(&kept);
let labels = labels.iter().map(|&l| renumber[l]).collect();
let weights = kept.iter().map(|&l| count[l] as f32).collect();
(centroids, labels, weights)
}
fn landmark_geodesics(centroids: &Mat, labels: &[usize], edges: &[(usize, usize, f32)]) -> Mat {
let k = centroids.nrows();
let straight = |a: usize, b: usize| (centroids.row(a) - centroids.row(b)).norm();
let mut links = vec![0_u32; k * k];
for &(i, j, _) in edges {
let (a, b) = (labels[i], labels[j]);
if a != b {
links[a * k + b] += 1;
links[b * k + a] += 1;
}
}
let mut g: Vec<f32> = (0..k * k)
.map(|ab| {
let (a, b) = (ab / k, ab % k);
if a == b {
0.0
} else if links[ab] as usize >= MIN_LINKS {
straight(a, b)
} else {
f32::INFINITY
}
})
.collect();
let mut via = vec![0.0_f32; k];
for m in 0..k {
via.copy_from_slice(&g[m * k..(m + 1) * k]);
g.par_chunks_mut(k).for_each(|row| {
let to_m = row[m];
if to_m.is_finite() {
for (d, &v) in row.iter_mut().zip(&via) {
*d = d.min(to_m + v);
}
}
});
}
for (ab, d) in g.iter_mut().enumerate() {
if !d.is_finite() {
*d = straight(ab / k, ab % k);
}
}
Mat::from_vec(k, k, g)
}
fn weighted_mds_2d(d: &Mat, w: &[f32]) -> Vec<[f32; 2]> {
let k = d.nrows();
let total: f32 = w.iter().sum();
let w: Vec<f32> = w.iter().map(|&x| x / total).collect();
let d2 = d.map(|x| x * x);
let mean: Vec<f32> = (0..k)
.map(|a| (0..k).map(|b| w[b] * d2[(a, b)]).sum())
.collect();
let grand: f32 = (0..k).map(|a| w[a] * mean[a]).sum();
let gram = Mat::from_fn(k, k, |a, b| {
-0.5 * (d2[(a, b)] - mean[a] - mean[b] + grand) * (w[a] * w[b]).sqrt()
});
let eig = gram.symmetric_eigen();
let mut order: Vec<usize> = (0..k).collect();
order.sort_by(|&x, &y| eig.eigenvalues[y].total_cmp(&eig.eigenvalues[x]));
let axis = |c: usize| {
let e = order[c];
(e, eig.eigenvalues[e].max(0.0).sqrt())
};
let ((e0, s0), (e1, s1)) = (axis(0), axis(1));
(0..k)
.map(|a| {
let unweigh = w[a].sqrt();
[
eig.eigenvectors[(a, e0)] * s0 / unweigh,
eig.eigenvectors[(a, e1)] * s1 / unweigh,
]
})
.collect()
}
#[cfg(test)]
pub(crate) mod tests;