jugar_probar/coverage/
hypotheses.rs1#[derive(Debug, Clone)]
11pub struct NullificationConfig {
12 pub runs: usize,
14 pub alpha: f64,
16}
17
18impl NullificationConfig {
19 #[must_use]
21 pub fn princeton() -> Self {
22 Self {
23 runs: 5,
24 alpha: 0.05,
25 }
26 }
27
28 #[must_use]
30 pub fn new(runs: usize, alpha: f64) -> Self {
31 Self { runs, alpha }
32 }
33}
34
35impl Default for NullificationConfig {
36 fn default() -> Self {
37 Self::princeton()
38 }
39}
40
41#[derive(Debug, Clone)]
43pub struct NullificationResult {
44 pub hypothesis_name: String,
46 pub rejected: bool,
48 pub p_value: f64,
50 pub effect_size: f64,
52 pub confidence_interval: (f64, f64),
54}
55
56impl NullificationResult {
57 #[must_use]
59 pub fn is_significant(&self) -> bool {
60 self.p_value < 0.05
61 }
62
63 #[must_use]
65 pub fn report(&self) -> String {
66 let status = if self.rejected {
67 "REJECTED"
68 } else {
69 "NOT REJECTED"
70 };
71 format!(
72 "{}: {} (p={:.3}, 95% CI [{:.1}, {:.1}], d={:.2})",
73 self.hypothesis_name,
74 status,
75 self.p_value,
76 self.confidence_interval.0,
77 self.confidence_interval.1,
78 self.effect_size
79 )
80 }
81}
82
83#[derive(Debug, Clone)]
85pub enum CoverageHypothesis {
86 Determinism,
88 Completeness {
90 threshold: f64,
92 },
93 NoRegression {
95 baseline: f64,
97 },
98 MutationCorrelation {
100 expected_r: f64,
102 },
103}
104
105impl CoverageHypothesis {
106 #[must_use]
108 pub fn determinism() -> Self {
109 Self::Determinism
110 }
111
112 #[must_use]
114 pub fn completeness(threshold: f64) -> Self {
115 Self::Completeness { threshold }
116 }
117
118 #[must_use]
120 pub fn no_regression(baseline: f64) -> Self {
121 Self::NoRegression { baseline }
122 }
123
124 #[must_use]
126 pub fn mutation_correlation(expected_r: f64) -> Self {
127 Self::MutationCorrelation { expected_r }
128 }
129
130 #[must_use]
132 pub fn name(&self) -> &'static str {
133 match self {
134 Self::Determinism => "H0-COV-01",
135 Self::Completeness { .. } => "H0-COV-02",
136 Self::NoRegression { .. } => "H0-COV-03",
137 Self::MutationCorrelation { .. } => "H0-COV-04",
138 }
139 }
140
141 #[must_use]
146 pub fn evaluate(&self, observations: &[f64]) -> NullificationResult {
147 if observations.is_empty() {
148 return NullificationResult {
149 hypothesis_name: self.name().to_string(),
150 rejected: true,
151 p_value: 0.0,
152 effect_size: f64::INFINITY,
153 confidence_interval: (0.0, 0.0),
154 };
155 }
156
157 match self {
158 Self::Determinism => self.evaluate_determinism(observations),
159 Self::Completeness { threshold } => {
160 self.evaluate_completeness(observations, *threshold)
161 }
162 Self::NoRegression { baseline } => self.evaluate_no_regression(observations, *baseline),
163 Self::MutationCorrelation { expected_r } => {
164 self.evaluate_mutation_correlation(observations, *expected_r)
165 }
166 }
167 }
168
169 fn evaluate_determinism(&self, observations: &[f64]) -> NullificationResult {
171 let mean = observations.iter().sum::<f64>() / observations.len() as f64;
172 let variance = observations.iter().map(|x| (x - mean).powi(2)).sum::<f64>()
173 / observations.len() as f64;
174
175 let rejected = variance > 0.01; let p_value = if rejected { 0.01 } else { 0.5 };
178
179 NullificationResult {
180 hypothesis_name: self.name().to_string(),
181 rejected,
182 p_value,
183 effect_size: variance.sqrt(),
184 confidence_interval: (mean - 2.0 * variance.sqrt(), mean + 2.0 * variance.sqrt()),
185 }
186 }
187
188 fn evaluate_completeness(&self, observations: &[f64], threshold: f64) -> NullificationResult {
190 let mean = observations.iter().sum::<f64>() / observations.len() as f64;
191 let std_dev = (observations.iter().map(|x| (x - mean).powi(2)).sum::<f64>()
192 / observations.len() as f64)
193 .sqrt();
194
195 let t_stat = (mean - threshold) / (std_dev / (observations.len() as f64).sqrt());
197
198 let rejected = mean < threshold;
200 let p_value = if rejected { 0.01 } else { 0.5 };
201
202 let margin = 1.96 * std_dev / (observations.len() as f64).sqrt();
203 NullificationResult {
204 hypothesis_name: self.name().to_string(),
205 rejected,
206 p_value,
207 effect_size: t_stat.abs(),
208 confidence_interval: (mean - margin, mean + margin),
209 }
210 }
211
212 fn evaluate_no_regression(&self, observations: &[f64], baseline: f64) -> NullificationResult {
214 let mean = observations.iter().sum::<f64>() / observations.len() as f64;
215 let std_dev = (observations.iter().map(|x| (x - mean).powi(2)).sum::<f64>()
216 / observations.len() as f64)
217 .sqrt();
218
219 let rejected = mean < baseline;
221 let p_value = if rejected { 0.01 } else { 0.5 };
222
223 let effect_size = if std_dev > 0.0 {
224 (baseline - mean) / std_dev
225 } else {
226 0.0
227 };
228
229 let margin = 1.96 * std_dev / (observations.len() as f64).sqrt();
230 NullificationResult {
231 hypothesis_name: self.name().to_string(),
232 rejected,
233 p_value,
234 effect_size,
235 confidence_interval: (mean - margin, mean + margin),
236 }
237 }
238
239 fn evaluate_mutation_correlation(
245 &self,
246 observations: &[f64],
247 expected_r: f64,
248 ) -> NullificationResult {
249 let mean = observations.iter().sum::<f64>() / observations.len() as f64;
252 let std_dev = (observations.iter().map(|x| (x - mean).powi(2)).sum::<f64>()
253 / observations.len() as f64)
254 .sqrt();
255
256 let estimated_r = mean / 100.0;
258 let rejected = estimated_r < expected_r;
259 let p_value = if rejected { 0.01 } else { 0.5 };
260
261 let margin = 1.96 * std_dev / (observations.len() as f64).sqrt();
262 NullificationResult {
263 hypothesis_name: self.name().to_string(),
264 rejected,
265 p_value,
266 effect_size: (expected_r - estimated_r).abs(),
267 confidence_interval: (mean - margin, mean + margin),
268 }
269 }
270}