#![cfg(feature = "math")]
use approx::{assert_abs_diff_eq, assert_relative_eq};
use ndarray::array;
use rustyml::math::*;
#[test]
fn test_entropy_balanced_binary() {
let labels = array![0.0_f64, 1.0, 1.0, 0.0];
assert_abs_diff_eq!(entropy(&labels), 1.0, epsilon = 1e-9);
}
#[test]
fn test_entropy_all_same_class() {
let labels = array![0.0_f64, 0.0, 0.0];
assert_abs_diff_eq!(entropy(&labels), 0.0, epsilon = 1e-9);
}
#[test]
fn test_entropy_empty() {
let labels: ndarray::Array1<f64> = array![];
assert_abs_diff_eq!(entropy(&labels), 0.0, epsilon = 1e-10);
}
#[test]
fn test_entropy_four_uniform_classes() {
let labels = array![0.0_f64, 1.0, 2.0, 3.0];
assert_abs_diff_eq!(entropy(&labels), 2.0, epsilon = 1e-9);
}
#[test]
fn test_entropy_single_element() {
let labels = array![42.0_f64];
assert_abs_diff_eq!(entropy(&labels), 0.0, epsilon = 1e-10);
}
#[test]
fn test_entropy_non_negative() {
let cases: &[ndarray::Array1<f64>] = &[
array![0.0, 0.0, 1.0],
array![0.0, 1.0, 2.0],
array![7.0, 7.0, 7.0],
];
for labels in cases {
assert!(entropy(labels) >= 0.0, "entropy must be non-negative");
}
}
#[test]
fn test_entropy_three_classes_unequal() {
let labels = array![0.0_f64, 0.0, 1.0, 2.0];
assert_abs_diff_eq!(entropy(&labels), 1.5, epsilon = 1e-9);
}
#[test]
fn test_gini_balanced_binary() {
let labels = array![0.0_f64, 0.0, 1.0, 1.0];
assert_abs_diff_eq!(gini(&labels), 0.5, epsilon = 1e-9);
}
#[test]
fn test_gini_all_same_class() {
let labels = array![3.0_f64, 3.0, 3.0];
assert_abs_diff_eq!(gini(&labels), 0.0, epsilon = 1e-9);
}
#[test]
fn test_gini_empty() {
let labels: ndarray::Array1<f64> = array![];
assert_abs_diff_eq!(gini(&labels), 0.0, epsilon = 1e-10);
}
#[test]
fn test_gini_four_uniform_classes() {
let labels = array![0.0_f64, 1.0, 2.0, 3.0];
assert_abs_diff_eq!(gini(&labels), 0.75, epsilon = 1e-9);
}
#[test]
fn test_gini_single_element() {
let labels = array![99.0_f64];
assert_abs_diff_eq!(gini(&labels), 0.0, epsilon = 1e-10);
}
#[test]
fn test_gini_value_range() {
let cases: &[ndarray::Array1<f64>] = &[
array![0.0, 1.0],
array![0.0, 1.0, 2.0],
array![0.0, 0.0, 1.0, 2.0, 3.0],
];
for labels in cases {
let g = gini(labels);
assert!((0.0..=1.0).contains(&g), "gini={g} not in [0,1]");
}
}
#[test]
fn test_gini_three_classes_unequal() {
let labels = array![0.0_f64, 0.0, 1.0, 2.0];
assert_abs_diff_eq!(gini(&labels), 0.625, epsilon = 1e-9);
}
#[test]
fn test_squared_euclidean_basic() {
let v1 = array![1.0_f64, 2.0];
let v2 = array![4.0_f64, 6.0];
assert_abs_diff_eq!(
squared_euclidean_distance_row(&v1, &v2),
25.0,
epsilon = 1e-10
);
}
#[test]
fn test_squared_euclidean_three_dim() {
let v1 = array![1.0_f64, 2.0, 3.0];
let v2 = array![4.0_f64, 5.0, 6.0];
assert_abs_diff_eq!(
squared_euclidean_distance_row(&v1, &v2),
27.0,
epsilon = 1e-10
);
}
#[test]
fn test_squared_euclidean_identical() {
let v = array![3.0_f64, 7.0, -1.0];
assert_abs_diff_eq!(squared_euclidean_distance_row(&v, &v), 0.0, epsilon = 1e-12);
}
#[test]
fn test_squared_euclidean_symmetry() {
let a = array![1.0_f64, 5.0];
let b = array![4.0_f64, 1.0];
let d_ab = squared_euclidean_distance_row(&a, &b);
let d_ba = squared_euclidean_distance_row(&b, &a);
assert_abs_diff_eq!(d_ab, d_ba, epsilon = 1e-12);
}
#[test]
fn test_squared_euclidean_non_negative() {
let a = array![2.0_f64, -3.0];
let b = array![-1.0_f64, 4.0];
assert!(squared_euclidean_distance_row(&a, &b) >= 0.0);
}
#[test]
fn test_manhattan_basic() {
let v1 = array![1.0_f64, 2.0];
let v2 = array![4.0_f64, 6.0];
assert_abs_diff_eq!(manhattan_distance_row(&v1, &v2), 7.0, epsilon = 1e-10);
}
#[test]
fn test_manhattan_identical() {
let v = array![1.0_f64, 2.0, 3.0];
assert_abs_diff_eq!(manhattan_distance_row(&v, &v), 0.0, epsilon = 1e-12);
}
#[test]
fn test_manhattan_symmetry() {
let a = array![0.0_f64, 5.0, -2.0];
let b = array![3.0_f64, 1.0, 4.0];
assert_abs_diff_eq!(
manhattan_distance_row(&a, &b),
manhattan_distance_row(&b, &a),
epsilon = 1e-12
);
}
#[test]
fn test_manhattan_from_origin() {
let a = array![3.0_f64, 4.0];
let b = array![0.0_f64, 0.0];
assert_abs_diff_eq!(manhattan_distance_row(&a, &b), 7.0, epsilon = 1e-10);
}
#[test]
fn test_minkowski_p1_equals_manhattan() {
let v1 = array![1.0_f64, 2.0];
let v2 = array![4.0_f64, 6.0];
let manhattan = manhattan_distance_row(&v1, &v2);
let mink1 = minkowski_distance_row(&v1, &v2, 1.0);
assert_abs_diff_eq!(mink1, manhattan, epsilon = 1e-10);
}
#[test]
fn test_minkowski_p2_equals_euclidean() {
let v1 = array![1.0_f64, 2.0];
let v2 = array![4.0_f64, 6.0];
let sq_euc = squared_euclidean_distance_row(&v1, &v2); let expected = sq_euc.sqrt(); let mink2 = minkowski_distance_row(&v1, &v2, 2.0);
assert_abs_diff_eq!(mink2, expected, epsilon = 1e-9);
}
#[test]
fn test_minkowski_p2_pythagorean() {
let a = array![3.0_f64, 4.0];
let b = array![0.0_f64, 0.0];
assert_abs_diff_eq!(minkowski_distance_row(&a, &b, 2.0), 5.0, epsilon = 1e-10);
}
#[test]
fn test_minkowski_p3_hand_calc() {
let v1 = array![1.0_f64, 2.0];
let v2 = array![4.0_f64, 6.0];
let expected = 91.0_f64.powf(1.0 / 3.0);
assert_relative_eq!(
minkowski_distance_row(&v1, &v2, 3.0),
expected,
max_relative = 1e-10
);
}
#[test]
fn test_minkowski_identical() {
let v = array![2.0_f64, 5.0, -1.0];
assert_abs_diff_eq!(minkowski_distance_row(&v, &v, 2.0), 0.0, epsilon = 1e-12);
assert_abs_diff_eq!(minkowski_distance_row(&v, &v, 3.0), 0.0, epsilon = 1e-12);
}
#[test]
fn test_minkowski_symmetry() {
let a = array![1.0_f64, 5.0];
let b = array![4.0_f64, 1.0];
assert_abs_diff_eq!(
minkowski_distance_row(&a, &b, 2.5),
minkowski_distance_row(&b, &a, 2.5),
epsilon = 1e-12
);
}
#[test]
fn test_sst_basic() {
let v = array![1.0_f64, 2.0, 3.0];
assert_abs_diff_eq!(sum_of_square_total(&v), 2.0, epsilon = 1e-10);
}
#[test]
fn test_sst_all_same() {
let v = array![5.0_f64, 5.0, 5.0, 5.0];
assert_abs_diff_eq!(sum_of_square_total(&v), 0.0, epsilon = 1e-12);
}
#[test]
fn test_sst_empty() {
let v: ndarray::Array1<f64> = array![];
assert_abs_diff_eq!(sum_of_square_total(&v), 0.0, epsilon = 1e-12);
}
#[test]
fn test_sst_single_element() {
let v = array![7.0_f64];
assert_abs_diff_eq!(sum_of_square_total(&v), 0.0, epsilon = 1e-12);
}
#[test]
fn test_sst_two_elements() {
let v = array![1.0_f64, 3.0];
assert_abs_diff_eq!(sum_of_square_total(&v), 2.0, epsilon = 1e-10);
}
#[test]
fn test_sst_equals_variance_times_n() {
let v = array![2.0_f64, 4.0, 4.0, 4.0, 5.0, 5.0, 7.0, 9.0];
let sst = sum_of_square_total(&v);
let var = variance(&v);
let n = v.len() as f64;
assert_abs_diff_eq!(sst, var * n, epsilon = 1e-9);
}
#[test]
fn test_sse_basic() {
let predicted = array![2.0_f64, 3.0];
let actual = array![1.0_f64, 3.0];
assert_abs_diff_eq!(
sum_of_squared_errors(&predicted, &actual),
1.0,
epsilon = 1e-10
);
}
#[test]
fn test_sse_perfect_prediction() {
let v = array![1.0_f64, 2.0, 3.0];
assert_abs_diff_eq!(sum_of_squared_errors(&v, &v), 0.0, epsilon = 1e-12);
}
#[test]
fn test_sse_unit_errors() {
let predicted = array![0.0_f64, 0.0, 0.0];
let actual = array![1.0_f64, 1.0, 1.0];
assert_abs_diff_eq!(
sum_of_squared_errors(&predicted, &actual),
3.0,
epsilon = 1e-10
);
}
#[test]
fn test_sse_single_element() {
let p = array![5.0_f64];
let a = array![2.0_f64];
assert_abs_diff_eq!(sum_of_squared_errors(&p, &a), 9.0, epsilon = 1e-10);
}
#[test]
fn test_sse_symmetry() {
let p = array![1.0_f64, 3.0, 5.0];
let a = array![2.0_f64, 1.0, 4.0];
assert_abs_diff_eq!(
sum_of_squared_errors(&p, &a),
sum_of_squared_errors(&a, &p),
epsilon = 1e-12
);
}
#[test]
fn test_variance_basic() {
let v = array![1.0_f64, 2.0, 3.0];
let expected = 2.0_f64 / 3.0;
assert_abs_diff_eq!(variance(&v), expected, epsilon = 1e-9);
}
#[test]
fn test_variance_known_textbook() {
let v = array![2.0_f64, 4.0, 4.0, 4.0, 5.0, 5.0, 7.0, 9.0];
assert_abs_diff_eq!(variance(&v), 4.0, epsilon = 1e-9);
}
#[test]
fn test_variance_single_element() {
let v = array![42.0_f64];
assert_abs_diff_eq!(variance(&v), 0.0, epsilon = 1e-12);
}
#[test]
fn test_variance_empty() {
let v: ndarray::Array1<f64> = array![];
assert_abs_diff_eq!(variance(&v), 0.0, epsilon = 1e-12);
}
#[test]
fn test_variance_constant() {
let v = array![3.0_f64, 3.0, 3.0, 3.0];
assert_abs_diff_eq!(variance(&v), 0.0, epsilon = 1e-12);
}
#[test]
fn test_variance_non_negative() {
let v = array![1.0_f64, 5.0, 2.0, 8.0, 3.0];
assert!(variance(&v) >= 0.0);
}
#[test]
fn test_std_basic() {
let v = array![1.0_f64, 2.0, 3.0];
let expected = (2.0_f64 / 3.0).sqrt();
assert_abs_diff_eq!(standard_deviation(&v), expected, epsilon = 1e-9);
}
#[test]
fn test_std_known_textbook() {
let v = array![2.0_f64, 4.0, 4.0, 4.0, 5.0, 5.0, 7.0, 9.0];
assert_abs_diff_eq!(standard_deviation(&v), 2.0, epsilon = 1e-9);
}
#[test]
fn test_std_empty() {
let v: ndarray::Array1<f64> = array![];
assert_abs_diff_eq!(standard_deviation(&v), 0.0, epsilon = 1e-12);
}
#[test]
fn test_std_equals_sqrt_variance() {
let v = array![1.0_f64, 3.0, 5.0, 7.0, 9.0];
let var = variance(&v);
let std = standard_deviation(&v);
assert_abs_diff_eq!(std, var.sqrt(), epsilon = 1e-10);
}
#[test]
fn test_std_constant() {
let v = array![5.0_f64, 5.0, 5.0];
assert_abs_diff_eq!(standard_deviation(&v), 0.0, epsilon = 1e-12);
}
#[test]
fn test_std_non_negative() {
let v = array![-3.0_f64, 1.0, 7.0];
assert!(standard_deviation(&v) >= 0.0);
}
#[test]
fn test_sigmoid_at_zero() {
assert_abs_diff_eq!(sigmoid(0.0), 0.5, epsilon = 1e-10);
}
#[test]
fn test_sigmoid_at_ln3() {
let x = 3.0_f64.ln();
assert_abs_diff_eq!(sigmoid(x), 0.75, epsilon = 1e-10);
}
#[test]
fn test_sigmoid_symmetry() {
let x = 3.0_f64.ln();
assert_abs_diff_eq!(sigmoid(-x), 1.0 - sigmoid(x), epsilon = 1e-12);
assert_abs_diff_eq!(sigmoid(-x), 0.25, epsilon = 1e-10);
}
#[test]
fn test_sigmoid_output_range() {
for &z in &[-1000.0_f64, -1.0, 0.0, 1.0, 1000.0] {
let s = sigmoid(z);
assert!((0.0..=1.0).contains(&s), "sigmoid({z}) = {s} not in [0,1]");
}
}
#[test]
fn test_sigmoid_large_positive() {
assert_abs_diff_eq!(sigmoid(1000.0), 1.0, epsilon = 1e-12);
}
#[test]
fn test_sigmoid_large_negative() {
assert_abs_diff_eq!(sigmoid(-1000.0), 0.0, epsilon = 1e-12);
}
#[test]
fn test_sigmoid_monotone() {
assert!(sigmoid(-2.0) < sigmoid(-1.0));
assert!(sigmoid(-1.0) < sigmoid(0.0));
assert!(sigmoid(0.0) < sigmoid(1.0));
assert!(sigmoid(1.0) < sigmoid(2.0));
}
#[test]
fn test_logistic_loss_logit_zero_label_zero() {
let logits = array![0.0_f64];
let labels = array![0.0_f64];
let expected = 2.0_f64.ln();
assert_abs_diff_eq!(logistic_loss(&logits, &labels), expected, epsilon = 1e-9);
}
#[test]
fn test_logistic_loss_near_zero_on_confident_correct() {
let logits = array![20.0_f64, -20.0];
let labels = array![1.0_f64, 0.0];
assert_abs_diff_eq!(logistic_loss(&logits, &labels), 0.0, epsilon = 1e-6);
}
#[test]
fn test_logistic_loss_non_negative() {
let logits = array![0.5_f64, -1.0, 2.0, -0.3];
let labels = array![1.0_f64, 0.0, 1.0, 0.0];
assert!(logistic_loss(&logits, &labels) >= 0.0);
}
#[test]
fn test_logistic_loss_three_samples() {
let logits = array![1.0_f64, -1.0, 0.0];
let labels = array![1.0_f64, 0.0, 1.0];
let e = std::f64::consts::E;
let loss_a = (1.0 + 1.0 / e).ln();
let loss_b = (1.0 + 1.0 / e).ln();
let loss_c = 2.0_f64.ln();
let expected = (loss_a + loss_b + loss_c) / 3.0;
assert_abs_diff_eq!(logistic_loss(&logits, &labels), expected, epsilon = 1e-9);
}
#[test]
fn test_hinge_loss_large_margins_zero() {
let margins = array![2.0_f64, -3.0];
let labels = array![1.0_f64, -1.0];
assert_abs_diff_eq!(hinge_loss(&margins, &labels), 0.0, epsilon = 1e-10);
}
#[test]
fn test_hinge_loss_all_wrong() {
let margins = array![-1.0_f64, 1.0];
let labels = array![1.0_f64, -1.0];
assert_abs_diff_eq!(hinge_loss(&margins, &labels), 2.0, epsilon = 1e-10);
}
#[test]
fn test_hinge_loss_on_boundary() {
let margins = array![1.0_f64];
let labels = array![1.0_f64];
assert_abs_diff_eq!(hinge_loss(&margins, &labels), 0.0, epsilon = 1e-12);
}
#[test]
fn test_hinge_loss_mixed() {
let margins = array![1.5_f64, 0.5, -0.5];
let labels = array![1.0_f64, -1.0, 1.0];
assert_abs_diff_eq!(hinge_loss(&margins, &labels), 1.0, epsilon = 1e-10);
}
#[test]
fn test_hinge_loss_non_negative() {
let margins = array![0.3_f64, -0.7, 1.2, 2.5];
let labels = array![1.0_f64, -1.0, 1.0, -1.0];
assert!(hinge_loss(&margins, &labels) >= 0.0);
}
#[test]
fn test_average_path_length_n4() {
let expected = 13.0_f64 / 6.0;
assert_abs_diff_eq!(average_path_length_factor(4), expected, epsilon = 1e-9);
}
#[test]
fn test_average_path_length_n5() {
let expected = 77.0_f64 / 30.0;
assert_abs_diff_eq!(average_path_length_factor(5), expected, epsilon = 1e-9);
}
#[test]
fn test_average_path_length_base_cases() {
assert_abs_diff_eq!(average_path_length_factor(0), 0.0, epsilon = 1e-12);
assert_abs_diff_eq!(average_path_length_factor(1), 0.0, epsilon = 1e-12);
assert_abs_diff_eq!(average_path_length_factor(2), 1.0, epsilon = 1e-12);
}
#[test]
fn test_average_path_length_positive_for_n_ge_3() {
for n in 3..=200 {
let f = average_path_length_factor(n);
assert!(f > 0.0, "expected positive factor for n={n}, got {f}");
}
}
#[test]
fn test_average_path_length_continuous_at_branch_boundary() {
let f50 = average_path_length_factor(50);
let f51 = average_path_length_factor(51);
let delta = (f51 - f50).abs();
assert!(
delta < 0.1,
"discontinuity at branch boundary: c(50)={f50}, c(51)={f51}, |diff|={delta}"
);
assert!(f51 > f50, "expected c(51)={f51} > c(50)={f50}");
}
#[test]
fn test_variance_skips_non_finite_and_uses_finite_subset() {
let finite_only = array![1.0_f64, 3.0];
assert_abs_diff_eq!(variance(&finite_only), 1.0, epsilon = 1e-12);
let with_nan = array![1.0_f64, f64::NAN, 3.0];
assert_abs_diff_eq!(variance(&with_nan), 1.0, epsilon = 1e-12);
let with_inf = array![1.0_f64, f64::INFINITY, 3.0];
assert_abs_diff_eq!(variance(&with_inf), 1.0, epsilon = 1e-12);
let all_nan = array![f64::NAN, f64::NAN];
assert_abs_diff_eq!(variance(&all_nan), 0.0, epsilon = 1e-12);
}
#[test]
fn test_standard_deviation_skips_non_finite() {
let with_nan = array![1.0_f64, f64::NAN, 3.0];
assert_abs_diff_eq!(standard_deviation(&with_nan), 1.0, epsilon = 1e-12);
assert_abs_diff_eq!(
standard_deviation(&with_nan),
variance(&with_nan).sqrt(),
epsilon = 1e-12
);
}