use crate::error::{LinalgError, LinalgResult};
use scirs2_core::ndarray::{Array1, Array2, ArrayView2, ScalarOperand};
use scirs2_core::numeric::{Float, NumAssign};
use std::iter::Sum;
pub trait TensorFloat: Float + NumAssign + Sum + ScalarOperand + Send + Sync + 'static {}
impl<T> TensorFloat for T where T: Float + NumAssign + Sum + ScalarOperand + Send + Sync + 'static {}
#[derive(Debug, Clone)]
pub struct Tensor<F> {
pub data: Vec<F>,
pub shape: Vec<usize>,
}
impl<F: TensorFloat> Tensor<F> {
pub fn new(shape: Vec<usize>, data: Vec<F>) -> LinalgResult<Self> {
let total: usize = shape.iter().product();
if data.len() != total {
return Err(LinalgError::ShapeError(format!(
"Data length {} does not match shape {:?} (total {})",
data.len(),
shape,
total
)));
}
Ok(Self { data, shape })
}
pub fn zeros(shape: Vec<usize>) -> Self {
let total: usize = shape.iter().product();
Self {
data: vec![F::zero(); total],
shape,
}
}
pub fn ndim(&self) -> usize {
self.shape.len()
}
pub fn numel(&self) -> usize {
self.data.len()
}
pub fn get(&self, idx: &[usize]) -> LinalgResult<F> {
let flat = self.flat_index(idx)?;
Ok(self.data[flat])
}
pub fn set(&mut self, idx: &[usize], value: F) -> LinalgResult<()> {
let flat = self.flat_index(idx)?;
self.data[flat] = value;
Ok(())
}
fn flat_index(&self, idx: &[usize]) -> LinalgResult<usize> {
if idx.len() != self.shape.len() {
return Err(LinalgError::IndexError(format!(
"Index has {} dimensions but tensor has {}",
idx.len(),
self.shape.len()
)));
}
let mut flat = 0usize;
let mut stride = 1usize;
for d in (0..self.shape.len()).rev() {
if idx[d] >= self.shape[d] {
return Err(LinalgError::IndexError(format!(
"Index {} out of bounds for dimension {} (size {})",
idx[d], d, self.shape[d]
)));
}
flat += idx[d] * stride;
stride *= self.shape[d];
}
Ok(flat)
}
fn multi_index(&self, mut flat: usize) -> Vec<usize> {
let mut idx = vec![0usize; self.shape.len()];
for d in (0..self.shape.len()).rev() {
idx[d] = flat % self.shape[d];
flat /= self.shape[d];
}
idx
}
}
pub fn unfold<F: TensorFloat>(tensor: &Tensor<F>, mode: usize) -> LinalgResult<Array2<F>> {
let ndim = tensor.ndim();
if mode >= ndim {
return Err(LinalgError::IndexError(format!(
"Mode {mode} out of range for {ndim}-D tensor"
)));
}
let i_mode = tensor.shape[mode];
let other_size: usize = tensor
.shape
.iter()
.enumerate()
.filter(|&(d, _)| d != mode)
.map(|(_, &s)| s)
.product();
let mut mat = Array2::<F>::zeros((i_mode, other_size));
for flat in 0..tensor.numel() {
let idx = tensor.multi_index(flat);
let row = idx[mode];
let mut col = 0usize;
let mut col_stride = 1usize;
for d in (0..ndim).rev() {
if d == mode {
continue;
}
col += idx[d] * col_stride;
col_stride *= tensor.shape[d];
}
mat[[row, col]] = tensor.data[flat];
}
Ok(mat)
}
pub fn fold<F: TensorFloat>(
mat: &ArrayView2<F>,
mode: usize,
shape: &[usize],
) -> LinalgResult<Tensor<F>> {
let ndim = shape.len();
if mode >= ndim {
return Err(LinalgError::IndexError(format!(
"Mode {mode} out of range for {ndim}-D tensor"
)));
}
if mat.shape()[0] != shape[mode] {
return Err(LinalgError::ShapeError(
"Matrix rows must equal shape[mode]".into(),
));
}
let total: usize = shape.iter().product();
let mut data = vec![F::zero(); total];
let mut tensor = Tensor {
data: data.clone(),
shape: shape.to_vec(),
};
for (flat, data_elem) in data.iter_mut().enumerate().take(total) {
let idx = tensor.multi_index(flat);
let row = idx[mode];
let mut col = 0usize;
let mut col_stride = 1usize;
for d in (0..ndim).rev() {
if d == mode {
continue;
}
col += idx[d] * col_stride;
col_stride *= shape[d];
}
*data_elem = mat[[row, col]];
}
tensor.data = data;
Ok(tensor)
}
pub fn n_mode_product<F: TensorFloat>(
tensor: &Tensor<F>,
matrix: &ArrayView2<F>,
mode: usize,
) -> LinalgResult<Tensor<F>> {
let ndim = tensor.ndim();
if mode >= ndim {
return Err(LinalgError::IndexError(format!(
"Mode {mode} out of range for {ndim}-D tensor"
)));
}
if matrix.shape()[1] != tensor.shape[mode] {
return Err(LinalgError::ShapeError(format!(
"Matrix columns ({}) must equal tensor dimension {} (size {})",
matrix.shape()[1],
mode,
tensor.shape[mode]
)));
}
let unfolded = unfold(tensor, mode)?;
let product = matrix.dot(&unfolded);
let mut new_shape = tensor.shape.clone();
new_shape[mode] = matrix.shape()[0];
fold(&product.view(), mode, &new_shape)
}
pub fn khatri_rao<F: TensorFloat>(a: &ArrayView2<F>, b: &ArrayView2<F>) -> LinalgResult<Array2<F>> {
let ra = a.shape()[1];
let rb = b.shape()[1];
if ra != rb {
return Err(LinalgError::ShapeError(
"Matrices must have the same number of columns for Khatri-Rao product".into(),
));
}
let ia = a.shape()[0];
let jb = b.shape()[0];
let r = ra;
let mut result = Array2::<F>::zeros((ia * jb, r));
for col in 0..r {
for i in 0..ia {
for j in 0..jb {
result[[i * jb + j, col]] = a[[i, col]] * b[[j, col]];
}
}
}
Ok(result)
}
pub fn tensor_norm_frobenius<F: TensorFloat>(tensor: &Tensor<F>) -> F {
let mut acc = F::zero();
for &v in &tensor.data {
acc += v * v;
}
acc.sqrt()
}
pub fn tensor_norm_max<F: TensorFloat>(tensor: &Tensor<F>) -> F {
let mut mx = F::zero();
for &v in &tensor.data {
let av = v.abs();
if av > mx {
mx = av;
}
}
mx
}
pub fn tensor_norm_l1<F: TensorFloat>(tensor: &Tensor<F>) -> F {
let mut acc = F::zero();
for &v in &tensor.data {
acc += v.abs();
}
acc
}
#[derive(Debug, Clone)]
pub struct CpResult<F> {
pub factors: Vec<Array2<F>>,
pub weights: Array1<F>,
pub reconstruction_error: F,
pub iterations: usize,
}
pub fn cp_als<F: TensorFloat>(
tensor: &Tensor<F>,
rank: usize,
max_iter: Option<usize>,
tol: Option<F>,
) -> LinalgResult<CpResult<F>> {
let ndim = tensor.ndim();
if ndim < 2 {
return Err(LinalgError::ValueError(
"Tensor must have at least 2 dimensions".into(),
));
}
if rank == 0 {
return Err(LinalgError::ValueError("Rank must be > 0".into()));
}
let max_it = max_iter.unwrap_or(200);
let eps = tol.unwrap_or_else(|| F::from(1e-8).unwrap_or(F::epsilon()));
let mut factors: Vec<Array2<F>> = Vec::with_capacity(ndim);
for d in 0..ndim {
let rows = tensor.shape[d];
let mut mat = Array2::<F>::zeros((rows, rank));
for i in 0..rows {
for r in 0..rank {
let val = F::from((i + r + d + 1) as f64).unwrap_or(F::one())
/ F::from((rows + rank) as f64).unwrap_or(F::one());
mat[[i, r]] = val;
}
}
factors.push(mat);
}
let tensor_norm = tensor_norm_frobenius(tensor);
for iter in 0..max_it {
for mode in 0..ndim {
let kr = khatri_rao_all_except(&factors, mode)?;
let x_unf = unfold(tensor, mode)?;
let mut v = Array2::<F>::ones((rank, rank));
for (j, factor_j) in factors.iter().enumerate().take(ndim) {
if j == mode {
continue;
}
let ftf = factor_j.t().dot(factor_j);
for r1 in 0..rank {
for r2 in 0..rank {
v[[r1, r2]] *= ftf[[r1, r2]];
}
}
}
let rhs = x_unf.dot(&kr);
match crate::inv(&v.view(), None) {
Ok(v_inv) => {
factors[mode] = rhs.dot(&v_inv);
}
Err(_) => {
let reg = F::from(1e-12).unwrap_or(F::epsilon());
let mut v_reg = v.clone();
for r in 0..rank {
v_reg[[r, r]] += reg;
}
let v_inv = crate::inv(&v_reg.view(), None)?;
factors[mode] = rhs.dot(&v_inv);
}
}
}
let reconstructed = reconstruct_cp(&factors, tensor)?;
let mut err_sq = F::zero();
for i in 0..tensor.numel() {
let diff = tensor.data[i] - reconstructed.data[i];
err_sq += diff * diff;
}
let err = err_sq.sqrt();
if tensor_norm > F::epsilon() {
let rel_err = err / tensor_norm;
if rel_err < eps {
let (normed_factors, weights) = normalize_cp_factors(&factors);
return Ok(CpResult {
factors: normed_factors,
weights,
reconstruction_error: err,
iterations: iter + 1,
});
}
}
}
let reconstructed = reconstruct_cp(&factors, tensor)?;
let mut err_sq = F::zero();
for i in 0..tensor.numel() {
let diff = tensor.data[i] - reconstructed.data[i];
err_sq += diff * diff;
}
let (normed_factors, weights) = normalize_cp_factors(&factors);
Ok(CpResult {
factors: normed_factors,
weights,
reconstruction_error: err_sq.sqrt(),
iterations: max_it,
})
}
fn khatri_rao_all_except<F: TensorFloat>(
factors: &[Array2<F>],
skip: usize,
) -> LinalgResult<Array2<F>> {
let ndim = factors.len();
let mut result: Option<Array2<F>> = None;
for (d, factor) in factors.iter().enumerate().take(ndim) {
if d == skip {
continue;
}
result = Some(match result {
None => factor.clone(),
Some(prev) => khatri_rao(&prev.view(), &factor.view())?,
});
}
result.ok_or_else(|| LinalgError::ValueError("No factors to combine".into()))
}
fn reconstruct_cp<F: TensorFloat>(
factors: &[Array2<F>],
reference: &Tensor<F>,
) -> LinalgResult<Tensor<F>> {
let ndim = factors.len();
let rank = factors[0].shape()[1];
let total = reference.numel();
let shape = &reference.shape;
let mut data = vec![F::zero(); total];
for (flat, item) in data.iter_mut().enumerate().take(total) {
let idx = reference.multi_index(flat);
let mut val = F::zero();
for r in 0..rank {
let mut prod = F::one();
for d in 0..ndim {
prod *= factors[d][[idx[d], r]];
}
val += prod;
}
*item = val;
}
Ok(Tensor {
data,
shape: shape.clone(),
})
}
fn normalize_cp_factors<F: TensorFloat>(factors: &[Array2<F>]) -> (Vec<Array2<F>>, Array1<F>) {
let rank = factors[0].shape()[1];
let mut weights = Array1::<F>::ones(rank);
let mut normed: Vec<Array2<F>> = factors.to_vec();
for factor in &mut normed {
let rows = factor.shape()[0];
for r in 0..rank {
let mut col_norm_sq = F::zero();
for i in 0..rows {
col_norm_sq += factor[[i, r]] * factor[[i, r]];
}
let col_norm = col_norm_sq.sqrt();
if col_norm > F::epsilon() {
weights[r] *= col_norm;
for i in 0..rows {
factor[[i, r]] /= col_norm;
}
}
}
}
(normed, weights)
}
#[derive(Debug, Clone)]
pub struct TuckerResult<F> {
pub core: Tensor<F>,
pub factors: Vec<Array2<F>>,
pub reconstruction_error: F,
}
pub fn tucker_hosvd<F: TensorFloat>(
tensor: &Tensor<F>,
ranks: &[usize],
) -> LinalgResult<TuckerResult<F>> {
let ndim = tensor.ndim();
if ranks.len() != ndim {
return Err(LinalgError::ShapeError(format!(
"ranks length {} must equal tensor ndim {}",
ranks.len(),
ndim
)));
}
let mut factors: Vec<Array2<F>> = Vec::with_capacity(ndim);
for (mode, &rank) in ranks.iter().enumerate().take(ndim) {
let x_n = unfold(tensor, mode)?;
let (u, _s, _vt) = crate::decomposition::svd(&x_n.view(), true, None)?;
let r = rank.min(u.shape()[1]);
let u_trunc = u.slice(scirs2_core::ndarray::s![.., ..r]).to_owned();
factors.push(u_trunc);
}
let mut core_tensor = tensor.clone();
for (mode, factor) in factors.iter().enumerate().take(ndim) {
let u_t = factor.t().to_owned();
core_tensor = n_mode_product(&core_tensor, &u_t.view(), mode)?;
}
let reconstructed = reconstruct_tucker(&core_tensor, &factors)?;
let mut err_sq = F::zero();
for i in 0..tensor.numel() {
let diff = tensor.data[i] - reconstructed.data[i];
err_sq += diff * diff;
}
Ok(TuckerResult {
core: core_tensor,
factors,
reconstruction_error: err_sq.sqrt(),
})
}
fn reconstruct_tucker<F: TensorFloat>(
core: &Tensor<F>,
factors: &[Array2<F>],
) -> LinalgResult<Tensor<F>> {
let mut result = core.clone();
for (mode, factor) in factors.iter().enumerate() {
result = n_mode_product(&result, &factor.view(), mode)?;
}
Ok(result)
}
#[cfg(test)]
mod tests {
use super::*;
use approx::assert_abs_diff_eq;
use scirs2_core::ndarray::array;
#[test]
fn test_tensor_create() {
let t = Tensor::new(vec![2, 3], vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0]);
assert!(t.is_ok());
let t = t.expect("create ok");
assert_eq!(t.ndim(), 2);
assert_eq!(t.numel(), 6);
}
#[test]
fn test_tensor_get_set() {
let mut t = Tensor::new(vec![2, 3], vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0]).expect("ok");
assert_abs_diff_eq!(t.get(&[0, 0]).expect("ok"), 1.0, epsilon = 1e-10);
assert_abs_diff_eq!(t.get(&[1, 2]).expect("ok"), 6.0, epsilon = 1e-10);
t.set(&[0, 1], 99.0).expect("ok");
assert_abs_diff_eq!(t.get(&[0, 1]).expect("ok"), 99.0, epsilon = 1e-10);
}
#[test]
fn test_tensor_shape_mismatch() {
let t = Tensor::new(vec![2, 3], vec![1.0, 2.0, 3.0]);
assert!(t.is_err());
}
#[test]
fn test_unfold_2d() {
let t = Tensor::new(vec![2, 3], vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0]).expect("ok");
let m0 = unfold(&t, 0).expect("ok");
assert_eq!(m0.shape(), &[2, 3]);
assert_abs_diff_eq!(m0[[0, 0]], 1.0, epsilon = 1e-10);
assert_abs_diff_eq!(m0[[1, 2]], 6.0, epsilon = 1e-10);
}
#[test]
fn test_unfold_3d() {
let data: Vec<f64> = (1..=12).map(|x| x as f64).collect();
let t = Tensor::new(vec![2, 3, 2], data).expect("ok");
let m0 = unfold(&t, 0).expect("ok");
assert_eq!(m0.shape(), &[2, 6]);
let m1 = unfold(&t, 1).expect("ok");
assert_eq!(m1.shape(), &[3, 4]);
let m2 = unfold(&t, 2).expect("ok");
assert_eq!(m2.shape(), &[2, 6]);
}
#[test]
fn test_fold_unfold_roundtrip() {
let data: Vec<f64> = (0..24).map(|x| x as f64).collect();
let t = Tensor::new(vec![2, 3, 4], data.clone()).expect("ok");
for mode in 0..3 {
let mat = unfold(&t, mode).expect("ok");
let t2 = fold(&mat.view(), mode, &[2, 3, 4]).expect("ok");
for i in 0..24 {
assert_abs_diff_eq!(t.data[i], t2.data[i], epsilon = 1e-10);
}
}
}
#[test]
fn test_n_mode_product_2d() {
let t = Tensor::new(vec![2, 3], vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0]).expect("ok");
let m = array![[1.0, 0.0], [0.0, 1.0]]; let res = n_mode_product(&t, &m.view(), 0).expect("ok");
assert_eq!(res.shape, vec![2, 3]);
for i in 0..6 {
assert_abs_diff_eq!(res.data[i], t.data[i], epsilon = 1e-10);
}
}
#[test]
fn test_n_mode_product_reduces() {
let t = Tensor::new(vec![2, 3], vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0]).expect("ok");
let m = array![[1.0, 1.0]]; let res = n_mode_product(&t, &m.view(), 0).expect("ok");
assert_eq!(res.shape, vec![1, 3]);
assert_abs_diff_eq!(res.data[0], 5.0, epsilon = 1e-10); assert_abs_diff_eq!(res.data[1], 7.0, epsilon = 1e-10); assert_abs_diff_eq!(res.data[2], 9.0, epsilon = 1e-10); }
#[test]
fn test_khatri_rao_basic() {
let a = array![[1.0, 2.0], [3.0, 4.0]];
let b = array![[5.0, 6.0], [7.0, 8.0]];
let kr = khatri_rao(&a.view(), &b.view()).expect("ok");
assert_eq!(kr.shape(), &[4, 2]);
assert_abs_diff_eq!(kr[[0, 0]], 5.0, epsilon = 1e-10);
assert_abs_diff_eq!(kr[[1, 0]], 7.0, epsilon = 1e-10);
assert_abs_diff_eq!(kr[[2, 0]], 15.0, epsilon = 1e-10);
assert_abs_diff_eq!(kr[[3, 0]], 21.0, epsilon = 1e-10);
}
#[test]
fn test_khatri_rao_mismatch() {
let a = array![[1.0, 2.0]]; let b = array![[1.0, 2.0, 3.0]]; assert!(khatri_rao(&a.view(), &b.view()).is_err());
}
#[test]
fn test_tensor_norm_frobenius() {
let t = Tensor::new(vec![2, 2], vec![1.0, 2.0, 3.0, 4.0]).expect("ok");
let nrm = tensor_norm_frobenius(&t);
assert_abs_diff_eq!(nrm, (30.0_f64).sqrt(), epsilon = 1e-10);
}
#[test]
fn test_tensor_norm_max() {
let t = Tensor::new(vec![2, 2], vec![1.0, -5.0, 3.0, 4.0]).expect("ok");
assert_abs_diff_eq!(tensor_norm_max(&t), 5.0, epsilon = 1e-10);
}
#[test]
fn test_tensor_norm_l1() {
let t = Tensor::new(vec![2, 2], vec![1.0, -2.0, 3.0, -4.0]).expect("ok");
assert_abs_diff_eq!(tensor_norm_l1(&t), 10.0, epsilon = 1e-10);
}
#[test]
fn test_cp_als_rank1() {
let data = vec![3.0, 4.0, 5.0, 6.0, 8.0, 10.0];
let t = Tensor::new(vec![2, 3], data).expect("ok");
let res = cp_als(&t, 1, Some(100), Some(1e-6)).expect("ok");
assert!(res.reconstruction_error < 1.0);
}
#[test]
fn test_cp_als_3d() {
let data = vec![1.0, 1.0, 1.0, 1.0, 2.0, 2.0, 2.0, 2.0];
let t = Tensor::new(vec![2, 2, 2], data).expect("ok");
let res = cp_als(&t, 1, None, None).expect("ok");
assert!(res.reconstruction_error < 0.5);
}
#[test]
fn test_tucker_hosvd_basic() {
let data: Vec<f64> = (0..24).map(|x| x as f64).collect();
let t = Tensor::new(vec![2, 3, 4], data).expect("ok");
let res = tucker_hosvd(&t, &[2, 2, 2]).expect("ok");
assert_eq!(res.core.shape, vec![2, 2, 2]);
assert_eq!(res.factors.len(), 3);
}
#[test]
fn test_tucker_hosvd_full_rank() {
let data: Vec<f64> = (0..12).map(|x| x as f64).collect();
let t = Tensor::new(vec![2, 3, 2], data).expect("ok");
let res = tucker_hosvd(&t, &[2, 3, 2]).expect("ok");
assert!(res.reconstruction_error < 1e-6);
}
#[test]
fn test_tucker_rank_mismatch() {
let t = Tensor::new(vec![2, 3], vec![1.0; 6]).expect("ok");
assert!(tucker_hosvd(&t, &[2]).is_err()); }
}