use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum NHSTResult {
Rejected {
p_value: f64,
effect_size: f64,
test_statistic: f64,
},
NotRejected {
p_value: f64,
power: f64,
},
}
impl NHSTResult {
#[must_use]
pub const fn is_rejected(&self) -> bool {
matches!(self, Self::Rejected { .. })
}
#[must_use]
pub const fn p_value(&self) -> f64 {
match self {
Self::Rejected { p_value, .. } | Self::NotRejected { p_value, .. } => *p_value,
}
}
}
#[derive(Debug, Clone, Default)]
pub struct Predictions {
pub values: Vec<f64>,
pub uncertainties: Vec<f64>,
}
impl Predictions {
#[must_use]
pub fn new() -> Self {
Self::default()
}
pub fn add(&mut self, value: f64, uncertainty: f64) {
self.values.push(value);
self.uncertainties.push(uncertainty);
}
#[must_use]
pub fn len(&self) -> usize {
self.values.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.values.is_empty()
}
}
#[derive(Debug, Clone, Default)]
pub struct Observations {
pub values: Vec<f64>,
pub uncertainties: Vec<f64>,
}
impl Observations {
#[must_use]
pub fn new() -> Self {
Self::default()
}
pub fn add(&mut self, value: f64, uncertainty: f64) {
self.values.push(value);
self.uncertainties.push(uncertainty);
}
#[must_use]
pub fn len(&self) -> usize {
self.values.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.values.is_empty()
}
}
#[derive(Debug, Clone)]
pub struct FalsificationCriteria {
pub metric: String,
pub operator: ComparisonOp,
pub threshold: f64,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ComparisonOp {
Lt,
Le,
Gt,
Ge,
Eq,
Ne,
}
impl FalsificationCriteria {
#[must_use]
pub fn check(&self, value: f64) -> bool {
match self.operator {
ComparisonOp::Lt => value < self.threshold,
ComparisonOp::Le => value <= self.threshold,
ComparisonOp::Gt => value > self.threshold,
ComparisonOp::Ge => value >= self.threshold,
ComparisonOp::Eq => (value - self.threshold).abs() < f64::EPSILON,
ComparisonOp::Ne => (value - self.threshold).abs() >= f64::EPSILON,
}
}
#[must_use]
pub fn less_than(metric: impl Into<String>, threshold: f64) -> Self {
Self {
metric: metric.into(),
operator: ComparisonOp::Lt,
threshold,
}
}
#[must_use]
pub fn greater_than(metric: impl Into<String>, threshold: f64) -> Self {
Self {
metric: metric.into(),
operator: ComparisonOp::Gt,
threshold,
}
}
}
pub trait FalsifiableHypothesis {
type State;
fn predict(&self, state: &Self::State) -> Predictions;
fn falsification_criteria(&self) -> Vec<FalsificationCriteria>;
fn robustness(&self, state: &Self::State) -> f64;
fn null_hypothesis_test(
&self,
predictions: &Predictions,
observations: &Observations,
significance: f64,
) -> NHSTResult;
}
pub mod stats {
use super::NHSTResult;
#[must_use]
pub fn mean(values: &[f64]) -> f64 {
if values.is_empty() {
return 0.0;
}
values.iter().sum::<f64>() / values.len() as f64
}
#[must_use]
pub fn variance(values: &[f64]) -> f64 {
if values.len() < 2 {
return 0.0;
}
let m = mean(values);
let sum_sq: f64 = values.iter().map(|x| (x - m).powi(2)).sum();
sum_sq / (values.len() - 1) as f64
}
#[must_use]
pub fn std_dev(values: &[f64]) -> f64 {
variance(values).sqrt()
}
#[must_use]
pub fn std_error(values: &[f64]) -> f64 {
if values.is_empty() {
return 0.0;
}
std_dev(values) / (values.len() as f64).sqrt()
}
#[must_use]
pub fn one_sample_t_test(values: &[f64], mu_0: f64, significance: f64) -> NHSTResult {
if values.len() < 2 {
return NHSTResult::NotRejected {
p_value: 1.0,
power: 0.0,
};
}
let n = values.len() as f64;
let sample_mean = mean(values);
let sample_std = std_dev(values);
if sample_std < f64::EPSILON {
return NHSTResult::NotRejected {
p_value: 1.0,
power: 0.0,
};
}
let t_stat = (sample_mean - mu_0) / (sample_std / n.sqrt());
let _df = n - 1.0;
let p_value = 2.0 * (1.0 - normal_cdf(t_stat.abs()));
let effect_size = (sample_mean - mu_0) / sample_std;
if p_value < significance {
NHSTResult::Rejected {
p_value,
effect_size: effect_size.abs(),
test_statistic: t_stat,
}
} else {
let power = 1.0 - normal_cdf(1.96 - effect_size.abs() * n.sqrt());
NHSTResult::NotRejected {
p_value,
power: power.clamp(0.0, 1.0),
}
}
}
#[must_use]
pub fn two_sample_t_test(sample1: &[f64], sample2: &[f64], significance: f64) -> NHSTResult {
if sample1.len() < 2 || sample2.len() < 2 {
return NHSTResult::NotRejected {
p_value: 1.0,
power: 0.0,
};
}
let n1 = sample1.len() as f64;
let n2 = sample2.len() as f64;
let mean1 = mean(sample1);
let mean2 = mean(sample2);
let var1 = variance(sample1);
let var2 = variance(sample2);
let se = (var1 / n1 + var2 / n2).sqrt();
if se < f64::EPSILON {
return NHSTResult::NotRejected {
p_value: 1.0,
power: 0.0,
};
}
let t_stat = (mean1 - mean2) / se;
let p_value = 2.0 * (1.0 - normal_cdf(t_stat.abs()));
let pooled_std = ((var1 + var2) / 2.0).sqrt();
let effect_size = if pooled_std > f64::EPSILON {
(mean1 - mean2) / pooled_std
} else {
0.0
};
if p_value < significance {
NHSTResult::Rejected {
p_value,
effect_size: effect_size.abs(),
test_statistic: t_stat,
}
} else {
NHSTResult::NotRejected {
p_value,
power: 0.5, }
}
}
#[must_use]
pub fn chi_square_test(observed: &[f64], expected: &[f64], significance: f64) -> NHSTResult {
if observed.len() != expected.len() || observed.is_empty() {
return NHSTResult::NotRejected {
p_value: 1.0,
power: 0.0,
};
}
let chi_sq: f64 = observed
.iter()
.zip(expected)
.filter(|(_, &e)| e > f64::EPSILON)
.map(|(&o, &e)| (o - e).powi(2) / e)
.sum();
let df = (observed.len() - 1) as f64;
let p_value = 1.0 - chi_square_cdf(chi_sq, df);
if p_value < significance {
NHSTResult::Rejected {
p_value,
effect_size: (chi_sq / df).sqrt(), test_statistic: chi_sq,
}
} else {
NHSTResult::NotRejected {
p_value,
power: 0.5,
}
}
}
fn normal_cdf(x: f64) -> f64 {
0.5 * (1.0 + erf(x / std::f64::consts::SQRT_2))
}
#[must_use]
pub fn normal_cdf_pub(x: f64) -> f64 {
normal_cdf(x)
}
fn chi_square_cdf(x: f64, df: f64) -> f64 {
if x <= 0.0 {
return 0.0;
}
let z = (x / df).powf(1.0 / 3.0) - (1.0 - 2.0 / (9.0 * df));
let se = (2.0 / (9.0 * df)).sqrt();
normal_cdf(z / se)
}
fn erf(x: f64) -> f64 {
let a1 = 0.254_829_592;
let a2 = -0.284_496_736;
let a3 = 1.421_413_741;
let a4 = -1.453_152_027;
let a5 = 1.061_405_429;
let p = 0.327_591_1;
let sign = if x < 0.0 { -1.0 } else { 1.0 };
let x = x.abs();
let t = 1.0 / (1.0 + p * x);
let y = 1.0 - (((((a5 * t + a4) * t) + a3) * t + a2) * t + a1) * t * (-x * x).exp();
sign * y
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct NullificationResult {
pub hypothesis_id: String,
pub rejected: bool,
pub p_value: f64,
pub confidence_interval: (f64, f64),
pub effect_size: f64,
pub observations: Vec<f64>,
}
impl NullificationResult {
#[must_use]
pub fn report(&self) -> String {
format!(
"{}: {} (p={:.4}, 95% CI [{:.4}, {:.4}], d={:.2})",
self.hypothesis_id,
if self.rejected {
"REJECTED"
} else {
"NOT REJECTED"
},
self.p_value,
self.confidence_interval.0,
self.confidence_interval.1,
self.effect_size,
)
}
}
#[derive(Debug, Clone)]
pub struct NullificationTest {
pub hypothesis_id: String,
pub n_runs: usize,
pub alpha: f64,
pub bootstrap_samples: usize,
pub expected_value: f64,
}
impl NullificationTest {
#[must_use]
pub fn new(hypothesis_id: impl Into<String>) -> Self {
Self {
hypothesis_id: hypothesis_id.into(),
n_runs: 5,
alpha: 0.05,
bootstrap_samples: 10_000,
expected_value: 0.0,
}
}
#[must_use]
pub fn with_runs(mut self, n_runs: usize) -> Self {
self.n_runs = n_runs.max(5);
self
}
#[must_use]
pub fn with_alpha(mut self, alpha: f64) -> Self {
self.alpha = alpha.clamp(0.001, 0.1);
self
}
#[must_use]
pub fn with_expected(mut self, expected: f64) -> Self {
self.expected_value = expected;
self
}
pub fn execute<F>(&self, test_fn: F) -> NullificationResult
where
F: Fn() -> f64,
{
let observations: Vec<f64> = (0..self.n_runs).map(|_| test_fn()).collect();
let ci = self.bootstrap_ci(&observations);
let (p_value, effect_size) = self.compute_stats(&observations);
NullificationResult {
hypothesis_id: self.hypothesis_id.clone(),
rejected: p_value < self.alpha,
p_value,
confidence_interval: ci,
effect_size,
observations,
}
}
#[must_use]
pub fn evaluate(&self, observations: &[f64]) -> NullificationResult {
let ci = self.bootstrap_ci(observations);
let (p_value, effect_size) = self.compute_stats(observations);
NullificationResult {
hypothesis_id: self.hypothesis_id.clone(),
rejected: p_value < self.alpha,
p_value,
confidence_interval: ci,
effect_size,
observations: observations.to_vec(),
}
}
fn bootstrap_ci(&self, observations: &[f64]) -> (f64, f64) {
if observations.is_empty() {
return (0.0, 0.0);
}
let n = observations.len();
let mut bootstrap_means = Vec::with_capacity(self.bootstrap_samples.min(1000));
let mut lcg_state = 12345_u64;
for _ in 0..self.bootstrap_samples.min(1000) {
let mut sum = 0.0;
for _ in 0..n {
lcg_state = lcg_state
.wrapping_mul(6_364_136_223_846_793_005)
.wrapping_add(1);
let idx = (lcg_state as usize) % n;
sum += observations[idx];
}
bootstrap_means.push(sum / n as f64);
}
bootstrap_means.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
let lower_idx = (bootstrap_means.len() as f64 * 0.025) as usize;
let upper_idx = (bootstrap_means.len() as f64 * 0.975) as usize;
let lower = bootstrap_means.get(lower_idx).copied().unwrap_or(0.0);
let upper = bootstrap_means
.get(upper_idx.min(bootstrap_means.len() - 1))
.copied()
.unwrap_or(0.0);
(lower, upper)
}
fn compute_stats(&self, observations: &[f64]) -> (f64, f64) {
if observations.len() < 2 {
return (1.0, 0.0);
}
let mean = stats::mean(observations);
let std = stats::std_dev(observations);
let n = observations.len() as f64;
if std < f64::EPSILON {
let p_value = if (mean - self.expected_value).abs() < f64::EPSILON {
1.0 } else {
0.0 };
return (p_value, 0.0);
}
let t_stat = (mean - self.expected_value) / (std / n.sqrt());
let p_value = 2.0 * (1.0 - stats::normal_cdf_pub(t_stat.abs()));
let effect_size = (mean - self.expected_value).abs() / std;
(p_value.clamp(0.0, 1.0), effect_size)
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct NullificationReport {
pub results: Vec<NullificationResult>,
pub summary: ReportSummary,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct ReportSummary {
pub total_tests: usize,
pub rejected: usize,
pub not_rejected: usize,
pub status: String,
}
impl NullificationReport {
#[must_use]
pub fn new() -> Self {
Self::default()
}
pub fn add(&mut self, result: NullificationResult) {
if result.rejected {
self.summary.rejected += 1;
} else {
self.summary.not_rejected += 1;
}
self.summary.total_tests += 1;
self.results.push(result);
self.summary.status = if self.summary.rejected == 0 {
"PASS".to_string()
} else if self.summary.rejected == self.summary.total_tests {
"FAIL".to_string()
} else {
"PARTIAL_PASS".to_string()
};
}
#[must_use]
pub fn full_report(&self) -> String {
use std::fmt::Write;
let mut report = String::new();
report.push_str("=== Nullification Report ===\n\n");
for result in &self.results {
report.push_str(&result.report());
report.push('\n');
}
let _ = write!(
report,
"\nSummary: {} tests, {} rejected, {} not rejected\n",
self.summary.total_tests, self.summary.rejected, self.summary.not_rejected
);
let _ = writeln!(report, "Status: {}", self.summary.status);
report
}
}
pub mod ml_hypotheses {
use super::NullificationTest;
#[must_use]
pub fn training_determinism() -> NullificationTest {
NullificationTest::new("H0-TRAIN-01").with_expected(0.0) }
#[must_use]
pub fn loss_stability(threshold_sigma: f64) -> NullificationTest {
NullificationTest::new("H0-TRAIN-02").with_expected(threshold_sigma) }
#[must_use]
pub fn gradient_bounded(max_norm: f64) -> NullificationTest {
NullificationTest::new("H0-TRAIN-03").with_expected(max_norm)
}
#[must_use]
pub fn params_finite() -> NullificationTest {
NullificationTest::new("H0-TRAIN-05").with_expected(0.0) }
#[must_use]
pub fn prediction_determinism() -> NullificationTest {
NullificationTest::new("H0-PRED-01").with_expected(0.0) }
#[must_use]
pub fn latency_sla(sla_ms: f64) -> NullificationTest {
NullificationTest::new("H0-PRED-03").with_expected(sla_ms)
}
#[must_use]
pub fn accuracy_baseline(baseline: f64) -> NullificationTest {
NullificationTest::new("H0-MULTI-01").with_expected(baseline)
}
#[must_use]
pub fn statistical_significance() -> NullificationTest {
NullificationTest::new("H0-MULTI-05")
.with_runs(5)
.with_alpha(0.05)
}
}
#[derive(Debug, Clone)]
pub struct EnergyConservationHypothesis {
pub initial_energy: f64,
pub tolerance: f64,
}
impl EnergyConservationHypothesis {
#[must_use]
pub fn new(initial_energy: f64, tolerance: f64) -> Self {
Self {
initial_energy,
tolerance,
}
}
}
impl FalsifiableHypothesis for EnergyConservationHypothesis {
type State = f64;
fn predict(&self, _state: &Self::State) -> Predictions {
let mut predictions = Predictions::new();
predictions.add(self.initial_energy, self.tolerance);
predictions
}
fn falsification_criteria(&self) -> Vec<FalsificationCriteria> {
vec![FalsificationCriteria::less_than(
"energy_drift",
self.tolerance,
)]
}
fn robustness(&self, state: &Self::State) -> f64 {
let drift =
(state - self.initial_energy).abs() / self.initial_energy.abs().max(f64::EPSILON);
self.tolerance - drift
}
fn null_hypothesis_test(
&self,
predictions: &Predictions,
observations: &Observations,
significance: f64,
) -> NHSTResult {
if predictions.is_empty() || observations.is_empty() {
return NHSTResult::NotRejected {
p_value: 1.0,
power: 0.0,
};
}
stats::two_sample_t_test(&predictions.values, &observations.values, significance)
}
}
#[cfg(test)]
mod tests {
use super::stats::*;
use super::*;
#[test]
fn test_nullification_test_new() {
let test = NullificationTest::new("H0-TEST-01");
assert_eq!(test.hypothesis_id, "H0-TEST-01");
assert_eq!(test.n_runs, 5);
assert!((test.alpha - 0.05).abs() < f64::EPSILON);
}
#[test]
fn test_nullification_test_with_runs() {
let test = NullificationTest::new("H0-TEST").with_runs(10);
assert_eq!(test.n_runs, 10);
let test = NullificationTest::new("H0-TEST").with_runs(3);
assert_eq!(test.n_runs, 5);
}
#[test]
fn test_nullification_test_execute() {
let test = NullificationTest::new("H0-TEST").with_expected(0.0);
let result = test.execute(|| 0.0);
assert!(!result.rejected); }
#[test]
fn test_nullification_test_execute_reject() {
let test = NullificationTest::new("H0-TEST").with_expected(0.0);
let result = test.execute(|| 100.0); assert!(result.rejected);
}
#[test]
fn test_nullification_test_evaluate() {
let test = NullificationTest::new("H0-TEST").with_expected(0.0);
let observations = vec![0.1, 0.05, -0.05, 0.02, -0.01];
let result = test.evaluate(&observations);
assert!(!result.rejected); }
#[test]
fn test_nullification_result_report() {
let result = NullificationResult {
hypothesis_id: "H0-TEST".to_string(),
rejected: true,
p_value: 0.01,
confidence_interval: (0.5, 1.5),
effect_size: 0.8,
observations: vec![1.0],
};
let report = result.report();
assert!(report.contains("H0-TEST"));
assert!(report.contains("REJECTED"));
assert!(report.contains("0.0100"));
}
#[test]
fn test_nullification_report() {
let mut report = NullificationReport::new();
report.add(NullificationResult {
hypothesis_id: "H0-1".to_string(),
rejected: false,
p_value: 0.1,
confidence_interval: (0.0, 1.0),
effect_size: 0.1,
observations: vec![],
});
report.add(NullificationResult {
hypothesis_id: "H0-2".to_string(),
rejected: true,
p_value: 0.01,
confidence_interval: (0.0, 1.0),
effect_size: 0.8,
observations: vec![],
});
assert_eq!(report.summary.total_tests, 2);
assert_eq!(report.summary.rejected, 1);
assert_eq!(report.summary.not_rejected, 1);
assert_eq!(report.summary.status, "PARTIAL_PASS");
}
#[test]
fn test_nullification_report_full() {
let mut report = NullificationReport::new();
report.add(NullificationResult {
hypothesis_id: "H0-PASS".to_string(),
rejected: false,
p_value: 0.5,
confidence_interval: (0.0, 1.0),
effect_size: 0.1,
observations: vec![1.0, 2.0],
});
let text = report.full_report();
assert!(text.contains("Nullification Report"));
assert!(text.contains("H0-PASS"));
assert!(text.contains("NOT REJECTED"));
}
#[test]
fn test_ml_hypotheses() {
let test = ml_hypotheses::training_determinism();
assert_eq!(test.hypothesis_id, "H0-TRAIN-01");
let test = ml_hypotheses::loss_stability(3.0);
assert_eq!(test.hypothesis_id, "H0-TRAIN-02");
let test = ml_hypotheses::prediction_determinism();
assert_eq!(test.hypothesis_id, "H0-PRED-01");
let test = ml_hypotheses::accuracy_baseline(0.9);
assert!((test.expected_value - 0.9).abs() < f64::EPSILON);
}
#[test]
fn test_nhst_result() {
let rejected = NHSTResult::Rejected {
p_value: 0.01,
effect_size: 0.5,
test_statistic: 2.5,
};
assert!(rejected.is_rejected());
assert!((rejected.p_value() - 0.01).abs() < f64::EPSILON);
let not_rejected = NHSTResult::NotRejected {
p_value: 0.1,
power: 0.8,
};
assert!(!not_rejected.is_rejected());
}
#[test]
fn test_predictions_observations() {
let mut pred = Predictions::new();
pred.add(1.0, 0.1);
pred.add(2.0, 0.2);
assert_eq!(pred.len(), 2);
assert!(!pred.is_empty());
let mut obs = Observations::new();
obs.add(1.1, 0.05);
assert_eq!(obs.len(), 1);
}
#[test]
fn test_falsification_criteria() {
let crit = FalsificationCriteria::less_than("energy_drift", 0.01);
assert!(crit.check(0.005));
assert!(!crit.check(0.02));
let crit = FalsificationCriteria::greater_than("power", 0.8);
assert!(crit.check(0.9));
assert!(!crit.check(0.7));
}
#[test]
fn test_stats_mean() {
let values = vec![1.0, 2.0, 3.0, 4.0, 5.0];
assert!((mean(&values) - 3.0).abs() < f64::EPSILON);
let empty: Vec<f64> = vec![];
assert!((mean(&empty) - 0.0).abs() < f64::EPSILON);
}
#[test]
fn test_stats_variance() {
let values = vec![2.0, 4.0, 4.0, 4.0, 5.0, 5.0, 7.0, 9.0];
let var = variance(&values);
assert!((var - 4.571_428_571_428_571).abs() < 0.001);
}
#[test]
fn test_stats_std_dev() {
let values = vec![2.0, 4.0, 4.0, 4.0, 5.0, 5.0, 7.0, 9.0];
let sd = std_dev(&values);
assert!((sd - 2.138).abs() < 0.01);
}
#[test]
fn test_one_sample_t_test() {
let values = vec![5.0, 5.1, 4.9, 5.2, 4.8, 5.0, 5.1, 4.9];
let result = one_sample_t_test(&values, 0.0, 0.05);
assert!(result.is_rejected());
let values = vec![0.1, -0.1, 0.05, -0.05, 0.0, 0.02, -0.02, 0.01];
let result = one_sample_t_test(&values, 0.0, 0.05);
assert!(!result.is_rejected());
}
#[test]
fn test_two_sample_t_test() {
let sample1 = vec![10.0, 11.0, 12.0, 10.5, 11.5];
let sample2 = vec![1.0, 2.0, 1.5, 2.5, 1.2];
let result = two_sample_t_test(&sample1, &sample2, 0.05);
assert!(result.is_rejected());
let sample1 = vec![5.0, 5.1, 4.9, 5.2, 4.8];
let sample2 = vec![5.05, 5.15, 4.95, 5.1, 4.85];
let result = two_sample_t_test(&sample1, &sample2, 0.05);
assert!(!result.is_rejected());
}
#[test]
fn test_energy_conservation_hypothesis() {
let hypothesis = EnergyConservationHypothesis::new(100.0, 0.01);
let robustness_good = hypothesis.robustness(&100.5); assert!(robustness_good > 0.0);
let robustness_bad = hypothesis.robustness(&110.0); assert!(robustness_bad < 0.0);
let criteria = hypothesis.falsification_criteria();
assert_eq!(criteria.len(), 1);
assert_eq!(criteria[0].metric, "energy_drift");
}
#[test]
fn test_falsification_criteria_all_operators() {
let crit = FalsificationCriteria {
metric: "test".to_string(),
operator: ComparisonOp::Le,
threshold: 10.0,
};
assert!(crit.check(9.0));
assert!(crit.check(10.0));
assert!(!crit.check(11.0));
let crit = FalsificationCriteria {
metric: "test".to_string(),
operator: ComparisonOp::Ge,
threshold: 10.0,
};
assert!(!crit.check(9.0));
assert!(crit.check(10.0));
assert!(crit.check(11.0));
let crit = FalsificationCriteria {
metric: "test".to_string(),
operator: ComparisonOp::Eq,
threshold: 10.0,
};
assert!(crit.check(10.0));
assert!(!crit.check(10.1));
let crit = FalsificationCriteria {
metric: "test".to_string(),
operator: ComparisonOp::Ne,
threshold: 10.0,
};
assert!(!crit.check(10.0));
assert!(crit.check(10.1));
}
#[test]
fn test_stats_std_error() {
let values = vec![1.0, 2.0, 3.0, 4.0, 5.0];
let se = std_error(&values);
assert!(se > 0.0);
let empty: Vec<f64> = vec![];
assert!((std_error(&empty) - 0.0).abs() < f64::EPSILON);
}
#[test]
fn test_stats_chi_square_test() {
let observed = vec![10.0, 20.0, 30.0, 40.0];
let expected = vec![25.0, 25.0, 25.0, 25.0];
let result = chi_square_test(&observed, &expected, 0.05);
assert!(result.is_rejected());
let observed = vec![24.0, 26.0, 24.0, 26.0];
let expected = vec![25.0, 25.0, 25.0, 25.0];
let result = chi_square_test(&observed, &expected, 0.05);
assert!(!result.is_rejected());
}
#[test]
fn test_chi_square_test_edge_cases() {
let result = chi_square_test(&[1.0, 2.0], &[1.0], 0.05);
assert!(!result.is_rejected());
assert!((result.p_value() - 1.0).abs() < f64::EPSILON);
let result = chi_square_test(&[], &[], 0.05);
assert!(!result.is_rejected());
}
#[test]
fn test_stats_normal_cdf_pub() {
assert!((normal_cdf_pub(0.0) - 0.5).abs() < 0.01);
assert!(normal_cdf_pub(3.0) > 0.99);
assert!(normal_cdf_pub(-3.0) < 0.01);
}
#[test]
fn test_ml_hypotheses_all() {
let test = ml_hypotheses::gradient_bounded(10.0);
assert_eq!(test.hypothesis_id, "H0-TRAIN-03");
assert!((test.expected_value - 10.0).abs() < f64::EPSILON);
let test = ml_hypotheses::params_finite();
assert_eq!(test.hypothesis_id, "H0-TRAIN-05");
let test = ml_hypotheses::latency_sla(100.0);
assert_eq!(test.hypothesis_id, "H0-PRED-03");
assert!((test.expected_value - 100.0).abs() < f64::EPSILON);
let test = ml_hypotheses::statistical_significance();
assert_eq!(test.hypothesis_id, "H0-MULTI-05");
assert_eq!(test.n_runs, 5);
assert!((test.alpha - 0.05).abs() < f64::EPSILON);
}
#[test]
fn test_nullification_test_with_alpha() {
let test = NullificationTest::new("H0-TEST").with_alpha(0.01);
assert!((test.alpha - 0.01).abs() < f64::EPSILON);
let test = NullificationTest::new("H0-TEST").with_alpha(0.0001); assert!((test.alpha - 0.001).abs() < f64::EPSILON);
let test = NullificationTest::new("H0-TEST").with_alpha(0.5); assert!((test.alpha - 0.1).abs() < f64::EPSILON);
}
#[test]
fn test_nullification_report_all_pass() {
let mut report = NullificationReport::new();
report.add(NullificationResult {
hypothesis_id: "H0-1".to_string(),
rejected: false,
p_value: 0.5,
confidence_interval: (0.0, 1.0),
effect_size: 0.1,
observations: vec![],
});
assert_eq!(report.summary.status, "PASS");
}
#[test]
fn test_nullification_report_all_fail() {
let mut report = NullificationReport::new();
report.add(NullificationResult {
hypothesis_id: "H0-1".to_string(),
rejected: true,
p_value: 0.01,
confidence_interval: (0.0, 1.0),
effect_size: 0.8,
observations: vec![],
});
assert_eq!(report.summary.status, "FAIL");
}
#[test]
fn test_bootstrap_ci_empty() {
let test = NullificationTest::new("H0-TEST");
let result = test.evaluate(&[]);
assert!((result.confidence_interval.0 - 0.0).abs() < f64::EPSILON);
assert!((result.confidence_interval.1 - 0.0).abs() < f64::EPSILON);
}
#[test]
fn test_compute_stats_single_value() {
let test = NullificationTest::new("H0-TEST").with_expected(0.0);
let result = test.evaluate(&[1.0]);
assert!((result.p_value - 1.0).abs() < f64::EPSILON);
}
#[test]
fn test_compute_stats_no_variance() {
let test = NullificationTest::new("H0-TEST").with_expected(5.0);
let result = test.evaluate(&[5.0, 5.0, 5.0, 5.0, 5.0]);
assert!((result.p_value - 1.0).abs() < f64::EPSILON);
let test = NullificationTest::new("H0-TEST").with_expected(0.0);
let result = test.evaluate(&[10.0, 10.0, 10.0, 10.0, 10.0]);
assert!((result.p_value - 0.0).abs() < f64::EPSILON);
}
#[test]
fn test_one_sample_t_test_edge_cases() {
let result = one_sample_t_test(&[5.0], 0.0, 0.05);
assert!(!result.is_rejected());
assert!((result.p_value() - 1.0).abs() < f64::EPSILON);
let result = one_sample_t_test(&[5.0, 5.0, 5.0, 5.0, 5.0], 5.0, 0.05);
assert!(!result.is_rejected());
}
#[test]
fn test_two_sample_t_test_edge_cases() {
let result = two_sample_t_test(&[5.0], &[10.0], 0.05);
assert!(!result.is_rejected());
let result = two_sample_t_test(
&[5.0, 5.0, 5.0, 5.0, 5.0],
&[10.0, 10.0, 10.0, 10.0, 10.0],
0.05,
);
assert!(!result.is_rejected()); }
#[test]
fn test_energy_conservation_hypothesis_predict() {
let hypothesis = EnergyConservationHypothesis::new(100.0, 0.01);
let predictions = hypothesis.predict(&100.0);
assert_eq!(predictions.len(), 1);
assert!((predictions.values[0] - 100.0).abs() < f64::EPSILON);
}
#[test]
fn test_energy_conservation_hypothesis_null_test() {
let hypothesis = EnergyConservationHypothesis::new(100.0, 0.01);
let pred = Predictions::new();
let obs = Observations::new();
let result = hypothesis.null_hypothesis_test(&pred, &obs, 0.05);
assert!(!result.is_rejected());
let mut pred = Predictions::new();
pred.add(100.0, 0.1);
pred.add(100.0, 0.1);
pred.add(100.0, 0.1);
let mut obs = Observations::new();
obs.add(100.5, 0.1);
obs.add(99.5, 0.1);
obs.add(100.2, 0.1);
let result = hypothesis.null_hypothesis_test(&pred, &obs, 0.05);
assert!(!result.is_rejected());
}
#[test]
fn test_predictions_default() {
let pred = Predictions::default();
assert!(pred.is_empty());
assert_eq!(pred.len(), 0);
}
#[test]
fn test_observations_default() {
let obs = Observations::default();
assert!(obs.is_empty());
assert_eq!(obs.len(), 0);
}
#[test]
fn test_falsification_criteria_debug() {
let crit = FalsificationCriteria::less_than("test", 0.01);
let debug = format!("{:?}", crit);
assert!(debug.contains("test"));
}
#[test]
fn test_comparison_op_eq() {
assert_eq!(ComparisonOp::Lt, ComparisonOp::Lt);
assert_ne!(ComparisonOp::Lt, ComparisonOp::Gt);
}
#[test]
fn test_nhst_result_clone() {
let result = NHSTResult::Rejected {
p_value: 0.01,
effect_size: 0.5,
test_statistic: 2.5,
};
let cloned = result.clone();
assert!(cloned.is_rejected());
}
#[test]
fn test_nullification_result_clone() {
let result = NullificationResult {
hypothesis_id: "H0-TEST".to_string(),
rejected: true,
p_value: 0.01,
confidence_interval: (0.5, 1.5),
effect_size: 0.8,
observations: vec![1.0, 2.0],
};
let cloned = result.clone();
assert_eq!(cloned.hypothesis_id, "H0-TEST");
assert!(cloned.rejected);
}
#[test]
fn test_energy_conservation_clone() {
let hypothesis = EnergyConservationHypothesis::new(100.0, 0.01);
let cloned = hypothesis.clone();
assert!((cloned.initial_energy - 100.0).abs() < f64::EPSILON);
}
}
#[cfg(test)]
mod proptests {
use super::stats::*;
use super::*;
use proptest::prelude::*;
proptest! {
#[test]
fn prop_mean_constant(value in -1000.0f64..1000.0, n in 1usize..100) {
let values: Vec<f64> = vec![value; n];
let m = mean(&values);
prop_assert!((m - value).abs() < 1e-10);
}
#[test]
fn prop_variance_constant(value in -1000.0f64..1000.0, n in 2usize..100) {
let values: Vec<f64> = vec![value; n];
let v = variance(&values);
prop_assert!(v.abs() < 1e-10);
}
#[test]
fn prop_robustness_sign(
initial in 1.0f64..1000.0,
tolerance in 0.001f64..0.1,
drift_factor in 0.0f64..0.2,
) {
let hypothesis = EnergyConservationHypothesis::new(initial, tolerance);
let current = initial * (1.0 + drift_factor);
let robustness = hypothesis.robustness(¤t);
if drift_factor < tolerance {
prop_assert!(robustness > 0.0,
"Expected positive robustness for drift {} < tolerance {}",
drift_factor, tolerance);
}
}
}
}