use crate::matrix::FdMatrix;
use super::{cross_distance_matrix, self_distance_matrix};
fn trapezoidal_weights(argvals: &[f64]) -> Vec<f64> {
let m = argvals.len();
if m < 2 {
return vec![0.0; m];
}
let mut w = vec![0.0; m];
for k in 0..m - 1 {
let h = argvals[k + 1] - argvals[k];
w[k] += 0.5 * h;
w[k + 1] += 0.5 * h;
}
w
}
fn normalize_density(values: &[f64], weights: &[f64], epsilon: f64) -> Vec<f64> {
let m = values.len();
let min_val = values.iter().cloned().fold(f64::INFINITY, f64::min);
let shift = if min_val < 0.0 { -min_val } else { 0.0 };
let mut density: Vec<f64> = values.iter().map(|&v| v + shift).collect();
let integral: f64 = density
.iter()
.zip(weights.iter())
.map(|(&d, &w)| d * w)
.sum();
if integral > 0.0 {
for d in &mut density {
*d /= integral;
}
} else {
let total_w: f64 = weights.iter().sum();
let uniform = if total_w > 0.0 {
1.0 / total_w
} else {
1.0 / m as f64
};
density.fill(uniform);
}
for d in &mut density {
*d += epsilon;
}
let integral2: f64 = density
.iter()
.zip(weights.iter())
.map(|(&d, &w)| d * w)
.sum();
if integral2 > 0.0 {
for d in &mut density {
*d /= integral2;
}
}
density
}
fn kl_divergence(p: &[f64], q: &[f64], weights: &[f64]) -> f64 {
p.iter()
.zip(q.iter())
.zip(weights.iter())
.map(|((&pi, &qi), &w)| {
if pi > 0.0 && qi > 0.0 {
pi * (pi / qi).ln() * w
} else {
0.0
}
})
.sum()
}
fn symmetric_kl(p: &[f64], q: &[f64], weights: &[f64]) -> f64 {
(kl_divergence(p, q, weights) + kl_divergence(q, p, weights)) * 0.5
}
#[must_use]
pub fn kl_self_1d(data: &FdMatrix, argvals: &[f64], epsilon: f64) -> FdMatrix {
let n = data.nrows();
let m = data.ncols();
if n == 0 || m < 2 || argvals.len() != m {
return FdMatrix::zeros(0, 0);
}
let weights = trapezoidal_weights(argvals);
let densities: Vec<Vec<f64>> = (0..n)
.map(|i| {
let row = data.row(i);
normalize_density(&row, &weights, epsilon)
})
.collect();
self_distance_matrix(n, |i, j| {
symmetric_kl(&densities[i], &densities[j], &weights)
})
}
#[must_use]
pub fn kl_cross_1d(data1: &FdMatrix, data2: &FdMatrix, argvals: &[f64], epsilon: f64) -> FdMatrix {
let n1 = data1.nrows();
let n2 = data2.nrows();
let m = data1.ncols();
if n1 == 0 || n2 == 0 || m < 2 || argvals.len() != m || data2.ncols() != m {
return FdMatrix::zeros(0, 0);
}
let weights = trapezoidal_weights(argvals);
let densities1: Vec<Vec<f64>> = (0..n1)
.map(|i| {
let row = data1.row(i);
normalize_density(&row, &weights, epsilon)
})
.collect();
let densities2: Vec<Vec<f64>> = (0..n2)
.map(|i| {
let row = data2.row(i);
normalize_density(&row, &weights, epsilon)
})
.collect();
cross_distance_matrix(n1, n2, |i, j| {
symmetric_kl(&densities1[i], &densities2[j], &weights)
})
}