use alloc::vec::Vec;
#[cfg(not(feature = "std"))]
use num_traits::Float;
use num_traits::{AsPrimitive, ToPrimitive};
use super::jacobi::{JacobiConfig, JacobiError, jacobi_decompose, sort_eigen, validate_config};
use crate::traits::{DenseValuedMatrix2D, Finite, Number};
#[derive(Debug, Clone, PartialEq)]
pub struct MdsConfig {
pub dimensions: usize,
pub jacobi: JacobiConfig,
}
impl Default for MdsConfig {
#[inline]
fn default() -> Self {
Self { dimensions: 2, jacobi: JacobiConfig::default() }
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct MdsResult {
coordinates: Vec<f64>,
eigenvalues: Vec<f64>,
stress: f64,
n: usize,
dims: usize,
}
impl MdsResult {
#[must_use]
#[inline]
pub fn point(&self, i: usize) -> &[f64] {
let start = i * self.dims;
&self.coordinates[start..start + self.dims]
}
#[must_use]
#[inline]
pub fn coordinates_flat(&self) -> &[f64] {
&self.coordinates
}
#[must_use]
#[inline]
pub fn eigenvalues(&self) -> &[f64] {
&self.eigenvalues
}
#[must_use]
#[inline]
pub fn stress(&self) -> f64 {
self.stress
}
#[must_use]
#[inline]
pub fn dimensions(&self) -> usize {
self.dims
}
#[must_use]
#[inline]
pub fn num_points(&self) -> usize {
self.n
}
}
#[derive(Debug, Clone, PartialEq, thiserror::Error)]
pub enum MdsError {
#[error("The distance matrix must be square, but has {rows} rows and {columns} columns.")]
NonSquareMatrix {
rows: usize,
columns: usize,
},
#[error(
"The distance matrix is not symmetric: value at ({row}, {column}) differs from ({column}, {row})."
)]
NonSymmetricMatrix {
row: usize,
column: usize,
},
#[error("Found a non-finite value at ({row}, {column}).")]
NonFiniteValue {
row: usize,
column: usize,
},
#[error("Found a negative distance at ({row}, {column}).")]
NegativeDistance {
row: usize,
column: usize,
},
#[error("The distance matrix is empty.")]
EmptyMatrix,
#[error("The number of embedding dimensions must be at least 1, but got {0}.")]
InvalidDimensions(usize),
#[error(
"Requested {dimensions} embedding dimensions, but the matrix has only {num_points} points."
)]
DimensionsExceedPoints {
dimensions: usize,
num_points: usize,
},
#[error("The maximum distance {0} is too large; squaring it would overflow f64.")]
DistanceTooLarge(f64),
#[error("The diagonal entry at ({index}, {index}) must be zero, but is {value}.")]
DiagonalNotZero {
index: usize,
value: f64,
},
#[error("Need at least 2 points for MDS, but got {0}.")]
TooFewPoints(usize),
#[error(transparent)]
JacobiError(#[from] JacobiError),
}
pub trait ClassicalMds: DenseValuedMatrix2D + Sized
where
Self::Value: Number + ToPrimitive + Finite,
Self::RowIndex: AsPrimitive<usize>,
Self::ColumnIndex: AsPrimitive<usize>,
{
fn classical_mds(&self, config: &MdsConfig) -> Result<MdsResult, MdsError> {
if config.dimensions == 0 {
return Err(MdsError::InvalidDimensions(0));
}
validate_config(&config.jacobi)?;
let num_rows: usize = self.number_of_rows().as_();
let num_cols: usize = self.number_of_columns().as_();
if num_rows != num_cols {
return Err(MdsError::NonSquareMatrix { rows: num_rows, columns: num_cols });
}
let n = num_rows;
if n == 0 {
return Err(MdsError::EmptyMatrix);
}
if n == 1 {
return Err(MdsError::TooFewPoints(1));
}
if config.dimensions > n {
return Err(MdsError::DimensionsExceedPoints {
dimensions: config.dimensions,
num_points: n,
});
}
let mut dist = Vec::with_capacity(n * n);
for row_id in self.row_indices() {
let row_idx: usize = row_id.as_();
for (col_id, val) in self.column_indices().zip(self.row_values(row_id)) {
let col_idx: usize = col_id.as_();
if !val.is_finite() {
return Err(MdsError::NonFiniteValue { row: row_idx, column: col_idx });
}
let value = val
.to_f64()
.ok_or(MdsError::NonFiniteValue { row: row_idx, column: col_idx })?;
if !value.is_finite() {
return Err(MdsError::NonFiniteValue { row: row_idx, column: col_idx });
}
if value < 0.0 {
return Err(MdsError::NegativeDistance { row: row_idx, column: col_idx });
}
dist.push(value);
}
}
for i in 0..n {
let d = dist[i * n + i];
if d != 0.0 {
return Err(MdsError::DiagonalNotZero { index: i, value: d });
}
}
for i in 0..n {
for j in (i + 1)..n {
let upper = dist[i * n + j];
let lower = dist[j * n + i];
let scale = upper.abs().max(lower.abs()).max(1.0);
if (upper - lower).abs() > 16.0 * f64::EPSILON * scale {
return Err(MdsError::NonSymmetricMatrix { row: i, column: j });
}
}
}
for i in 0..n {
for j in (i + 1)..n {
let avg = dist[i * n + j].midpoint(dist[j * n + i]);
dist[i * n + j] = avg;
dist[j * n + i] = avg;
}
}
let max_dist = dist.iter().copied().fold(0.0_f64, f64::max);
if max_dist > 1e154 {
return Err(MdsError::DistanceTooLarge(max_dist));
}
let orig_dist = dist.clone();
let mut b = square_and_double_center(&dist, n);
let mut evecs =
jacobi_decompose(&mut b, n, config.jacobi.tolerance, config.jacobi.max_sweeps)?;
let mut eigenvalues: Vec<f64> = (0..n).map(|i| b[i * n + i]).collect();
sort_eigen(&mut eigenvalues, &mut evecs, n);
let k = config.dimensions;
let top_eigenvalues: Vec<f64> = eigenvalues[..k].to_vec();
let mut coordinates = vec![0.0; n * k];
for d in 0..k {
let sqrt_lambda = eigenvalues[d].max(0.0).sqrt();
for i in 0..n {
coordinates[i * k + d] = sqrt_lambda * evecs[d * n + i];
}
}
let stress = compute_stress(&orig_dist, &coordinates, n, k);
Ok(MdsResult { coordinates, eigenvalues: top_eigenvalues, stress, n, dims: k })
}
}
impl<M: DenseValuedMatrix2D> ClassicalMds for M
where
M::Value: Number + ToPrimitive + Finite,
M::RowIndex: AsPrimitive<usize>,
M::ColumnIndex: AsPrimitive<usize>,
{
}
#[allow(clippy::cast_precision_loss)]
fn square_and_double_center(dist: &[f64], n: usize) -> Vec<f64> {
let mut d_sq = vec![0.0; n * n];
for (sq, &d) in d_sq.iter_mut().zip(dist.iter()) {
*sq = d * d;
}
let mut row_means = vec![0.0; n];
for i in 0..n {
let mut sum = 0.0;
for j in 0..n {
sum += d_sq[i * n + j];
}
row_means[i] = sum / n as f64;
}
let grand_mean: f64 = row_means.iter().sum::<f64>() / n as f64;
let mut b = vec![0.0; n * n];
for i in 0..n {
for j in 0..n {
b[i * n + j] = -0.5 * (d_sq[i * n + j] - row_means[i] - row_means[j] + grand_mean);
}
}
for i in 0..n {
for j in (i + 1)..n {
let avg = b[i * n + j].midpoint(b[j * n + i]);
b[i * n + j] = avg;
b[j * n + i] = avg;
}
}
b
}
fn compute_stress(orig_dist: &[f64], coordinates: &[f64], n: usize, k: usize) -> f64 {
let mut numerator = 0.0;
let mut denominator = 0.0;
for i in 0..n {
for j in (i + 1)..n {
let d_orig = orig_dist[i * n + j];
let mut d_embed_sq = 0.0;
for d in 0..k {
let diff = coordinates[i * k + d] - coordinates[j * k + d];
d_embed_sq += diff * diff;
}
let d_embed = d_embed_sq.sqrt();
let residual = d_orig - d_embed;
numerator += residual * residual;
denominator += d_orig * d_orig;
}
}
if denominator == 0.0 { 0.0 } else { (numerator / denominator).sqrt() }
}