#[cfg(test)]
use crate::loss::{l1_loss, mse_loss, smooth_l1_loss, ReductionType};
#[cfg(test)]
use torsh_core::Result as TorshResult;
#[cfg(test)]
use torsh_tensor::{
creation::{randn, zeros},
Tensor,
};
pub const DEFAULT_TOLERANCE: f32 = 1e-6;
pub const RELAXED_TOLERANCE: f32 = 1e-4;
#[cfg(test)]
pub fn assert_tensors_close(a: &Tensor, b: &Tensor, tolerance: f32) -> TorshResult<()> {
let a_data = a.data()?;
let b_data = b.data()?;
assert_eq!(a_data.len(), b_data.len(), "Tensors have different sizes");
for (i, (&a_val, &b_val)) in a_data.iter().zip(b_data.iter()).enumerate() {
assert!(
(a_val - b_val).abs() <= tolerance,
"Mismatch at index {}: {} vs {}",
i,
a_val,
b_val
);
}
Ok(())
}
#[cfg(test)]
pub mod numerical_properties {
use super::*;
#[test]
fn test_mse_loss_non_negative() -> TorshResult<()> {
let input = randn(&[10, 5])?;
let target = randn(&[10, 5])?;
let loss = mse_loss(&input, &target, ReductionType::Mean)?;
let loss_data = loss.data()?;
assert!(
loss_data[0] >= 0.0,
"MSE loss should be non-negative, got {}",
loss_data[0]
);
Ok(())
}
#[test]
fn test_mse_loss_zero_when_equal() -> TorshResult<()> {
let input = randn(&[5, 3])?;
let target = input.clone();
let loss = mse_loss(&input, &target, ReductionType::Mean)?;
let loss_data = loss.data()?;
assert!((loss_data[0] - 0.0).abs() <= DEFAULT_TOLERANCE);
Ok(())
}
#[test]
fn test_l1_loss_non_negative() -> TorshResult<()> {
let input = randn(&[8, 4])?;
let target = randn(&[8, 4])?;
let loss = l1_loss(&input, &target, ReductionType::Mean)?;
let loss_data = loss.data()?;
assert!(
loss_data[0] >= 0.0,
"L1 loss should be non-negative, got {}",
loss_data[0]
);
Ok(())
}
#[test]
fn test_l1_loss_zero_when_equal() -> TorshResult<()> {
let input = randn(&[6, 7])?;
let target = input.clone();
let loss = l1_loss(&input, &target, ReductionType::Mean)?;
let loss_data = loss.data()?;
assert!((loss_data[0] - 0.0).abs() <= DEFAULT_TOLERANCE);
Ok(())
}
#[test]
fn test_smooth_l1_loss_properties() -> TorshResult<()> {
let input = randn(&[10, 3])?;
let target = randn(&[10, 3])?;
let beta = 1.0;
let loss = smooth_l1_loss(&input, &target, ReductionType::Mean, beta)?;
let loss_data = loss.data()?;
assert!(loss_data[0] >= 0.0, "Smooth L1 loss should be non-negative");
let same_input = randn(&[5, 5])?;
let zero_loss = smooth_l1_loss(&same_input, &same_input, ReductionType::Mean, beta)?;
let zero_data = zero_loss.data()?;
assert!((zero_data[0] - 0.0).abs() <= DEFAULT_TOLERANCE);
Ok(())
}
#[test]
fn test_reduction_consistency() -> TorshResult<()> {
let input = randn(&[4, 6])?;
let target = randn(&[4, 6])?;
let none_loss = mse_loss(&input, &target, ReductionType::None)?;
let sum_loss = mse_loss(&input, &target, ReductionType::Sum)?;
let mean_loss = mse_loss(&input, &target, ReductionType::Mean)?;
let manual_sum = none_loss.sum()?;
assert_tensors_close(&sum_loss, &manual_sum, DEFAULT_TOLERANCE)?;
let manual_mean = manual_sum.div_scalar(none_loss.numel() as f32)?;
assert_tensors_close(&mean_loss, &manual_mean, DEFAULT_TOLERANCE)?;
Ok(())
}
}
#[cfg(test)]
pub mod property_tests {
use super::*;
#[test]
fn test_loss_functions_with_various_shapes() -> TorshResult<()> {
let shapes = vec![
vec![1],
vec![5],
vec![3, 4],
vec![2, 3, 4],
vec![1, 1, 1, 1],
];
for shape in shapes {
let input = randn(&shape)?;
let target = randn(&shape)?;
let _mse = mse_loss(&input, &target, ReductionType::Mean)?;
let _l1 = l1_loss(&input, &target, ReductionType::Mean)?;
let _smooth_l1 = smooth_l1_loss(&input, &target, ReductionType::Mean, 1.0)?;
}
Ok(())
}
#[test]
fn test_loss_function_relationships() -> TorshResult<()> {
let input = zeros(&[10, 5])?;
let small_perturbation = input.add_scalar(0.01)?;
let l2_loss = mse_loss(&input, &small_perturbation, ReductionType::Mean)?;
let smooth_l1_loss_val =
smooth_l1_loss(&input, &small_perturbation, ReductionType::Mean, 1.0)?;
let expected_smooth = l2_loss.div_scalar(2.0)?;
assert_tensors_close(&smooth_l1_loss_val, &expected_smooth, RELAXED_TOLERANCE)?;
Ok(())
}
#[test]
fn test_edge_cases() -> TorshResult<()> {
let tiny_input = randn(&[1])?;
let tiny_target = randn(&[1])?;
let _loss = mse_loss(&tiny_input, &tiny_target, ReductionType::Mean)?;
let large_input = randn(&[100, 100])?;
let large_target = randn(&[100, 100])?;
let _large_loss = l1_loss(&large_input, &large_target, ReductionType::Sum)?;
Ok(())
}
}
#[cfg(test)]
pub mod performance_tests {
use super::*;
use std::time::Instant;
#[test]
fn test_loss_function_performance() -> TorshResult<()> {
let sizes = vec![
(100, 10), (1000, 100), (10000, 1000), ];
for (rows, cols) in sizes {
let input = randn(&[rows, cols])?;
let target = randn(&[rows, cols])?;
let start = Instant::now();
let _loss = mse_loss(&input, &target, ReductionType::Mean)?;
let duration = start.elapsed();
assert!(
duration.as_millis() < 1000,
"MSE loss took too long for size {}x{}: {:?}",
rows,
cols,
duration
);
}
Ok(())
}
}
#[cfg(test)]
mod validation_tests {
use super::*;
use crate::utils::{validate_elementwise_shapes, validate_positive, validate_range};
#[test]
fn test_shape_validation() -> TorshResult<()> {
let tensor_a = zeros(&[3, 4])?;
let tensor_b = zeros(&[3, 4])?;
let tensor_c = zeros(&[3, 5])?;
validate_elementwise_shapes(&tensor_a, &tensor_b)?;
let result = validate_elementwise_shapes(&tensor_a, &tensor_c);
assert!(result.is_err());
Ok(())
}
#[test]
fn test_range_validation() {
assert!(validate_range(5.0, 0.0, 10.0, "value", "test").is_ok());
assert!(validate_range(-1.0, 0.0, 10.0, "value", "test").is_err());
assert!(validate_range(15.0, 0.0, 10.0, "value", "test").is_err());
}
#[test]
fn test_positive_validation() {
assert!(validate_positive(1.0, "value", "test").is_ok());
assert!(validate_positive(0.001, "value", "test").is_ok());
assert!(validate_positive(0.0, "value", "test").is_err());
assert!(validate_positive(-1.0, "value", "test").is_err());
}
}
#[cfg(test)]
pub mod pytorch_reference_tests {
use super::*;
use crate::{activations::*, reduction::*};
#[test]
fn test_relu_pytorch_reference() -> TorshResult<()> {
let input = Tensor::from_data(
vec![-2.0, -1.0, 0.0, 1.0, 2.0],
vec![5],
torsh_core::DeviceType::Cpu,
)?;
let expected = vec![0.0, 0.0, 0.0, 1.0, 2.0];
let output = relu(&input, false)?;
let output_data = output.data()?;
for (i, (&actual, &expected_val)) in output_data.iter().zip(expected.iter()).enumerate() {
assert!(
(actual - expected_val as f32).abs() <= DEFAULT_TOLERANCE,
"ReLU mismatch at index {}: got {}, expected {}",
i,
actual,
expected_val
);
}
Ok(())
}
#[test]
fn test_sigmoid_pytorch_reference() -> TorshResult<()> {
let input = Tensor::from_data(
vec![-2.0, -1.0, 0.0, 1.0, 2.0],
vec![5],
torsh_core::DeviceType::Cpu,
)?;
let expected = vec![0.1192029, 0.2689414, 0.5, 0.7310586, 0.8807971];
let output = sigmoid(&input)?;
let output_data = output.data()?;
for (i, (&actual, &expected_val)) in output_data.iter().zip(expected.iter()).enumerate() {
assert!(
(actual - expected_val as f32).abs() <= RELAXED_TOLERANCE,
"Sigmoid mismatch at index {}: got {}, expected {}",
i,
actual,
expected_val
);
}
Ok(())
}
#[test]
fn test_tanh_pytorch_reference() -> TorshResult<()> {
let input = Tensor::from_data(
vec![-2.0, -1.0, 0.0, 1.0, 2.0],
vec![5],
torsh_core::DeviceType::Cpu,
)?;
let expected = vec![-0.9640276, -0.7615942, 0.0, 0.7615942, 0.9640276];
let output = crate::activations::tanh(&input)?;
let output_data = output.data()?;
for (i, (&actual, &expected_val)) in output_data.iter().zip(expected.iter()).enumerate() {
assert!(
(actual - expected_val as f32).abs() <= RELAXED_TOLERANCE,
"Tanh mismatch at index {}: got {}, expected {}",
i,
actual,
expected_val
);
}
Ok(())
}
#[test]
fn test_softmax_pytorch_reference() -> TorshResult<()> {
let input = Tensor::from_data(
vec![1.0, 2.0, 3.0, 4.0],
vec![4],
torsh_core::DeviceType::Cpu,
)?;
let expected = vec![0.0320586, 0.0871443, 0.2368828, 0.6439142];
let output = softmax(&input, 0, None)?;
let output_data = output.data()?;
for (i, (&actual, &expected_val)) in output_data.iter().zip(expected.iter()).enumerate() {
assert!(
(actual - expected_val as f32).abs() <= RELAXED_TOLERANCE,
"Softmax mismatch at index {}: got {}, expected {}",
i,
actual,
expected_val
);
}
Ok(())
}
#[test]
fn test_gelu_pytorch_reference() -> TorshResult<()> {
let input = Tensor::from_data(
vec![-2.0, -1.0, 0.0, 1.0, 2.0],
vec![5],
torsh_core::DeviceType::Cpu,
)?;
let expected = vec![-0.0454023, -0.1587989, 0.0, 0.8411995, 1.9545977];
let output = gelu(&input)?;
let output_data = output.data()?;
for (i, (&actual, &expected_val)) in output_data.iter().zip(expected.iter()).enumerate() {
assert!(
(actual - expected_val as f32).abs() <= RELAXED_TOLERANCE,
"GELU mismatch at index {}: got {}, expected {}",
i,
actual,
expected_val
);
}
Ok(())
}
#[test]
fn test_sum_pytorch_reference() -> TorshResult<()> {
let input = Tensor::from_data(
vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0],
vec![2, 3],
torsh_core::DeviceType::Cpu,
)?;
let expected_total = 21.0;
let output = sum(&input)?;
let output_data = output.data()?;
assert!(
(output_data[0] - expected_total).abs() <= DEFAULT_TOLERANCE,
"Sum mismatch: got {}, expected {}",
output_data[0],
expected_total
);
Ok(())
}
#[test]
fn test_mean_pytorch_reference() -> TorshResult<()> {
let input = Tensor::from_data(
vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0],
vec![2, 3],
torsh_core::DeviceType::Cpu,
)?;
let expected_mean = 3.5;
let output = mean(&input)?;
let output_data = output.data()?;
assert!(
(output_data[0] - expected_mean).abs() <= DEFAULT_TOLERANCE,
"Mean mismatch: got {}, expected {}",
output_data[0],
expected_mean
);
Ok(())
}
#[test]
fn test_sum_dim_pytorch_reference() -> TorshResult<()> {
let input = Tensor::from_data(
vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0],
vec![2, 3],
torsh_core::DeviceType::Cpu,
)?;
let expected_total = 21.0;
let output = sum(&input)?; let output_data = output.data()?;
assert!(
(output_data[0] - expected_total).abs() <= DEFAULT_TOLERANCE,
"Sum total mismatch: got {}, expected {}",
output_data[0],
expected_total
);
Ok(())
}
#[test]
fn test_matmul_pytorch_reference() -> TorshResult<()> {
let a = Tensor::from_data(
vec![1.0, 2.0, 3.0, 4.0],
vec![2, 2],
torsh_core::DeviceType::Cpu,
)?;
let b = Tensor::from_data(
vec![5.0, 6.0, 7.0, 8.0],
vec![2, 2],
torsh_core::DeviceType::Cpu,
)?;
let expected = vec![19.0, 22.0, 43.0, 50.0];
let output = a.matmul(&b)?;
let output_data = output.data()?;
for (i, (&actual, &expected_val)) in output_data.iter().zip(expected.iter()).enumerate() {
assert!(
(actual - expected_val as f32).abs() <= DEFAULT_TOLERANCE,
"MatMul mismatch at index {}: got {}, expected {}",
i,
actual,
expected_val
);
}
Ok(())
}
#[test]
fn test_mse_loss_pytorch_reference() -> TorshResult<()> {
let input = Tensor::from_data(
vec![1.0, 2.0, 3.0, 4.0],
vec![4],
torsh_core::DeviceType::Cpu,
)?;
let target = Tensor::from_data(
vec![2.0, 3.0, 4.0, 5.0],
vec![4],
torsh_core::DeviceType::Cpu,
)?;
let expected = 1.0;
let output = mse_loss(&input, &target, ReductionType::Mean)?;
let output_data = output.data()?;
assert!(
(output_data[0] - expected).abs() <= DEFAULT_TOLERANCE,
"MSE loss mismatch: got {}, expected {}",
output_data[0],
expected
);
Ok(())
}
#[test]
fn test_edge_cases_pytorch_reference() -> TorshResult<()> {
let extreme_input =
Tensor::from_data(vec![-100.0, 100.0], vec![2], torsh_core::DeviceType::Cpu)?;
let output = sigmoid(&extreme_input)?;
let output_data = output.data()?;
assert!(
output_data[0] < 1e-10,
"Sigmoid of -100 should be ~0, got {}",
output_data[0]
);
assert!(
(output_data[1] - 1.0f32).abs() < 1e-10,
"Sigmoid of 100 should be ~1, got {}",
output_data[1]
);
let output = crate::activations::tanh(&extreme_input)?;
let output_data = output.data()?;
assert!(
(output_data[0] + 1.0f32).abs() < 1e-10,
"Tanh of -100 should be ~-1, got {}",
output_data[0]
);
assert!(
(output_data[1] - 1.0f32).abs() < 1e-10,
"Tanh of 100 should be ~1, got {}",
output_data[1]
);
Ok(())
}
}