pub mod matmul;
pub mod reduction;
use ahash::AHashMap;
use ndarray::{ArrayBase, Data, Ix1, Zip};
const EULER_GAMMA: f64 = 0.57721566490153286060651209008240243104215933593992;
#[inline]
fn validate_pair(expected: usize, found: usize, what: &str) {
if expected != found {
panic!("dimension mismatch: expected {expected}, found {found}");
}
if expected == 0 {
panic!("input is empty: {what}");
}
}
#[inline]
pub fn sum_of_square_total<S>(values: &ArrayBase<S, Ix1>) -> f64
where
S: Data<Elem = f64>,
{
if values.is_empty() {
return 0.0;
}
let mean = values.mean().unwrap();
values.mapv(|x| (x - mean).powi(2)).sum()
}
#[inline]
pub fn sum_of_squared_errors<S1, S2>(
predicted: &ArrayBase<S1, Ix1>,
actual: &ArrayBase<S2, Ix1>,
) -> f64
where
S1: Data<Elem = f64>,
S2: Data<Elem = f64>,
{
validate_pair(predicted.len(), actual.len(), "predicted and actual");
predicted
.iter()
.zip(actual.iter())
.map(|(p, a)| (p - a).powi(2))
.sum()
}
#[inline]
pub fn variance<S>(y: &ArrayBase<S, Ix1>) -> f64
where
S: Data<Elem = f64>,
{
if y.is_empty() {
return 0.0;
}
let (finite_sum, finite_count) = y.fold((0.0_f64, 0_usize), |(sum, count), &val| {
if val.is_finite() {
(sum + val, count + 1)
} else {
(sum, count)
}
});
if finite_count == 0 {
return 0.0;
}
let mean = finite_sum / finite_count as f64;
let sum_squared_diff = y.fold(0.0, |acc, &val| {
if val.is_finite() {
let diff = val - mean;
acc + diff * diff
} else {
acc
}
});
sum_squared_diff / finite_count as f64
}
#[inline]
pub fn sigmoid(z: f64) -> f64 {
const MAX_SIGMOID_INPUT: f64 = 500.0;
const MIN_SIGMOID_INPUT: f64 = -500.0;
if z > MAX_SIGMOID_INPUT {
return 1.0;
} else if z < MIN_SIGMOID_INPUT {
return 0.0;
}
1.0 / (1.0 + (-z).exp())
}
const EXP_REDUCE_MIN_ELEMS: usize = 32_768;
#[inline]
pub fn logistic_loss<S1, S2>(logits: &ArrayBase<S1, Ix1>, actual_labels: &ArrayBase<S2, Ix1>) -> f64
where
S1: Data<Elem = f64>,
S2: Data<Elem = f64>,
{
validate_pair(
logits.len(),
actual_labels.len(),
"logits and actual_labels",
);
let n = logits.len() as f64;
let loss_term = |x: f64, y: f64| x.max(0.0) - x * y + (1.0 + (-x.abs()).exp()).ln();
let total_loss = match (logits.as_slice(), actual_labels.as_slice()) {
(Some(x), Some(y)) => reduction::det_reduce_range(
x.len(),
x.len() >= EXP_REDUCE_MIN_ELEMS,
|range| range.map(|i| loss_term(x[i], y[i])).sum::<f64>(),
|a, b| a + b,
0.0,
),
_ => logits
.iter()
.zip(actual_labels.iter())
.map(|(&x, &y)| loss_term(x, y))
.sum::<f64>(),
};
total_loss / n
}
#[inline]
pub fn hinge_loss<S1, S2>(margins: &ArrayBase<S1, Ix1>, labels: &ArrayBase<S2, Ix1>) -> f64
where
S1: Data<Elem = f64>,
S2: Data<Elem = f64>,
{
validate_pair(margins.len(), labels.len(), "margins and labels");
let n = margins.len();
margins
.iter()
.zip(labels.iter())
.map(|(&m, &y)| (1.0 - y * m).max(0.0))
.sum::<f64>()
/ n as f64
}
#[inline]
pub fn squared_euclidean_distance_row<S1, S2>(
x1: &ArrayBase<S1, Ix1>,
x2: &ArrayBase<S2, Ix1>,
) -> f64
where
S1: Data<Elem = f64>,
S2: Data<Elem = f64>,
{
let mut sum = 0.0;
Zip::from(x1).and(x2).for_each(|&a, &b| {
let d = a - b;
sum += d * d;
});
sum
}
#[inline]
pub fn manhattan_distance_row<S1, S2>(x1: &ArrayBase<S1, Ix1>, x2: &ArrayBase<S2, Ix1>) -> f64
where
S1: Data<Elem = f64>,
S2: Data<Elem = f64>,
{
let mut sum = 0.0;
Zip::from(x1)
.and(x2)
.for_each(|&a, &b| sum += (a - b).abs());
sum
}
#[inline]
pub fn minkowski_distance_row<S1, S2>(
x1: &ArrayBase<S1, Ix1>,
x2: &ArrayBase<S2, Ix1>,
p: f64,
) -> f64
where
S1: Data<Elem = f64>,
S2: Data<Elem = f64>,
{
if p < 1.0 || p.is_nan() {
panic!("invalid parameter `p`: Minkowski order must be at least 1.0, got {p}");
}
let mut sum = 0.0;
Zip::from(x1)
.and(x2)
.for_each(|&a, &b| sum += (a - b).abs().powf(p));
sum.powf(1.0 / p)
}
#[inline]
pub fn gini<S>(y: &ArrayBase<S, Ix1>) -> f64
where
S: Data<Elem = f64>,
{
let total_samples = y.len() as f64;
if total_samples == 0.0 {
return 0.0;
}
let mut class_counts = AHashMap::with_capacity(10);
y.fold((), |_, &value| {
if value.is_nan() {
return; }
let key = (value * 1000.0).round() as i64;
*class_counts.entry(key).or_insert(0) += 1;
});
if class_counts.is_empty() {
return 0.0;
}
let valid_samples: f64 = class_counts.values().map(|&c| c as f64).sum();
let mut sum_squared_proportions = 0.0;
for &count in class_counts.values() {
let p = count as f64 / valid_samples;
sum_squared_proportions += p * p;
}
1.0 - sum_squared_proportions
}
#[inline]
pub fn entropy<S>(y: &ArrayBase<S, Ix1>) -> f64
where
S: Data<Elem = f64>,
{
let total_samples = y.len() as f64;
if total_samples == 0.0 {
return 0.0;
}
let mut class_counts = AHashMap::with_capacity(10);
y.fold((), |_, &value| {
if value.is_nan() {
return; }
let key = (value * 1000.0).round() as i64;
*class_counts.entry(key).or_insert(0) += 1;
});
if class_counts.is_empty() {
return 0.0;
}
let valid_samples: f64 = class_counts.values().map(|&c| c as f64).sum();
let mut entropy = 0.0;
for &count in class_counts.values() {
let p = count as f64 / valid_samples;
if p > 0.0 {
entropy -= p * p.log2();
}
}
entropy
}
#[inline]
pub fn standard_deviation<S>(values: &ArrayBase<S, Ix1>) -> f64
where
S: Data<Elem = f64>,
{
variance(values).sqrt()
}
#[inline]
pub fn average_path_length_factor(n: usize) -> f64 {
if n <= 1 {
return 0.0;
}
if n == 2 {
return 1.0;
}
let h_n_minus_1 = if n > 50 {
let m = (n - 1) as f64;
m.ln() + EULER_GAMMA + 0.5 / m
} else {
(1..n).map(|i| 1.0 / i as f64).sum::<f64>()
};
2.0 * h_n_minus_1 - 2.0 * (n - 1) as f64 / n as f64
}
#[cfg(test)]
mod tests {
use super::*;
use approx::assert_abs_diff_eq;
use ndarray::array;
#[test]
fn test_hinge_loss_basic() {
let margins = array![0.8, -0.5, 2.0];
let labels = array![1.0, -1.0, 1.0];
let expected = 0.7_f64 / 3.0;
assert_abs_diff_eq!(hinge_loss(&margins, &labels), expected, epsilon = 1e-6);
}
#[test]
fn test_hinge_loss_all_correct() {
let margins = array![2.0, -3.0];
let labels = array![1.0, -1.0];
assert_abs_diff_eq!(hinge_loss(&margins, &labels), 0.0, epsilon = 1e-10);
}
#[test]
#[should_panic(expected = "input is empty")]
fn test_hinge_loss_empty_panics() {
let margins: ndarray::Array1<f64> = array![];
let labels: ndarray::Array1<f64> = array![];
let _ = hinge_loss(&margins, &labels);
}
#[test]
#[should_panic(expected = "dimension mismatch")]
fn test_hinge_loss_length_mismatch_panics() {
let margins = array![0.8, -0.5, 2.0];
let labels = array![1.0, -1.0];
let _ = hinge_loss(&margins, &labels);
}
#[test]
fn test_hinge_loss_single_sample() {
let margins = array![0.5];
let labels = array![1.0];
assert_abs_diff_eq!(hinge_loss(&margins, &labels), 0.5, epsilon = 1e-10);
}
#[test]
fn test_average_path_length_factor_n0() {
assert_abs_diff_eq!(average_path_length_factor(0), 0.0, epsilon = 1e-10);
}
#[test]
fn test_average_path_length_factor_n1() {
assert_abs_diff_eq!(average_path_length_factor(1), 0.0, epsilon = 1e-10);
}
#[test]
fn test_average_path_length_factor_n2() {
assert_abs_diff_eq!(average_path_length_factor(2), 1.0, epsilon = 1e-10);
}
#[test]
fn test_average_path_length_factor_n3() {
let expected = 5.0_f64 / 3.0;
assert_abs_diff_eq!(average_path_length_factor(3), expected, epsilon = 1e-9);
}
#[test]
fn test_average_path_length_factor_monotone() {
let f10 = average_path_length_factor(10);
let f100 = average_path_length_factor(100);
let f1000 = average_path_length_factor(1000);
assert!(
f10 < f100,
"expected factor(10) < factor(100), got {f10} vs {f100}"
);
assert!(
f100 < f1000,
"expected factor(100) < factor(1000), got {f100} vs {f1000}"
);
}
#[test]
fn test_average_path_length_factor_monotone_across_branch_boundary() {
let f49 = average_path_length_factor(49); let f50 = average_path_length_factor(50); let f51 = average_path_length_factor(51); let f52 = average_path_length_factor(52); assert!(f49 < f50, "factor(49)={f49} should be < factor(50)={f50}");
assert!(f50 < f51, "factor(50)={f50} should be < factor(51)={f51}");
assert!(f51 < f52, "factor(51)={f51} should be < factor(52)={f52}");
}
#[test]
fn test_average_path_length_factor_matches_exact_harmonic_within_tolerance() {
for &n in &[51usize, 100, 1000] {
let exact_h: f64 = (1..n).map(|i| 1.0 / i as f64).sum();
let theoretical = 2.0 * exact_h - 2.0 * (n - 1) as f64 / n as f64;
let got = average_path_length_factor(n);
assert!(
(got - theoretical).abs() < 1e-3,
"n={n}: factor={got} deviates from theoretical {theoretical} by more than the documented approximation bound",
);
}
}
#[test]
fn test_logistic_loss_single_logit_zero_label_one() {
let logits = array![0.0];
let labels = array![1.0];
let expected = 2.0_f64.ln();
assert_abs_diff_eq!(logistic_loss(&logits, &labels), expected, epsilon = 1e-6);
}
#[test]
fn test_logistic_loss_docstring_example() {
let logits = array![0.0, 2.0, -1.0];
let labels = array![0.0, 1.0, 0.0];
assert_abs_diff_eq!(logistic_loss(&logits, &labels), 0.377779, epsilon = 1e-5);
}
#[test]
fn test_gini_float_key_collapse_same_class() {
let labels = array![1.0000_f64, 1.0003_f64];
assert_abs_diff_eq!(gini(&labels), 0.0, epsilon = 1e-10);
}
#[test]
fn test_gini_float_key_two_distinct_classes() {
let labels = array![1.000_f64, 1.001_f64];
assert_abs_diff_eq!(gini(&labels), 0.5, epsilon = 1e-10);
}
#[test]
fn test_gini_all_nan() {
let labels = array![f64::NAN];
assert_abs_diff_eq!(gini(&labels), 0.0, epsilon = 1e-10);
}
#[test]
fn test_entropy_float_key_collapse_same_class() {
let labels = array![1.0000_f64, 1.0003_f64];
assert_abs_diff_eq!(entropy(&labels), 0.0, epsilon = 1e-10);
}
#[test]
fn test_entropy_float_key_two_distinct_classes() {
let labels = array![1.000_f64, 1.001_f64];
assert_abs_diff_eq!(entropy(&labels), 1.0, epsilon = 1e-10);
}
#[test]
fn test_entropy_all_nan() {
let labels = array![f64::NAN];
assert_abs_diff_eq!(entropy(&labels), 0.0, epsilon = 1e-10);
}
#[test]
fn test_entropy_nan_excluded_from_denominator() {
let labels = array![0.0_f64, f64::NAN, 1.0];
assert_abs_diff_eq!(entropy(&labels), 1.0, epsilon = 1e-10);
}
#[test]
fn test_gini_nan_excluded_from_denominator() {
let labels = array![0.0_f64, f64::NAN, 1.0];
assert_abs_diff_eq!(gini(&labels), 0.5, epsilon = 1e-10);
}
}