const STEEPNESS: f64 = 2.0;
fn logistic(x: f64) -> f64 {
1.0 / (1.0 + (-x).exp())
}
pub fn from_exceedance(statistic: f64, threshold: f64) -> f64 {
if threshold <= 0.0 {
return if statistic > 0.0 { 1.0 } else { 0.5 };
}
logistic(STEEPNESS * (statistic / threshold - 1.0))
}
pub fn from_undercut(statistic: f64, threshold: f64) -> f64 {
if statistic <= 0.0 {
return 1.0;
}
if threshold <= 0.0 {
return 0.0;
}
logistic(STEEPNESS * (threshold / statistic - 1.0))
}
#[cfg(test)]
mod tests {
use super::*;
const TWO_X: f64 = 0.8807970779778823;
#[test]
fn at_threshold_is_one_half() {
assert_eq!(from_exceedance(3.5, 3.5), 0.5);
assert_eq!(from_exceedance(0.2, 0.2), 0.5);
assert_eq!(from_undercut(0.05, 0.05), 0.5);
assert_eq!(from_undercut(0.001, 0.001), 0.5);
}
#[test]
fn exceedance_rises_above_threshold_falls_below() {
assert!(from_exceedance(7.0, 3.5) > 0.5); assert!(from_exceedance(2.0, 3.5) < 0.5); assert!(from_exceedance(10.0, 3.5) > from_exceedance(5.0, 3.5)); }
#[test]
fn undercut_rises_as_statistic_shrinks() {
assert!(from_undercut(0.025, 0.05) > 0.5); assert!(from_undercut(0.10, 0.05) < 0.5); assert!(from_undercut(0.001, 0.05) > from_undercut(0.01, 0.05)); }
#[test]
fn boundaries_are_saturated_not_nan() {
assert_eq!(from_undercut(0.0, 0.05), 1.0); assert_eq!(from_undercut(0.0, 0.0), 1.0); assert_eq!(from_exceedance(1.0, 0.0), 1.0); assert_eq!(from_exceedance(0.0, 0.0), 0.5);
assert_eq!(from_undercut(0.5, 0.0), 0.0);
}
#[test]
fn two_x_past_threshold_is_identical_across_directions_and_units() {
let a = from_exceedance(7.0, 3.5); let b = from_exceedance(0.4, 0.2); let c = from_undercut(0.025, 0.05); assert!((a - TWO_X).abs() < 1e-12);
assert!((b - TWO_X).abs() < 1e-12);
assert!((c - TWO_X).abs() < 1e-12);
}
#[test]
fn stays_in_unit_interval() {
for &(s, t) in &[(1e9, 3.5), (3.5, 3.5), (0.0, 3.5)] {
let c = from_exceedance(s, t);
assert!((0.0..=1.0).contains(&c));
}
for &(s, t) in &[(1e-12, 0.05), (0.05, 0.05), (1.0, 0.05)] {
let c = from_undercut(s, t);
assert!((0.0..=1.0).contains(&c));
}
}
}