use scirs2_core::ndarray::{Array2, ArrayView2, Axis};
use scirs2_linalg::compat::{ArrayLinalgExt, UPLO};
use sklears_core::{
error::{Result as SklResult, SklearsError},
traits::{Estimator, Fit, Transform, Untrained},
types::Float,
};
#[derive(Debug, Clone)]
pub struct LTSA<S = Untrained> {
state: S,
n_neighbors: usize,
n_components: usize,
reg: f64,
eigen_solver: String,
tol: f64,
max_iter: Option<usize>,
neighbors_algorithm: String,
random_state: Option<u64>,
n_jobs: Option<i32>,
}
#[derive(Debug, Clone)]
pub struct LtsaTrained {
pub embedding: Array2<f64>,
pub alignment_matrix: Array2<f64>,
pub local_tangent_spaces: Vec<Array2<f64>>,
}
impl LTSA<Untrained> {
pub fn new() -> Self {
Self {
state: Untrained,
n_neighbors: 5,
n_components: 2,
reg: 1e-3,
eigen_solver: "auto".to_string(),
tol: 1e-6,
max_iter: Some(100),
neighbors_algorithm: "auto".to_string(),
random_state: None,
n_jobs: None,
}
}
pub fn n_neighbors(mut self, n_neighbors: usize) -> Self {
self.n_neighbors = n_neighbors;
self
}
pub fn n_components(mut self, n_components: usize) -> Self {
self.n_components = n_components;
self
}
pub fn reg(mut self, reg: f64) -> Self {
self.reg = reg;
self
}
pub fn eigen_solver(mut self, eigen_solver: &str) -> Self {
self.eigen_solver = eigen_solver.to_string();
self
}
pub fn tol(mut self, tol: f64) -> Self {
self.tol = tol;
self
}
pub fn max_iter(mut self, max_iter: Option<usize>) -> Self {
self.max_iter = max_iter;
self
}
pub fn neighbors_algorithm(mut self, neighbors_algorithm: &str) -> Self {
self.neighbors_algorithm = neighbors_algorithm.to_string();
self
}
pub fn random_state(mut self, random_state: Option<u64>) -> Self {
self.random_state = random_state;
self
}
pub fn n_jobs(mut self, n_jobs: Option<i32>) -> Self {
self.n_jobs = n_jobs;
self
}
}
impl Default for LTSA<Untrained> {
fn default() -> Self {
Self::new()
}
}
impl Estimator for LTSA<Untrained> {
type Config = ();
type Error = SklearsError;
type Float = Float;
fn config(&self) -> &Self::Config {
&()
}
}
impl Fit<ArrayView2<'_, Float>, ()> for LTSA<Untrained> {
type Fitted = LTSA<LtsaTrained>;
fn fit(self, x: &ArrayView2<'_, Float>, _y: &()) -> SklResult<Self::Fitted> {
let x = x.mapv(|x| x);
let (n_samples, n_features) = x.dim();
if n_samples <= self.n_components {
return Err(SklearsError::InvalidInput(
"Number of samples must be greater than n_components".to_string(),
));
}
if self.n_neighbors >= n_samples {
return Err(SklearsError::InvalidInput(
"n_neighbors must be less than number of samples".to_string(),
));
}
if self.n_neighbors <= self.n_components {
return Err(SklearsError::InvalidInput(
"LTSA requires n_neighbors > n_components".to_string(),
));
}
let neighbor_indices = self.find_neighbors(&x)?;
let local_tangent_spaces =
self.compute_local_tangent_spaces(&x, &neighbor_indices, n_features)?;
let alignment_matrix =
self.compute_alignment_matrix(&x, &neighbor_indices, &local_tangent_spaces)?;
let embedding = self.compute_global_embedding(&alignment_matrix)?;
Ok(LTSA {
state: LtsaTrained {
embedding,
alignment_matrix,
local_tangent_spaces,
},
n_neighbors: self.n_neighbors,
n_components: self.n_components,
reg: self.reg,
eigen_solver: self.eigen_solver,
tol: self.tol,
max_iter: self.max_iter,
neighbors_algorithm: self.neighbors_algorithm,
random_state: self.random_state,
n_jobs: self.n_jobs,
})
}
}
impl LTSA<Untrained> {
fn find_neighbors(&self, x: &Array2<f64>) -> SklResult<Array2<usize>> {
let n_samples = x.nrows();
let mut neighbor_indices = Array2::zeros((n_samples, self.n_neighbors));
for i in 0..n_samples {
let mut distances: Vec<(f64, usize)> = Vec::new();
for j in 0..n_samples {
if i != j {
let diff = &x.row(i) - &x.row(j);
let dist = diff.mapv(|x| x * x).sum().sqrt();
distances.push((dist, j));
}
}
distances.sort_by(|a, b| a.0.partial_cmp(&b.0).expect("operation should succeed"));
for (neighbor_idx, &(_, j)) in distances.iter().take(self.n_neighbors).enumerate() {
neighbor_indices[[i, neighbor_idx]] = j;
}
}
Ok(neighbor_indices)
}
fn compute_local_tangent_spaces(
&self,
x: &Array2<f64>,
neighbor_indices: &Array2<usize>,
n_features: usize,
) -> SklResult<Vec<Array2<f64>>> {
let n_samples = x.nrows();
let mut local_tangent_spaces = Vec::with_capacity(n_samples);
for i in 0..n_samples {
let neighbors: Vec<usize> = (0..self.n_neighbors)
.map(|j| neighbor_indices[[i, j]])
.collect();
let mut neighborhood = Array2::zeros((self.n_neighbors, n_features));
for (k, &neighbor_idx) in neighbors.iter().enumerate() {
for d in 0..n_features {
neighborhood[[k, d]] = x[[neighbor_idx, d]];
}
}
let mean = neighborhood
.mean_axis(Axis(0))
.expect("operation should succeed");
for k in 0..self.n_neighbors {
for d in 0..n_features {
neighborhood[[k, d]] -= mean[d];
}
}
let (_, s, vt) = neighborhood
.svd(true)
.map_err(|e| SklearsError::InvalidInput(format!("SVD failed: {e}")))?;
let vt_matrix = vt;
let mut tangent_space = Array2::zeros((self.n_components, n_features));
for comp in 0..self.n_components {
if comp < s.len() && s[comp] > 1e-12 {
for d in 0..n_features {
tangent_space[[comp, d]] = vt_matrix[[comp, d]];
}
}
}
local_tangent_spaces.push(tangent_space);
}
Ok(local_tangent_spaces)
}
fn compute_alignment_matrix(
&self,
x: &Array2<f64>,
neighbor_indices: &Array2<usize>,
local_tangent_spaces: &[Array2<f64>],
) -> SklResult<Array2<f64>> {
let n_samples = x.nrows();
let mut alignment_matrix = Array2::zeros((n_samples, n_samples));
for i in 0..n_samples {
let neighbors: Vec<usize> = (0..self.n_neighbors)
.map(|j| neighbor_indices[[i, j]])
.collect();
let tangent_space = &local_tangent_spaces[i];
let mut local_coords = Array2::zeros((self.n_neighbors, self.n_components));
let mut center = Array2::<f64>::zeros((1, x.ncols()));
for &neighbor_idx in &neighbors {
for d in 0..x.ncols() {
center[[0, d]] += x[[neighbor_idx, d]];
}
}
for d in 0..x.ncols() {
center[[0, d]] /= self.n_neighbors as f64;
}
for (k, &neighbor_idx) in neighbors.iter().enumerate() {
for comp in 0..self.n_components {
let mut coord = 0.0;
for d in 0..x.ncols() {
coord += (x[[neighbor_idx, d]] - center[[0, d]]) * tangent_space[[comp, d]];
}
local_coords[[k, comp]] = coord;
}
}
let weights = self.compute_reconstruction_weights(&local_coords)?;
for (a, &neighbor_a) in neighbors.iter().enumerate() {
for (b, &neighbor_b) in neighbors.iter().enumerate() {
alignment_matrix[[neighbor_a, neighbor_b]] += weights[[a, b]];
}
}
}
Ok(alignment_matrix)
}
fn compute_reconstruction_weights(&self, local_coords: &Array2<f64>) -> SklResult<Array2<f64>> {
let n_neighbors = local_coords.nrows();
let mut weights = Array2::zeros((n_neighbors, n_neighbors));
for i in 0..n_neighbors {
weights[[i, i]] = 1.0;
for j in 0..n_neighbors {
if i != j {
let mut dist_sq = 0.0;
for comp in 0..self.n_components {
let diff = local_coords[[i, comp]] - local_coords[[j, comp]];
dist_sq += diff * diff;
}
weights[[i, j]] = (-dist_sq / (2.0 * self.reg)).exp();
}
}
}
for i in 0..n_neighbors {
let row_sum: f64 = weights.row(i).sum();
if row_sum > 1e-12 {
for j in 0..n_neighbors {
weights[[i, j]] /= row_sum;
}
}
}
Ok(weights)
}
fn compute_global_embedding(&self, alignment_matrix: &Array2<f64>) -> SklResult<Array2<f64>> {
let n_samples = alignment_matrix.nrows();
let (eigenvals, eigenvecs) = alignment_matrix
.eigh(UPLO::Lower)
.map_err(|e| SklearsError::InvalidInput(format!("Eigendecomposition failed: {e}")))?;
let mut eigen_pairs: Vec<(f64, usize)> = eigenvals
.iter()
.enumerate()
.map(|(i, &val)| (val, i))
.collect();
eigen_pairs.sort_by(|a, b| a.0.partial_cmp(&b.0).expect("operation should succeed"));
let mut embedding = Array2::zeros((n_samples, self.n_components));
for (comp_idx, &(eigenval, eigen_idx)) in eigen_pairs
.iter()
.skip(1)
.take(self.n_components)
.enumerate()
{
if eigenval > 1e-12 {
for i in 0..n_samples {
embedding[[i, comp_idx]] = eigenvecs[[i, eigen_idx]];
}
}
}
Ok(embedding)
}
}
impl Transform<ArrayView2<'_, Float>, Array2<Float>> for LTSA<LtsaTrained> {
fn transform(&self, _x: &ArrayView2<'_, Float>) -> SklResult<Array2<Float>> {
Err(SklearsError::InvalidOperation(
"LTSA does not support transforming new data. Use fit_transform for training data."
.to_string(),
))
}
}
impl LTSA<LtsaTrained> {
pub fn embedding(&self) -> &Array2<f64> {
&self.state.embedding
}
pub fn alignment_matrix(&self) -> &Array2<f64> {
&self.state.alignment_matrix
}
pub fn local_tangent_spaces(&self) -> &[Array2<f64>] {
&self.state.local_tangent_spaces
}
}