use crate::{Error, Result};
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum Threshold {
BicLike { c: f64 },
Lil { c: f64 },
MaxOfK { c: f64 },
Fixed(f64),
Gumbel { alpha: f64 },
}
impl Default for Threshold {
fn default() -> Self {
Threshold::BicLike { c: 1.5 }
}
}
impl Threshold {
pub fn validate(self) -> Result<()> {
let ok = match self {
Threshold::BicLike { c } | Threshold::Lil { c } => c.is_finite() && c > 0.0,
Threshold::MaxOfK { c } => c.is_finite() && c >= 0.0,
Threshold::Fixed(v) => v.is_finite() && v >= 0.0,
Threshold::Gumbel { alpha } => alpha.is_finite() && alpha > 0.0 && alpha < 1.0,
};
if ok {
Ok(())
} else {
Err(Error::Invalid(
"threshold constant must be finite and positive (BicLike/Lil) or non-negative (MaxOfK/Fixed)",
))
}
}
pub fn value(self, n: usize, p_n: usize) -> f64 {
let nf = (n.max(3)) as f64;
match self {
Threshold::BicLike { c } => (c * nf.ln()).sqrt(),
Threshold::Lil { c } => (2.0 * c * nf.ln().ln().max(0.0)).sqrt(),
Threshold::MaxOfK { c } => (2.0 * (p_n.max(2) as f64).ln()).sqrt() + c,
Threshold::Fixed(v) => v,
Threshold::Gumbel { alpha } => {
let m = (2 * p_n.max(2)) as f64;
let l = (2.0 * m.ln()).sqrt();
let a = 1.0 / l;
let b = l - (m.ln().ln() + (4.0 * core::f64::consts::PI).ln()) / (2.0 * l);
b + a * (-(-(1.0 - alpha).ln()).ln())
}
}
}
pub fn name(self) -> String {
match self {
Threshold::BicLike { c } => format!("bic(c={c})"),
Threshold::Lil { c } => format!("lil(c={c})"),
Threshold::MaxOfK { c } => format!("maxk(c={c})"),
Threshold::Fixed(v) => format!("fixed({v})"),
Threshold::Gumbel { alpha } => format!("gumbel(alpha={alpha})"),
}
}
}
pub fn polylog_memory(n: usize, c: f64) -> usize {
let p = (c * (n.max(3) as f64).ln()).ceil() as usize;
p.clamp(2, (n / 4).max(2))
}
#[derive(Debug, Clone, Copy, PartialEq, Default)]
pub enum Memory {
#[default]
Default,
PowerLaw { alpha: f64 },
PolyLog { c: f64 },
Fixed(usize),
}
impl Memory {
pub fn resolve(&self, n: usize, statistic_default: usize) -> usize {
match self {
Memory::Default => statistic_default,
Memory::PowerLaw { alpha } => {
let p = (n as f64).powf(*alpha).floor() as usize;
p.clamp(2, (n / 4).max(2))
}
Memory::PolyLog { c } => polylog_memory(n, *c),
Memory::Fixed(k) => *k,
}
}
}
pub fn power_memory(n: usize) -> usize {
let p = (n as f64).powf(0.4).floor() as usize;
p.clamp(2, (n / 4).max(2))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn invalid_constants_rejected() {
assert!(Threshold::BicLike { c: -1.0 }.validate().is_err());
assert!(Threshold::BicLike { c: 0.0 }.validate().is_err());
assert!(Threshold::Lil { c: f64::NAN }.validate().is_err());
assert!(Threshold::Fixed(-5.0).validate().is_err());
assert!(Threshold::Fixed(1.96).validate().is_ok());
assert!(Threshold::MaxOfK { c: 0.0 }.validate().is_ok());
assert!(Threshold::Gumbel { alpha: 0.0 }.validate().is_err());
assert!(Threshold::Gumbel { alpha: 1.0 }.validate().is_err());
assert!(Threshold::Gumbel { alpha: 0.05 }.validate().is_ok());
}
#[test]
fn gumbel_threshold_is_sane() {
let t10 = Threshold::Gumbel { alpha: 0.05 }.value(1000, 10);
assert!(t10 > 2.3 && t10 < 3.3, "{t10}");
assert!(Threshold::Gumbel { alpha: 0.05 }.value(1000, 100) > t10);
assert!(Threshold::Gumbel { alpha: 0.001 }.value(1000, 10) > t10);
}
#[test]
fn memory_rules_resolve() {
assert_eq!(Memory::Default.resolve(1000, 14), 14);
assert_eq!(
Memory::PowerLaw { alpha: 0.4 }.resolve(1000, 14),
power_memory(1000)
);
assert_eq!(
Memory::PolyLog { c: 2.0 }.resolve(1000, 14),
polylog_memory(1000, 2.0)
);
assert_eq!(Memory::Fixed(7).resolve(1000, 14), 7);
assert_eq!(Memory::default(), Memory::Default);
}
#[test]
fn polylog_memory_grows_slowly() {
assert_eq!(polylog_memory(100, 2.0), 10);
assert!(polylog_memory(10_000, 2.0) < power_memory(10_000));
assert!(polylog_memory(10, 2.0) >= 2);
}
}