use ndarray::{arr2, array};
use rustyml::error::Error;
use rustyml::utils::{to_categorical, to_categorical_with_mapping, to_sparse_categorical};
use crate::common::assert_allclose;
#[test]
fn to_categorical_basic_full_matrix() {
let labels = array![0i32, 1, 2, 1, 0];
let result = to_categorical(&labels, None).unwrap();
assert_eq!(result.shape(), &[5, 3], "shape must be (5, 3)");
let expected = arr2(&[
[1.0f64, 0.0, 0.0],
[0.0, 1.0, 0.0],
[0.0, 0.0, 1.0],
[0.0, 1.0, 0.0],
[1.0, 0.0, 0.0],
]);
assert_allclose(&result, &expected, 0.0);
}
#[test]
fn to_categorical_single_class() {
let labels = array![0i32];
let result = to_categorical(&labels, None).unwrap();
assert_eq!(result.shape(), &[1, 1]);
let expected = arr2(&[[1.0f64]]);
assert_allclose(&result, &expected, 0.0);
}
#[test]
fn to_categorical_explicit_num_classes_wider_with_zero_padding() {
let labels = array![0i32, 2];
let result = to_categorical(&labels, Some(5)).unwrap();
assert_eq!(result.shape(), &[2, 5], "shape must be (2, 5)");
let expected = arr2(&[[1.0f64, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 1.0, 0.0, 0.0]]);
assert_allclose(&result, &expected, 0.0);
assert_eq!(result[[0, 3]], 0.0);
assert_eq!(result[[0, 4]], 0.0);
assert_eq!(result[[1, 3]], 0.0);
assert_eq!(result[[1, 4]], 0.0);
}
#[test]
fn to_categorical_exact_num_classes_ok() {
let labels = array![0i32, 1, 2];
let result = to_categorical(&labels, Some(3)).unwrap();
assert_eq!(result.shape(), &[3, 3]);
assert_eq!(result[[1, 1]], 1.0);
assert_eq!(result[[1, 0]], 0.0);
}
#[test]
fn to_categorical_empty_labels() {
let labels = ndarray::Array1::<i32>::from_vec(vec![]); let result = to_categorical(&labels, None).unwrap();
assert_eq!(result.shape(), &[0, 1]);
assert_eq!(result.len(), 0);
}
#[test]
fn to_categorical_negative_label_is_invalid_input() {
let labels = array![0i32, -1, 2];
let err = to_categorical(&labels, None).unwrap_err();
assert!(
matches!(err, Error::InvalidInput(_)),
"expected InvalidInput, got {:?}",
err
);
}
#[test]
fn to_categorical_num_classes_too_small_is_invalid_parameter() {
let labels = array![0i32, 1, 2]; let err = to_categorical(&labels, Some(2)).unwrap_err();
assert!(
matches!(err, Error::InvalidParameter { .. }),
"expected InvalidParameter, got {:?}",
err
);
}
#[test]
fn to_categorical_num_classes_zero_with_labels_is_invalid_parameter() {
let labels = array![0i32, 1];
let err = to_categorical(&labels, Some(0)).unwrap_err();
assert!(
matches!(err, Error::InvalidParameter { .. }),
"expected InvalidParameter, got {:?}",
err
);
}
#[test]
fn to_sparse_categorical_strict_one_hot() {
let categorical = arr2(&[[1.0f64, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]]);
let labels = to_sparse_categorical(&categorical).unwrap();
let expected = array![0i32, 1, 2];
assert_eq!(labels, expected);
}
#[test]
fn to_sparse_categorical_soft_probabilities() {
let probs = arr2(&[[0.1f64, 0.9, 0.0], [0.2, 0.3, 0.5], [0.6, 0.3, 0.1]]);
let labels = to_sparse_categorical(&probs).unwrap();
let expected = array![1i32, 2, 0];
assert_eq!(labels, expected);
}
#[test]
fn to_sparse_categorical_tie_breaks_to_first_index() {
let tied = arr2(&[[0.5f64, 0.5, 0.0], [0.0, 0.5, 0.5]]);
let labels = to_sparse_categorical(&tied).unwrap();
assert_eq!(labels[0], 0i32, "tie in row 0 should give index 0 (first)");
assert_eq!(labels[1], 1i32, "tie in row 1 should give index 1 (first)");
}
#[test]
fn to_sparse_categorical_nan_is_non_finite() {
let mat = arr2(&[[0.1f64, f64::NAN, 0.5], [0.2, 0.3, 0.5]]);
let err = to_sparse_categorical(&mat).unwrap_err();
assert!(
matches!(err, Error::NonFinite(_)),
"expected NonFinite, got {:?}",
err
);
}
#[test]
fn to_sparse_categorical_pos_inf_is_non_finite() {
let mat = arr2(&[[0.0f64, f64::INFINITY, 0.0]]);
let err = to_sparse_categorical(&mat).unwrap_err();
assert!(
matches!(err, Error::NonFinite(_)),
"expected NonFinite, got {:?}",
err
);
}
#[test]
fn to_sparse_categorical_neg_inf_is_non_finite() {
let mat = arr2(&[[0.5f64, f64::NEG_INFINITY, 0.3]]);
let err = to_sparse_categorical(&mat).unwrap_err();
assert!(
matches!(err, Error::NonFinite(_)),
"expected NonFinite, got {:?}",
err
);
}
#[test]
fn round_trip_to_categorical_then_to_sparse() {
let original = array![0i32, 1, 2, 1, 0];
let one_hot = to_categorical(&original, None).unwrap();
assert_eq!(one_hot.shape(), &[5, 3]);
let recovered = to_sparse_categorical(&one_hot).unwrap();
assert_eq!(
recovered, original,
"round-trip must recover original labels"
);
}
#[test]
fn round_trip_non_monotone_labels() {
let original = array![3i32, 0, 1, 2];
let one_hot = to_categorical(&original, Some(4)).unwrap();
assert_eq!(one_hot.shape(), &[4, 4]);
let recovered = to_sparse_categorical(&one_hot).unwrap();
assert_eq!(recovered, original);
}
#[test]
fn to_categorical_with_mapping_non_consecutive_integers() {
let labels = vec![10i32, 20, 30, 20, 10];
let (matrix, mapping) = to_categorical_with_mapping(&labels, None).unwrap();
assert_eq!(matrix.shape(), &[5, 3], "shape must be (5, 3)");
assert_eq!(mapping[&10i32], 0, "10 must map to index 0 (first seen)");
assert_eq!(mapping[&20i32], 1, "20 must map to index 1 (second seen)");
assert_eq!(mapping[&30i32], 2, "30 must map to index 2 (third seen)");
let expected = arr2(&[
[1.0f64, 0.0, 0.0],
[0.0, 1.0, 0.0],
[0.0, 0.0, 1.0],
[0.0, 1.0, 0.0],
[1.0, 0.0, 0.0],
]);
assert_allclose(&matrix, &expected, 0.0);
for (i, &label) in labels.iter().enumerate() {
let col = mapping[&label];
assert_eq!(
matrix[[i, col]],
1.0,
"row {} label {} should be hot at column {}",
i,
label,
col
);
for j in 0..3 {
if j != col {
assert_eq!(
matrix[[i, j]],
0.0,
"row {} col {} should be 0 (only col {} is hot)",
i,
j,
col
);
}
}
}
}
#[test]
fn to_categorical_with_mapping_string_labels() {
let labels = vec!["cat", "dog", "bird", "dog", "cat"];
let (matrix, mapping) = to_categorical_with_mapping(&labels, None).unwrap();
assert_eq!(matrix.shape(), &[5, 3]);
assert_eq!(mapping["cat"], 0, "\"cat\" must map to 0 (first seen)");
assert_eq!(mapping["dog"], 1, "\"dog\" must map to 1 (second seen)");
assert_eq!(mapping["bird"], 2, "\"bird\" must map to 2 (third seen)");
for (i, &label) in labels.iter().enumerate() {
let col = mapping[label];
assert_eq!(
matrix[[i, col]],
1.0,
"row {i} should be hot at column {col}"
);
for j in 0..3 {
if j != col {
assert_eq!(matrix[[i, j]], 0.0);
}
}
}
}
#[test]
fn to_categorical_with_mapping_explicit_wider_num_classes() {
let labels = vec![1i32, 2];
let (matrix, mapping) = to_categorical_with_mapping(&labels, Some(5)).unwrap();
assert_eq!(matrix.shape(), &[2, 5], "shape must be (2, 5)");
assert_eq!(mapping[&1i32], 0);
assert_eq!(mapping[&2i32], 1);
assert_eq!(matrix[[0, 0]], 1.0);
assert_eq!(matrix[[1, 1]], 1.0);
for col in 2..5 {
assert_eq!(matrix[[0, col]], 0.0, "row 0 col {col} must be 0");
assert_eq!(matrix[[1, col]], 0.0, "row 1 col {col} must be 0");
}
}
#[test]
fn round_trip_with_mapping() {
let labels = vec![10i32, 20, 30, 20, 10];
let (matrix, mapping) = to_categorical_with_mapping(&labels, None).unwrap();
let mut inverse: std::collections::HashMap<usize, i32> = std::collections::HashMap::new();
for (&label, &idx) in &mapping {
inverse.insert(idx, label);
}
let sparse = to_sparse_categorical(&matrix).unwrap();
assert_eq!(sparse.shape(), &[5]);
let recovered: Vec<i32> = sparse.iter().map(|&idx| inverse[&(idx as usize)]).collect();
assert_eq!(recovered, labels, "round-trip must recover original labels");
}
#[test]
fn to_categorical_with_mapping_insufficient_num_classes_is_invalid_parameter() {
let labels = vec!["a", "b", "c"];
let err = to_categorical_with_mapping(&labels, Some(2)).unwrap_err();
assert!(
matches!(err, Error::InvalidParameter { .. }),
"expected InvalidParameter, got {:?}",
err
);
}
#[test]
fn to_categorical_with_mapping_zero_num_classes_non_empty_is_invalid_parameter() {
let labels = vec![42i32];
let err = to_categorical_with_mapping(&labels, Some(0)).unwrap_err();
assert!(
matches!(err, Error::InvalidParameter { .. }),
"expected InvalidParameter, got {:?}",
err
);
}
#[test]
fn to_categorical_with_mapping_empty_slice_is_zero_by_zero() {
let labels: Vec<i32> = Vec::new(); let (matrix, mapping) = to_categorical_with_mapping(&labels, None).unwrap();
assert_eq!(
matrix.shape(),
&[0, 0],
"empty mapping input must give shape (0, 0)"
);
assert_eq!(matrix.len(), 0);
assert!(mapping.is_empty(), "no labels → empty mapping");
}