1use crate::noncentral::{nct_cdf, nct_sf};
8use crate::weightstats::Alternative;
9use solow_distributions::{norm_cdf, norm_isf, norm_ppf, norm_sf, t_isf, t_ppf};
10
11fn alpha_tail(alpha: f64, alternative: Alternative) -> f64 {
13 match alternative {
14 Alternative::TwoSided => alpha / 2.0,
15 Alternative::Larger | Alternative::Smaller => alpha,
16 }
17}
18
19#[derive(Debug, Clone, Copy, Default)]
26pub struct TTestPower;
27
28impl TTestPower {
29 pub fn power(
31 &self,
32 effect_size: f64,
33 nobs: f64,
34 alpha: f64,
35 df: Option<f64>,
36 alternative: Alternative,
37 ) -> f64 {
38 let d = effect_size;
39 let df = df.unwrap_or(nobs - 1.0);
40 let alpha_ = alpha_tail(alpha, alternative);
41 let nc = d * nobs.sqrt();
42 let mut pow_ = 0.0;
43 if matches!(alternative, Alternative::TwoSided | Alternative::Larger) {
44 let crit_upp = t_isf(alpha_, df);
45 pow_ += nct_sf(crit_upp, df, nc);
46 }
47 if matches!(alternative, Alternative::TwoSided | Alternative::Smaller) {
48 let crit_low = t_ppf(alpha_, df);
49 pow_ += nct_cdf(crit_low, df, nc);
50 }
51 pow_
52 }
53
54 pub fn solve_power(
58 &self,
59 effect_size: f64,
60 alpha: f64,
61 power: f64,
62 alternative: Alternative,
63 ) -> f64 {
64 let f = |n: f64| self.power(effect_size, n, alpha, None, alternative) - power;
65 solve_nobs(f, 2.000_001, 1.0e7)
66 }
67}
68
69#[derive(Debug, Clone, Copy, Default)]
76pub struct NormalIndPower;
77
78impl NormalIndPower {
79 pub fn power(
81 &self,
82 effect_size: f64,
83 nobs1: f64,
84 alpha: f64,
85 ratio: f64,
86 alternative: Alternative,
87 ) -> f64 {
88 let ddof = 0.0;
89 let nobs = if ratio > 0.0 {
90 let nobs2 = nobs1 * ratio;
91 1.0 / (1.0 / (nobs1 - ddof) + 1.0 / (nobs2 - ddof))
92 } else {
93 nobs1 - ddof
94 };
95 normal_power(effect_size, nobs, alpha, alternative)
96 }
97
98 pub fn solve_power(
100 &self,
101 effect_size: f64,
102 alpha: f64,
103 power: f64,
104 ratio: f64,
105 alternative: Alternative,
106 ) -> f64 {
107 let f = |n: f64| self.power(effect_size, n, alpha, ratio, alternative) - power;
108 solve_nobs(f, 1.000_001, 1.0e7)
109 }
110}
111
112fn normal_power(effect_size: f64, nobs: f64, alpha: f64, alternative: Alternative) -> f64 {
114 let d = effect_size;
115 let alpha_ = alpha_tail(alpha, alternative);
116 let mut pow_ = 0.0;
117 if matches!(alternative, Alternative::TwoSided | Alternative::Larger) {
118 let crit = norm_isf(alpha_);
119 pow_ += norm_sf(crit - d * nobs.sqrt());
120 }
121 if matches!(alternative, Alternative::TwoSided | Alternative::Smaller) {
122 let crit = norm_ppf(alpha_);
123 pow_ += norm_cdf(crit - d * nobs.sqrt());
124 }
125 pow_
126}
127
128fn solve_nobs<F: Fn(f64) -> f64>(f: F, lo0: f64, hi0: f64) -> f64 {
133 let mut lo = lo0;
134 let mut hi = hi0;
135 let flo = f(lo);
136 let fhi = f(hi);
137 if flo * fhi > 0.0 {
139 return if flo.abs() < fhi.abs() { lo } else { hi };
141 }
142 for _ in 0..200 {
143 let mid = 0.5 * (lo + hi);
144 let fm = f(mid);
145 if fm == 0.0 {
146 return mid;
147 }
148 if (flo < 0.0) == (fm < 0.0) {
149 lo = mid;
150 } else {
151 hi = mid;
152 }
153 if hi - lo <= 1e-12 * (1.0 + hi) {
154 break;
155 }
156 }
157 0.5 * (lo + hi)
158}
159
160#[cfg(test)]
161mod tests {
162 use super::*;
163
164 #[test]
165 fn ttest_power_in_unit_interval() {
166 let tt = TTestPower;
167 let p = tt.power(0.5, 30.0, 0.05, None, Alternative::TwoSided);
168 assert!(p > 0.0 && p < 1.0);
169 }
170
171 #[test]
172 fn solve_power_roundtrips() {
173 let tt = TTestPower;
174 let n = tt.solve_power(0.5, 0.05, 0.8, Alternative::TwoSided);
175 let p = tt.power(0.5, n, 0.05, None, Alternative::TwoSided);
176 assert!((p - 0.8).abs() < 1e-6, "{p}");
177 }
178
179 #[test]
180 fn normal_power_solve_roundtrips() {
181 let nip = NormalIndPower;
182 let n = nip.solve_power(0.5, 0.05, 0.8, 1.0, Alternative::TwoSided);
183 let p = nip.power(0.5, n, 0.05, 1.0, Alternative::TwoSided);
184 assert!((p - 0.8).abs() < 1e-9, "{p}");
185 }
186}