use crate::noncentral::{nct_cdf, nct_sf};
use crate::weightstats::Alternative;
use solow_distributions::{norm_cdf, norm_isf, norm_ppf, norm_sf, t_isf, t_ppf};
fn alpha_tail(alpha: f64, alternative: Alternative) -> f64 {
match alternative {
Alternative::TwoSided => alpha / 2.0,
Alternative::Larger | Alternative::Smaller => alpha,
}
}
#[derive(Debug, Clone, Copy, Default)]
pub struct TTestPower;
impl TTestPower {
pub fn power(
&self,
effect_size: f64,
nobs: f64,
alpha: f64,
df: Option<f64>,
alternative: Alternative,
) -> f64 {
let d = effect_size;
let df = df.unwrap_or(nobs - 1.0);
let alpha_ = alpha_tail(alpha, alternative);
let nc = d * nobs.sqrt();
let mut pow_ = 0.0;
if matches!(alternative, Alternative::TwoSided | Alternative::Larger) {
let crit_upp = t_isf(alpha_, df);
pow_ += nct_sf(crit_upp, df, nc);
}
if matches!(alternative, Alternative::TwoSided | Alternative::Smaller) {
let crit_low = t_ppf(alpha_, df);
pow_ += nct_cdf(crit_low, df, nc);
}
pow_
}
pub fn solve_power(
&self,
effect_size: f64,
alpha: f64,
power: f64,
alternative: Alternative,
) -> f64 {
let f = |n: f64| self.power(effect_size, n, alpha, None, alternative) - power;
solve_nobs(f, 2.000_001, 1.0e7)
}
}
#[derive(Debug, Clone, Copy, Default)]
pub struct NormalIndPower;
impl NormalIndPower {
pub fn power(
&self,
effect_size: f64,
nobs1: f64,
alpha: f64,
ratio: f64,
alternative: Alternative,
) -> f64 {
let ddof = 0.0;
let nobs = if ratio > 0.0 {
let nobs2 = nobs1 * ratio;
1.0 / (1.0 / (nobs1 - ddof) + 1.0 / (nobs2 - ddof))
} else {
nobs1 - ddof
};
normal_power(effect_size, nobs, alpha, alternative)
}
pub fn solve_power(
&self,
effect_size: f64,
alpha: f64,
power: f64,
ratio: f64,
alternative: Alternative,
) -> f64 {
let f = |n: f64| self.power(effect_size, n, alpha, ratio, alternative) - power;
solve_nobs(f, 1.000_001, 1.0e7)
}
}
fn normal_power(effect_size: f64, nobs: f64, alpha: f64, alternative: Alternative) -> f64 {
let d = effect_size;
let alpha_ = alpha_tail(alpha, alternative);
let mut pow_ = 0.0;
if matches!(alternative, Alternative::TwoSided | Alternative::Larger) {
let crit = norm_isf(alpha_);
pow_ += norm_sf(crit - d * nobs.sqrt());
}
if matches!(alternative, Alternative::TwoSided | Alternative::Smaller) {
let crit = norm_ppf(alpha_);
pow_ += norm_cdf(crit - d * nobs.sqrt());
}
pow_
}
fn solve_nobs<F: Fn(f64) -> f64>(f: F, lo0: f64, hi0: f64) -> f64 {
let mut lo = lo0;
let mut hi = hi0;
let flo = f(lo);
let fhi = f(hi);
if flo * fhi > 0.0 {
return if flo.abs() < fhi.abs() { lo } else { hi };
}
for _ in 0..200 {
let mid = 0.5 * (lo + hi);
let fm = f(mid);
if fm == 0.0 {
return mid;
}
if (flo < 0.0) == (fm < 0.0) {
lo = mid;
} else {
hi = mid;
}
if hi - lo <= 1e-12 * (1.0 + hi) {
break;
}
}
0.5 * (lo + hi)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn ttest_power_in_unit_interval() {
let tt = TTestPower;
let p = tt.power(0.5, 30.0, 0.05, None, Alternative::TwoSided);
assert!(p > 0.0 && p < 1.0);
}
#[test]
fn solve_power_roundtrips() {
let tt = TTestPower;
let n = tt.solve_power(0.5, 0.05, 0.8, Alternative::TwoSided);
let p = tt.power(0.5, n, 0.05, None, Alternative::TwoSided);
assert!((p - 0.8).abs() < 1e-6, "{p}");
}
#[test]
fn normal_power_solve_roundtrips() {
let nip = NormalIndPower;
let n = nip.solve_power(0.5, 0.05, 0.8, 1.0, Alternative::TwoSided);
let p = nip.power(0.5, n, 0.05, 1.0, Alternative::TwoSided);
assert!((p - 0.8).abs() < 1e-9, "{p}");
}
}