use super::graph_prep::{split, PreparedGraph};
use super::{classical_mds_2d, rows_as_contiguous, sq_dist, Mat};
use crate::matrix::utils::median;
use nalgebra::{DMatrix, DVector};
use rayon::prelude::*;
type Mat64 = DMatrix<f64>;
const DIM: usize = 2;
#[must_use]
pub fn lle_layout_2d_on_graph(features: &Mat, edges: &[(usize, usize, f32)]) -> Mat {
let n = features.nrows();
let graph = PreparedGraph::new(n, edges);
let mut left: Vec<usize> = (0..n).map(|i| graph.neighbours(i).len()).collect();
let mut in_core = vec![true; n];
let mut peeled: Vec<usize> = Vec::new();
let mut stack: Vec<usize> = (0..n).filter(|&i| left[i] <= DIM).collect();
while let Some(i) = stack.pop() {
if !in_core[i] {
continue;
}
in_core[i] = false;
peeled.push(i);
for j in graph.neighbour_ids(i) {
if in_core[j] {
left[j] -= 1;
if left[j] == DIM {
stack.push(j);
}
}
}
}
let core: Vec<usize> = (0..n).filter(|&i| in_core[i]).collect();
let rows = rows_as_contiguous(features);
let core_rows: Vec<&[f32]> = core.iter().map(|&i| rows[i].as_slice()).collect();
let mut xy = Mat::zeros(n, DIM);
let core_xy = lay_out_pieces(&core_rows, &graph.induced(&in_core));
for (p, &i) in core.iter().enumerate() {
xy.row_mut(i).copy_from(&core_xy.row(p));
}
let mut placed = in_core;
let centre = if core.is_empty() {
nalgebra::RowDVector::zeros(DIM)
} else {
core_xy.row_mean()
};
for &i in peeled.iter().rev() {
let mine: Vec<usize> = graph.neighbour_ids(i).filter(|&j| placed[j]).collect();
let at_i = if mine.is_empty() {
centre.clone()
} else {
mine.iter()
.fold(nalgebra::RowDVector::zeros(DIM), |s, &j| s + xy.row(j))
/ mine.len() as f32
};
xy.row_mut(i).copy_from(&at_i);
placed[i] = true;
}
xy
}
fn lay_out_pieces(rows: &[&[f32]], graph: &PreparedGraph) -> Mat {
let pieces = split(graph);
let n_pieces = pieces.len();
let dims = rows.first().map_or(0, |r| r.len());
let centroids: Vec<Vec<f32>> = pieces
.iter()
.map(|p| {
let mut c = vec![0.0_f32; dims];
for &i in &p.nodes {
c.iter_mut().zip(rows[i]).for_each(|(a, &b)| *a += b);
}
c.iter_mut().for_each(|a| *a /= p.nodes.len() as f32);
c
})
.collect();
let layouts: Vec<Mat> = pieces
.par_iter()
.zip(¢roids)
.map(|(p, centroid)| {
let piece_rows: Vec<&[f32]> = p.nodes.iter().map(|&i| rows[i]).collect();
let coords = mlle_coords(&piece_rows, &p.graph);
let ss: f32 = piece_rows.iter().map(|r| sq_dist(r, centroid)).sum();
let scale = (ss / p.nodes.len() as f32).sqrt() / (DIM as f32).sqrt();
coords * scale
})
.collect();
let offsets = if n_pieces > 1 {
log::info!("LLE: {n_pieces} separate pieces, each laid out on its own");
let d2 = Mat::from_fn(n_pieces, n_pieces, |a, b| {
sq_dist(¢roids[a], ¢roids[b])
});
let radius: Vec<f32> = layouts
.iter()
.map(|y| y.row_iter().map(|r| r.norm()).fold(0.0, f32::max))
.collect();
apart(classical_mds_2d(&d2), &radius)
} else {
Mat::zeros(n_pieces, DIM)
};
let mut xy = Mat::zeros(rows.len(), DIM);
for (c, p) in pieces.iter().enumerate() {
for (k, &i) in p.nodes.iter().enumerate() {
for d in 0..DIM {
xy[(i, d)] = offsets[(c, d)] + layouts[c][(k, d)];
}
}
}
xy
}
fn apart(centres: Mat, radius: &[f32]) -> Mat {
let c = centres.nrows();
let mut factor = 1.0_f32;
for a in 0..c {
for b in 0..a {
let d = (centres.row(a) - centres.row(b)).norm();
if d > 1e-6 {
factor = factor.max((radius[a] + radius[b]) / d);
}
}
}
centres * factor
}
fn mlle_coords(rows: &[&[f32]], graph: &PreparedGraph) -> Mat {
let m = rows.len();
let nbrs: Vec<Vec<usize>> = (0..m).map(|i| graph.neighbour_ids(i).collect()).collect();
if m <= DIM + 1 || nbrs.iter().any(|nb| nb.len() <= DIM) {
return Mat::zeros(m, DIM);
}
let local: Vec<(DVector<f64>, Mat64)> = (0..m)
.into_par_iter()
.map(|i| {
let k = nbrs[i].len();
let eig = local_gram(rows[i], &nbrs[i], |j| rows[j]).symmetric_eigen();
let mut o: Vec<usize> = (0..k).collect();
o.sort_by(|&a, &b| eig.eigenvalues[b].total_cmp(&eig.eigenvalues[a]));
let floor = 1e-12 * eig.eigenvalues[o[0]].max(0.0);
let vals = DVector::from_fn(k, |r, _| {
let v = eig.eigenvalues[o[r]];
if v > floor {
v
} else {
0.0
}
});
let vecs = Mat64::from_fn(k, k, |r, c| eig.eigenvectors[(r, o[c])]);
(vals, vecs)
})
.collect();
let rho: Vec<f32> = local
.iter()
.map(|(vals, _)| {
let head: f64 = vals.rows(0, DIM).sum();
(vals.rows(DIM, vals.len() - DIM).sum() / head.max(f64::MIN_POSITIVE)) as f32
})
.collect();
let eta = f64::from(median(&rho));
let blocks: Vec<Mat64> = local
.par_iter()
.map(|(vals, vecs)| mlle_block(vals, vecs, eta))
.collect();
let mut big = Mat64::zeros(m, m);
for (i, w) in blocks.iter().enumerate() {
let nb = &nbrs[i];
let ww = w * w.transpose();
let row_sums: Vec<f64> = (0..nb.len()).map(|a| w.row(a).sum()).collect();
for a in 0..nb.len() {
for b in 0..nb.len() {
big[(nb[a], nb[b])] += ww[(a, b)];
}
big[(i, nb[a])] -= row_sums[a];
big[(nb[a], i)] -= row_sums[a];
}
big[(i, i)] += w.ncols() as f64;
}
let eig = big.symmetric_eigen();
let mut o: Vec<usize> = (0..m).collect();
o.sort_by(|&a, &b| eig.eigenvalues[a].total_cmp(&eig.eigenvalues[b]));
let scale = (m as f64).sqrt();
Mat::from_fn(m, DIM, |r, d| {
(eig.eigenvectors[(r, o[d + 1])] * scale) as f32
})
}
fn mlle_block(vals: &DVector<f64>, vecs: &Mat64, eta: f64) -> Mat64 {
let k = vals.len();
let r = ridge(vals.sum());
let ones = DVector::from_element(k, 1.0);
let mut w_reg = vecs * DVector::from_fn(k, |c, _| vecs.column(c).sum() / (vals[c] + r));
let total = w_reg.sum();
w_reg /= total;
let mut s = 1;
let mut tail = 0.0;
let all: f64 = vals.sum();
for t in 1..=k - DIM {
tail += vals[k - t];
let head = all - tail;
if head > 0.0 && tail / head <= eta {
s = t;
} else {
break;
}
}
let v = vecs.columns(k - s, s).into_owned();
let col_sums = v.transpose() * &ones;
let alpha = col_sums.norm() / (s as f64).sqrt();
let mut h = DVector::from_element(s, alpha) - &col_sums;
let norm_h = h.norm();
if norm_h < 1e-12 {
h.fill(0.0);
} else {
h /= norm_h;
}
let reflected = &v - (&v * &h) * (2.0 * h.transpose());
reflected + w_reg * DVector::from_element(s, 1.0 - alpha).transpose()
}
fn ridge(trace: f64) -> f64 {
(1e-3 * trace).max(1e-12)
}
fn local_gram<'a>(x: &[f32], around: &[usize], row: impl Fn(usize) -> &'a [f32]) -> Mat64 {
let offsets: Vec<Vec<f64>> = around
.iter()
.map(|&j| {
row(j)
.iter()
.zip(x)
.map(|(&a, &b)| f64::from(a - b))
.collect()
})
.collect();
let k = around.len();
Mat64::from_fn(k, k, |a, b| {
offsets[a].iter().zip(&offsets[b]).map(|(p, q)| p * q).sum()
})
}
#[cfg(test)]
mod tests;