use std::f32;
use torsh_core::Result as TorshResult;
use torsh_tensor::stats::StatMode;
use torsh_tensor::Tensor;
pub struct Tolerance {
pub abs: f32,
pub rel: f32,
}
impl Tolerance {
pub const DEFAULT: Tolerance = Tolerance {
abs: 1e-6,
rel: 1e-6,
};
pub const STRICT: Tolerance = Tolerance {
abs: 1e-8,
rel: 1e-8,
};
pub const RELAXED: Tolerance = Tolerance {
abs: 1e-4,
rel: 1e-4,
};
pub const APPROXIMATE: Tolerance = Tolerance {
abs: 1e-3,
rel: 1e-3,
};
}
pub fn assert_tensors_close(
actual: &Tensor,
expected: &Tensor,
tolerance: &Tolerance,
message: &str,
) -> TorshResult<()> {
let actual_data = actual.data()?;
let expected_data = expected.data()?;
if actual_data.len() != expected_data.len() {
panic!(
"{}: Tensor sizes don't match: {} vs {}",
message,
actual_data.len(),
expected_data.len()
);
}
let mut max_abs_error = 0.0f32;
let mut max_rel_error = 0.0f32;
let mut error_count = 0;
for (i, (&a, &e)) in actual_data.iter().zip(expected_data.iter()).enumerate() {
let abs_error = (a - e).abs();
let max_val = a.abs().max(e.abs());
let rel_error = if max_val > f32::EPSILON {
abs_error / max_val
} else {
0.0
};
max_abs_error = max_abs_error.max(abs_error);
max_rel_error = max_rel_error.max(rel_error);
let threshold = tolerance.abs + tolerance.rel * max_val;
if abs_error > threshold {
error_count += 1;
if error_count <= 5 {
println!("ERROR at index {}: actual={}, expected={}, abs_err={:.2e}, rel_err={:.2e}, threshold={:.2e}",
i, a, e, abs_error, rel_error, threshold);
}
}
}
if error_count > 0 {
panic!(
"{}: {} of {} elements exceeded tolerance. Max errors: abs={:.2e}, rel={:.2e}",
message,
error_count,
actual_data.len(),
max_abs_error,
max_rel_error
);
}
Ok(())
}
pub mod activation_correctness {
use super::*;
use crate::activations::*;
use torsh_tensor::creation::linspace;
#[test]
fn test_relu_analytical() -> TorshResult<()> {
let x = linspace(-5.0, 5.0, 101)?;
let result = relu(&x, false)?;
let x_data = x.data()?;
let expected_data: Vec<f32> = x_data.iter().map(|&v: &f32| v.max(0.0f32)).collect();
let expected = Tensor::from_data(expected_data, x.shape().dims().to_vec(), x.device())?;
assert_tensors_close(
&result,
&expected,
&Tolerance::STRICT,
"ReLU analytical test",
)?;
Ok(())
}
#[test]
fn test_sigmoid_analytical() -> TorshResult<()> {
let x = linspace(-10.0, 10.0, 201)?;
let result = sigmoid(&x)?;
let x_data = x.data()?;
let expected_data: Vec<f32> = x_data
.iter()
.map(|&v: &f32| 1.0f32 / (1.0f32 + (-v).exp()))
.collect();
let expected = Tensor::from_data(expected_data, x.shape().dims().to_vec(), x.device())?;
assert_tensors_close(
&result,
&expected,
&Tolerance::DEFAULT,
"Sigmoid analytical test",
)?;
Ok(())
}
#[test]
fn test_tanh_analytical() -> TorshResult<()> {
let x = linspace(-5.0, 5.0, 101)?;
let result = tanh(&x)?;
let x_data = x.data()?;
let expected_data: Vec<f32> = x_data
.iter()
.map(|&v: &f32| {
let exp2x = (2.0f32 * v).exp();
(exp2x - 1.0f32) / (exp2x + 1.0f32)
})
.collect();
let expected = Tensor::from_data(expected_data, x.shape().dims().to_vec(), x.device())?;
assert_tensors_close(
&result,
&expected,
&Tolerance::DEFAULT,
"Tanh analytical test",
)?;
Ok(())
}
#[test]
fn test_softmax_properties() -> TorshResult<()> {
let x = Tensor::from_data(
vec![1.0, 2.0, 3.0, 4.0],
vec![4],
torsh_core::DeviceType::Cpu,
)?;
let result = softmax(&x, 0, None)?;
let sum = result.data()?.iter().sum::<f32>();
assert!(
(sum - 1.0).abs() < 1e-6,
"Softmax sum should equal 1, got {}",
sum
);
let x_data = x.data()?;
let max_val = x_data.iter().fold(f32::NEG_INFINITY, |a, &b| a.max(b));
let exp_sum: f32 = x_data.iter().map(|&v| (v - max_val).exp()).sum();
let expected_data: Vec<f32> = x_data
.iter()
.map(|&v| (v - max_val).exp() / exp_sum)
.collect();
let expected = Tensor::from_data(expected_data, x.shape().dims().to_vec(), x.device())?;
assert_tensors_close(
&result,
&expected,
&Tolerance::DEFAULT,
"Softmax analytical test",
)?;
Ok(())
}
#[test]
fn test_gelu_approximation() -> TorshResult<()> {
let x = linspace(-3.0, 3.0, 61)?;
let result = gelu(&x)?;
let x_data = x.data()?;
let sqrt_2_pi = (2.0 / std::f32::consts::PI).sqrt();
let expected_data: Vec<f32> = x_data
.iter()
.map(|&v| {
let inner = sqrt_2_pi * (v + 0.044715 * v * v * v);
0.5 * v * (1.0 + inner.tanh())
})
.collect();
let expected = Tensor::from_data(expected_data, x.shape().dims().to_vec(), x.device())?;
assert_tensors_close(
&result,
&expected,
&Tolerance::RELAXED,
"GELU approximation test",
)?;
Ok(())
}
}
pub mod math_correctness {
use super::*;
use crate::linalg::*;
use torsh_tensor::creation::{eye, randn};
#[test]
fn test_matmul_correctness() -> TorshResult<()> {
let a = randn(&[3, 4])?;
let b = randn(&[4, 5])?;
let result = a.matmul(&b)?;
let a_data = a.data()?;
let b_data = b.data()?;
let mut expected_data = vec![0.0f32; 3 * 5];
for i in 0..3 {
for j in 0..5 {
let mut sum = 0.0f32;
for k in 0..4 {
sum += a_data[i * 4 + k] * b_data[k * 5 + j];
}
expected_data[i * 5 + j] = sum;
}
}
let expected = Tensor::from_data(expected_data, vec![3, 5], a.device())?;
assert_tensors_close(
&result,
&expected,
&Tolerance::DEFAULT,
"Matrix multiplication test",
)?;
Ok(())
}
#[test]
fn test_matrix_inverse_identity() -> TorshResult<()> {
let a_data = vec![2.0, 1.0, 0.0, 1.0, 3.0, 1.0, 0.0, 1.0, 2.0];
let a = Tensor::from_data(a_data.clone(), vec![3, 3], torsh_core::DeviceType::Cpu)?;
let a_inv = inv(&a)?;
let identity_approx = a.matmul(&a_inv)?;
let identity_expected = eye(3)?;
assert_tensors_close(
&identity_approx,
&identity_expected,
&Tolerance::RELAXED,
"Matrix inverse identity test",
)?;
Ok(())
}
#[test]
fn test_eigenvalue_decomposition() -> TorshResult<()> {
let a = eye(3)?;
let (eigenvals, eigenvecs) = eig(&a)?;
let eigenvecs_data = eigenvecs.data()?;
let eigenvals_data = eigenvals.data()?;
let a_data = a.data()?;
for i in 0..3 {
let mut v = vec![0.0f32; 3];
for j in 0..3 {
v[j] = eigenvecs_data[j * 3 + i];
}
let mut av = vec![0.0f32; 3];
for j in 0..3 {
for k in 0..3 {
av[j] += a_data[j * 3 + k] * v[k];
}
}
let lambda = eigenvals_data[i];
let lambda_v: Vec<f32> = v.iter().map(|&x| lambda * x).collect();
let av_tensor = Tensor::from_data(av, vec![3], a.device())?;
let lambda_v_tensor = Tensor::from_data(lambda_v, vec![3], a.device())?;
assert_tensors_close(
&av_tensor,
&lambda_v_tensor,
&Tolerance::RELAXED,
&format!("Eigenvalue equation for eigenvalue {}", i),
)?;
}
Ok(())
}
}
pub mod reduction_correctness {
use super::*;
use torsh_tensor::creation::arange;
#[test]
fn test_sum_analytical() -> TorshResult<()> {
let n = 100;
let x = arange(1.0, (n + 1) as f32, 1.0)?;
let result = x.sum()?;
let expected_sum = (n * (n + 1)) as f32 / 2.0;
let result_data = result.data()?;
assert!(
(result_data[0] - expected_sum as f32).abs() < 1e-6,
"Sum test: expected {}, got {}",
expected_sum,
result_data[0]
);
Ok(())
}
#[test]
fn test_mean_analytical() -> TorshResult<()> {
let x = arange(1.0, 101.0, 1.0)?;
let result = x.mean(None, false)?;
let expected_mean = 50.5;
let result_data = result.data()?;
assert!(
(result_data[0] - expected_mean as f32).abs() < 1e-6,
"Mean test: expected {}, got {}",
expected_mean,
result_data[0]
);
Ok(())
}
#[test]
fn test_variance_analytical() -> TorshResult<()> {
let x = arange(0.0, 12.0, 1.0)?; let result = x.var(None, false, StatMode::Population)?;
let n = 12.0;
let expected_var = (n * n - 1.0) / 12.0;
let result_data = result.data()?;
assert!(
(result_data[0] - expected_var as f32).abs() < 1e-5,
"Variance test: expected {}, got {}",
expected_var,
result_data[0]
);
Ok(())
}
}
pub mod cross_validation {
use super::*;
use crate::sparse::*;
#[test]
fn test_sparse_dense_equivalence() -> TorshResult<()> {
let values = Tensor::from_data(vec![5.0], vec![1], torsh_core::DeviceType::Cpu)?;
let indices = Tensor::from_data(
vec![0.0, 1.0], vec![2, 1], torsh_core::DeviceType::Cpu,
)?;
let sparse = sparse_coo_tensor(&indices, &values, &[2, 2])?;
let dense = sparse.to_dense()?;
let sparse_again = SparseTensor::from_dense(&dense)?;
let dense_original = sparse.to_dense()?;
let dense_reconstructed = sparse_again.to_dense()?;
assert_tensors_close(
&dense_original,
&dense_reconstructed,
&Tolerance::STRICT,
"Sparse-dense-sparse roundtrip test",
)?;
Ok(())
}
#[test]
fn test_fusion_equivalence() -> TorshResult<()> {
use crate::fusion::*;
let x = Tensor::from_data(
vec![-2.0, -1.0, 0.0, 1.0, 2.0],
vec![5],
torsh_core::DeviceType::Cpu,
)?;
let y = Tensor::from_data(
vec![0.5, 1.0, 1.5, 2.0, 2.5],
vec![5],
torsh_core::DeviceType::Cpu,
)?;
let fused_result = fused_relu_add(&x, &y)?;
let temp = x.add_op(&y)?;
let separate_result = crate::activations::relu(&temp, false)?;
assert_tensors_close(
&fused_result,
&separate_result,
&Tolerance::STRICT,
"Fused vs separate ReLU+Add test",
)?;
Ok(())
}
}
#[cfg(test)]
mod integration_tests {
use super::*;
#[test]
fn test_tolerance_levels() {
assert!(Tolerance::STRICT.abs < Tolerance::DEFAULT.abs);
assert!(Tolerance::DEFAULT.abs < Tolerance::RELAXED.abs);
assert!(Tolerance::RELAXED.abs < Tolerance::APPROXIMATE.abs);
}
#[test]
fn test_assert_tensors_close_basic() -> TorshResult<()> {
let a = Tensor::from_data(vec![1.0, 2.0, 3.0], vec![3], torsh_core::DeviceType::Cpu)?;
let b = Tensor::from_data(
vec![1.000001, 2.000001, 3.000001],
vec![3],
torsh_core::DeviceType::Cpu,
)?;
assert_tensors_close(&a, &b, &Tolerance::DEFAULT, "Basic close test")?;
Ok(())
}
}