use rayon::prelude::*;
use crate::core::math::vector_helpers::quantile_sorted;
use crate::prelude::*;
const KDE_GRID: usize = 512;
const KDE_BINS: usize = 512;
const EM_MAX_ITER: usize = 200;
const EM_TOL: f64 = 1e-8;
const MIN_VARIANCE: f64 = 1e-12;
const UNIMODAL_SD_MULTIPLE: f64 = 2.0;
#[derive(Clone, Copy, Debug)]
pub struct BinariseParams {
pub bw_adjust: f64,
pub n_grid: usize,
pub n_bins: usize,
}
impl Default for BinariseParams {
fn default() -> Self {
Self {
bw_adjust: 1.0,
n_grid: KDE_GRID,
n_bins: KDE_BINS,
}
}
}
#[derive(Clone, Debug)]
pub struct RegulonThresholds {
pub thresholds: Vec<f64>,
pub bimodal: Vec<bool>,
}
#[derive(Clone, Copy, Debug)]
struct TwoComponent {
mu: [f64; 2],
log_lik: f64,
}
#[inline(always)]
fn gaussian_pdf(x: f64, mu: f64, sd: f64) -> f64 {
const INV_SQRT_2PI: f64 = 0.398_942_280_401_432_7;
let z = (x - mu) / sd;
INV_SQRT_2PI / sd * (-0.5 * z * z).exp()
}
#[inline]
fn bic(log_lik: f64, n_params: usize, n: usize) -> f64 {
n_params as f64 * (n as f64).ln() - 2.0 * log_lik
}
fn fit_two_component(x: &[f64], sorted: &[f64], mean: f64, variance: f64) -> Option<TwoComponent> {
let n = x.len();
if n < 4 || variance <= MIN_VARIANCE {
return None;
}
let mut mu = [quantile_sorted(sorted, 0.25), quantile_sorted(sorted, 0.75)];
if (mu[1] - mu[0]).abs() < f64::EPSILON {
let sd = variance.sqrt();
mu = [mean - sd, mean + sd];
}
let mut var = [variance, variance];
let mut weight = [0.5_f64, 0.5];
let mut resp = vec![0.0_f64; n];
let mut log_lik = f64::NEG_INFINITY;
for _ in 0..EM_MAX_ITER {
let mut new_log_lik = 0.0;
let sd = [var[0].sqrt(), var[1].sqrt()];
for (i, &xi) in x.iter().enumerate() {
let d0 = weight[0] * gaussian_pdf(xi, mu[0], sd[0]);
let d1 = weight[1] * gaussian_pdf(xi, mu[1], sd[1]);
let total = d0 + d1;
if total <= f64::MIN_POSITIVE {
resp[i] = 0.5;
continue;
}
resp[i] = d0 / total;
new_log_lik += total.ln();
}
if !new_log_lik.is_finite() {
return None;
}
let n0: f64 = resp.iter().sum();
let n1 = n as f64 - n0;
if n0 < 1.0 || n1 < 1.0 {
return None;
}
let mut mean0 = 0.0;
let mut mean1 = 0.0;
for (i, &xi) in x.iter().enumerate() {
mean0 += resp[i] * xi;
mean1 += (1.0 - resp[i]) * xi;
}
mu = [mean0 / n0, mean1 / n1];
let mut var0 = 0.0;
let mut var1 = 0.0;
for (i, &xi) in x.iter().enumerate() {
let d0 = xi - mu[0];
let d1 = xi - mu[1];
var0 += resp[i] * d0 * d0;
var1 += (1.0 - resp[i]) * d1 * d1;
}
var = [(var0 / n0).max(MIN_VARIANCE), (var1 / n1).max(MIN_VARIANCE)];
weight = [n0 / n as f64, n1 / n as f64];
if (new_log_lik - log_lik).abs() < EM_TOL * new_log_lik.abs().max(1.0) {
log_lik = new_log_lik;
break;
}
log_lik = new_log_lik;
}
if !log_lik.is_finite() {
return None;
}
Some(TwoComponent { mu, log_lik })
}
fn silverman_bandwidth(sorted: &[f64], sd: f64) -> f64 {
let n = sorted.len();
let iqr = quantile_sorted(sorted, 0.75) - quantile_sorted(sorted, 0.25);
let spread = if iqr > 0.0 { sd.min(iqr / 1.349) } else { sd };
let bw = 0.9 * spread * (n as f64).powf(-0.2);
if bw > 0.0 {
bw
} else {
f64::EPSILON.sqrt()
}
}
fn find_density_trough(
x: &[f64],
lo: f64,
hi: f64,
bandwidth: f64,
params: &BinariseParams,
) -> Option<f64> {
if hi <= lo || params.n_grid < 3 {
return None;
}
let (data_min, data_max) = x
.iter()
.fold((f64::INFINITY, f64::NEG_INFINITY), |(mn, mx), &v| {
(mn.min(v), mx.max(v))
});
if data_max <= data_min {
return None;
}
let n_bins = params.n_bins.max(2);
let bin_width = (data_max - data_min) / n_bins as f64;
let mut counts = vec![0.0_f64; n_bins];
for &v in x {
let idx = (((v - data_min) / bin_width) as usize).min(n_bins - 1);
counts[idx] += 1.0;
}
let inv_2h2 = 1.0 / (2.0 * bandwidth * bandwidth);
let step = (hi - lo) / (params.n_grid - 1) as f64;
let mut best_idx = 0_usize;
let mut best_dens = f64::INFINITY;
for g in 0..params.n_grid {
let point = lo + step * g as f64;
let mut dens = 0.0;
for (b, &count) in counts.iter().enumerate() {
if count == 0.0 {
continue;
}
let centre = data_min + bin_width * (b as f64 + 0.5);
let d = point - centre;
dens += count * (-d * d * inv_2h2).exp();
}
if dens < best_dens {
best_dens = dens;
best_idx = g;
}
}
if best_idx == 0 || best_idx == params.n_grid - 1 {
return None;
}
Some(lo + step * best_idx as f64)
}
fn threshold_one_row(row: &[f32], params: &BinariseParams) -> (f64, bool) {
let n = row.len();
if n == 0 {
return (f64::INFINITY, false);
}
let x: Vec<f64> = row.iter().map(|&v| v as f64).collect();
let mean = x.iter().sum::<f64>() / n as f64;
if n < 2 {
return (mean, false);
}
let variance = x.iter().map(|v| (v - mean).powi(2)).sum::<f64>() / (n - 1) as f64;
let sd = variance.sqrt();
let unimodal = mean + UNIMODAL_SD_MULTIPLE * sd;
if variance <= MIN_VARIANCE {
return (unimodal, false);
}
let mut sorted = x.clone();
sorted.sort_unstable_by(|a, b| a.total_cmp(b));
let Some(fit) = fit_two_component(&x, &sorted, mean, variance) else {
return (unimodal, false);
};
let log_lik_1: f64 = x.iter().map(|&v| gaussian_pdf(v, mean, sd).ln()).sum();
if !log_lik_1.is_finite() {
return (unimodal, false);
}
if bic(fit.log_lik, 5, n) > bic(log_lik_1, 2, n) {
return (unimodal, false);
}
let lo = fit.mu[0].min(fit.mu[1]);
let hi = fit.mu[0].max(fit.mu[1]);
let bandwidth = silverman_bandwidth(&sorted, sd) * params.bw_adjust;
match find_density_trough(&x, lo, hi, bandwidth, params) {
Some(trough) => (trough, true),
None => (unimodal, false),
}
}
pub fn derive_regulon_thresholds(
rows: &[Vec<f32>],
params: Option<BinariseParams>,
) -> Result<RegulonThresholds, BixverseErrors> {
if rows.is_empty() {
return Err(BixverseErrors::InvalidArgument(
"The AUC matrix has no rows".to_string(),
));
}
let params = params.unwrap_or_default();
if params.n_grid < 3 || params.n_bins < 2 {
return Err(BixverseErrors::InvalidArgument(
"n_grid must be at least 3 and n_bins at least 2".to_string(),
));
}
let per_row: Vec<(f64, bool)> = rows
.par_iter()
.map(|row| threshold_one_row(row, ¶ms))
.collect();
let (thresholds, bimodal) = per_row.into_iter().unzip();
Ok(RegulonThresholds {
thresholds,
bimodal,
})
}
#[cfg(test)]
mod tests {
use super::*;
fn bimodal_row() -> Vec<f32> {
let mut out = Vec::with_capacity(400);
for i in 0..200 {
out.push(0.1 + (i as f32 - 100.0) * 0.0002);
}
for i in 0..200 {
out.push(0.6 + (i as f32 - 100.0) * 0.0002);
}
out
}
fn unimodal_row() -> Vec<f32> {
(0..400)
.map(|i| 0.3 + (i as f32 - 200.0) * 0.0005)
.collect()
}
#[test]
fn test_threshold_splits_two_clean_modes() {
let row = bimodal_row();
let res = derive_regulon_thresholds(std::slice::from_ref(&row), None).unwrap();
assert!(res.bimodal[0]);
assert!(res.thresholds[0] > 0.15 && res.thresholds[0] < 0.55);
let on = row
.iter()
.filter(|&&v| v as f64 > res.thresholds[0])
.count();
assert_eq!(on, 200);
}
#[test]
fn test_threshold_falls_back_on_unimodal() {
let row = unimodal_row();
let res = derive_regulon_thresholds(std::slice::from_ref(&row), None).unwrap();
assert!(!res.bimodal[0]);
let n = row.len() as f64;
let mean = row.iter().map(|&v| v as f64).sum::<f64>() / n;
let sd = (row.iter().map(|&v| (v as f64 - mean).powi(2)).sum::<f64>() / (n - 1.0)).sqrt();
approx::assert_relative_eq!(res.thresholds[0], mean + 2.0 * sd, epsilon = 1e-9);
}
#[test]
fn test_threshold_is_reproducible() {
let rows = vec![bimodal_row(), unimodal_row()];
let first = derive_regulon_thresholds(&rows, None).unwrap();
let second = derive_regulon_thresholds(&rows, None).unwrap();
assert_eq!(first.thresholds, second.thresholds);
assert_eq!(first.bimodal, second.bimodal);
}
#[test]
fn test_threshold_constant_row_calls_nothing_on() {
let row = vec![0.42_f32; 100];
let res = derive_regulon_thresholds(std::slice::from_ref(&row), None).unwrap();
assert!(!res.bimodal[0]);
assert!(row.iter().all(|&v| (v as f64) <= res.thresholds[0]));
}
#[test]
fn test_threshold_rejects_empty_input() {
assert!(derive_regulon_thresholds(&[], None).is_err());
}
#[test]
fn test_two_component_recovers_known_means() {
let row = bimodal_row();
let x: Vec<f64> = row.iter().map(|&v| v as f64).collect();
let n = x.len() as f64;
let mean = x.iter().sum::<f64>() / n;
let variance = x.iter().map(|v| (v - mean).powi(2)).sum::<f64>() / (n - 1.0);
let mut sorted = x.clone();
sorted.sort_unstable_by(|a, b| a.total_cmp(b));
let fit = fit_two_component(&x, &sorted, mean, variance).unwrap();
let lo = fit.mu[0].min(fit.mu[1]);
let hi = fit.mu[0].max(fit.mu[1]);
approx::assert_relative_eq!(lo, 0.1, epsilon = 1e-3);
approx::assert_relative_eq!(hi, 0.6, epsilon = 1e-3);
}
}