use approx::assert_relative_eq;
use infomeasure::estimators::{Entropy, GlobalValue, LocalValues};
use ndarray::{Array1, Array2};
use rand::rngs::StdRng;
use rand::{Rng, SeedableRng};
use rand_distr::{Distribution, Normal};
use rstest::rstest;
use validation::python;
use crate::test_helpers::generate_gaussian_data;
fn compare_kernel_entropy(
data: Vec<f64>,
bandwidth: f64,
kernel_type: &str,
test_name: &str,
) -> (f64, Vec<f64>) {
let data_array = Array1::from(data.clone());
let kernel_entropy =
Entropy::new_kernel_with_type(data_array, kernel_type.to_string(), bandwidth);
let rust_global_entropy = kernel_entropy.global_value();
let rust_local_entropy = kernel_entropy.local_values();
let kernel_kwargs = [
("kernel".to_string(), format!("\"{kernel_type}\"")),
("bandwidth".to_string(), bandwidth.to_string()),
];
let python_global_entropy =
python::calculate_entropy_float(&data, "kernel", &kernel_kwargs).unwrap();
let python_local_entropy =
python::calculate_local_entropy_float(&data, "kernel", &kernel_kwargs).unwrap();
println!("{test_name} - Rust global entropy (base e): {rust_global_entropy}");
println!("{test_name} - Python global entropy (base e): {python_global_entropy}");
let (epsilon, max_relative) = match kernel_type {
"box" => (1e-12, 1e-12),
"gaussian" => {
#[cfg(feature = "gpu")]
{
(1e-6, 1e-6) }
#[cfg(not(feature = "gpu"))]
{
(1e-12, 1e-12)
}
}
_ => (1e-12, 1e-12),
};
assert_relative_eq!(
rust_global_entropy,
python_global_entropy,
epsilon = epsilon,
max_relative = max_relative
);
for (rust_val, python_val) in rust_local_entropy.iter().zip(python_local_entropy.iter()) {
assert_relative_eq!(
*rust_val,
*python_val,
epsilon = epsilon,
max_relative = max_relative
);
}
(rust_global_entropy, rust_local_entropy.to_vec())
}
#[test]
fn test_kernel_entropy_2d_different_dimensions() {
let seed = 42;
let mut rng = StdRng::seed_from_u64(seed);
let size = 100;
let bandwidth = 1.0;
let kernel_types = ["box", "gaussian"];
macro_rules! test_dimension {
($dim:expr, $kernel_type:expr) => {{
let mut data = Array2::zeros((size, $dim));
for i in 0..size {
for j in 0..$dim {
data[[i, j]] = rng.gen_range(0.0..20.0);
}
}
let test_name = format!("Kernel Entropy 2D (dim={}, kernel={})", $dim, $kernel_type);
match $dim {
1 => compare_kernel_entropy_2d_generic::<1>(
data,
bandwidth,
$kernel_type,
&test_name,
),
2 => compare_kernel_entropy_2d_generic::<2>(
data,
bandwidth,
$kernel_type,
&test_name,
),
3 => compare_kernel_entropy_2d_generic::<3>(
data,
bandwidth,
$kernel_type,
&test_name,
),
4 => compare_kernel_entropy_2d_generic::<4>(
data,
bandwidth,
$kernel_type,
&test_name,
),
5 => compare_kernel_entropy_2d_generic::<5>(
data,
bandwidth,
$kernel_type,
&test_name,
),
6 => compare_kernel_entropy_2d_generic::<6>(
data,
bandwidth,
$kernel_type,
&test_name,
),
7 => compare_kernel_entropy_2d_generic::<7>(
data,
bandwidth,
$kernel_type,
&test_name,
),
8 => compare_kernel_entropy_2d_generic::<8>(
data,
bandwidth,
$kernel_type,
&test_name,
),
_ => panic!("Unsupported dimension: {}", $dim),
};
}};
}
for &kernel_type in &kernel_types {
for dim in 1..=8 {
test_dimension!(dim, kernel_type);
}
}
}
fn compare_kernel_entropy_2d_generic<const K: usize>(
data: Array2<f64>,
bandwidth: f64,
kernel_type: &str,
test_name: &str,
) -> (f64, Vec<f64>) {
let kernel_entropy =
Entropy::nd_kernel_with_type::<K>(data.clone(), kernel_type.to_string(), bandwidth);
let rust_global_entropy = kernel_entropy.global_value();
let rust_local_entropy = kernel_entropy.local_values();
let flat_data: Vec<f64> = data.iter().cloned().collect();
let dims = data.ncols();
let kernel_kwargs = [
("kernel".to_string(), format!("\"{kernel_type}\"")),
("bandwidth".to_string(), bandwidth.to_string()),
];
let python_global_entropy =
python::calculate_entropy_float_nd(&flat_data, dims, "kernel", &kernel_kwargs).unwrap();
let python_local_entropy =
python::calculate_local_entropy_float_nd(&flat_data, dims, "kernel", &kernel_kwargs)
.unwrap();
println!("{test_name} - Rust global entropy (base e): {rust_global_entropy}");
println!("{test_name} - Python global entropy (base e): {python_global_entropy}");
let (epsilon, max_relative) = match kernel_type {
"box" => (1e-12, 1e-12),
"gaussian" => {
#[cfg(feature = "gpu")]
{
(1e-6, 1e-6) }
#[cfg(not(feature = "gpu"))]
{
(1e-12, 1e-12)
}
}
_ => (1e-12, 1e-12),
};
assert_relative_eq!(
rust_global_entropy,
python_global_entropy,
epsilon = epsilon,
max_relative = max_relative
);
let sample_size = rust_local_entropy.len().min(10);
let step = rust_local_entropy.len() / sample_size.max(1);
for i in (0..rust_local_entropy.len()).step_by(step.max(1)) {
if i < rust_local_entropy.len() && i < python_local_entropy.len() {
let rust_val = rust_local_entropy[i];
let python_val = python_local_entropy[i];
assert_relative_eq!(
rust_val,
python_val,
epsilon = epsilon,
max_relative = max_relative
);
}
}
(rust_global_entropy, rust_local_entropy.to_vec())
}
#[rstest]
fn test_kernel_entropy_different_bandwidths(
#[values("box", "gaussian")] kernel_type: &str,
#[values(0.5, 1.0, 2.0)] bandwidth: f64,
) {
let seed = 42;
let mut rng = StdRng::seed_from_u64(seed);
let size = 100;
let data: Vec<f64> = (0..size).map(|_| rng.gen_range(0.0..20.0)).collect();
let test_name = format!("Kernel Entropy (kernel={kernel_type}, bandwidth={bandwidth})");
compare_kernel_entropy(data, bandwidth, kernel_type, &test_name);
}
#[rstest]
fn test_kernel_entropy_gaussian(#[values("box", "gaussian")] kernel_type: &str) {
let seed = 123;
let mut rng = StdRng::seed_from_u64(seed);
let size = 100;
let normal = Normal::new(0.0, 1.0).unwrap();
let data: Vec<f64> = (0..size).map(|_| normal.sample(&mut rng)).collect();
let bandwidth = 1.0;
let test_name = format!("Kernel Entropy (Gaussian data, kernel={kernel_type})");
compare_kernel_entropy(data, bandwidth, kernel_type, &test_name);
}
#[rstest]
fn test_kernel_entropy_gaussian_parameter_combinations(
#[values(1, 2, 3, 4)] dim: usize,
#[values(0.4, 1.0, 2.1)] bandwidth: f64,
#[values(1.0, 1.5)] mean: f64,
#[values(100, 1000)] size: usize,
) {
let seed = 42;
let std_dev = 1.0; let kernel_type = "gaussian";
let data = generate_gaussian_data(size, dim, mean, std_dev, seed);
let test_name =
format!("Kernel Entropy (dim={dim}, bandwidth={bandwidth}, mean={mean}, size={size})");
match dim {
1 => compare_kernel_entropy_2d_generic::<1>(data, bandwidth, kernel_type, &test_name),
2 => compare_kernel_entropy_2d_generic::<2>(data, bandwidth, kernel_type, &test_name),
3 => compare_kernel_entropy_2d_generic::<3>(data, bandwidth, kernel_type, &test_name),
4 => compare_kernel_entropy_2d_generic::<4>(data, bandwidth, kernel_type, &test_name),
_ => panic!("Unsupported dimension: {dim}"),
};
}