use serde::{Deserialize, Serialize};
use super::config::FeatureFamily;
use super::dataset::{PowerUnits, StageId};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum Regime {
WellConditioned,
Stressed,
Overstressed,
}
impl Regime {
pub fn from_ratio(r: f64) -> Self {
if r <= 0.05 {
Regime::WellConditioned
} else if r <= 0.15 {
Regime::Stressed
} else {
Regime::Overstressed
}
}
}
impl std::fmt::Display for Regime {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Regime::WellConditioned => write!(f, "Well-conditioned"),
Regime::Stressed => write!(f, "Stressed"),
Regime::Overstressed => write!(f, "Overstressed"),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DimensionInfo {
pub d: usize,
pub n_eff: f64,
pub r: f64,
pub regime: Regime,
pub shrinkage_lambda: Option<f64>,
}
impl DimensionInfo {
pub fn new(d: usize, n_eff: f64) -> Self {
let r = d as f64 / n_eff;
Self {
d,
n_eff,
r,
regime: Regime::from_ratio(r),
shrinkage_lambda: None,
}
}
pub fn with_shrinkage(mut self, lambda: f64) -> Self {
self.shrinkage_lambda = Some(lambda);
self
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FeatureHotspot {
pub feature_index: usize,
pub partition_index: usize,
pub component: Option<String>,
pub effect_magnitude: f64,
pub contribution: f64,
pub credible_interval: (f64, f64),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StageReport {
pub stage_id: StageId,
pub leak_probability: f64,
pub max_effect: f64,
pub hotspots: Vec<FeatureHotspot>,
pub num_features: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PowerDiagnostics {
pub n_fixed: usize,
pub n_random: usize,
pub n_total: usize,
pub n_eff: f64,
pub iact_fixed: f64,
pub iact_random: f64,
pub iact_combined: f64,
pub theta_floor: f64,
pub block_length: usize,
pub gibbs_samples: usize,
pub gibbs_burnin: usize,
pub convergence: Option<f64>,
pub warnings: Vec<String>,
}
impl Default for PowerDiagnostics {
fn default() -> Self {
Self {
n_fixed: 0,
n_random: 0,
n_total: 0,
n_eff: 0.0,
iact_fixed: 1.0,
iact_random: 1.0,
iact_combined: 1.0,
theta_floor: 0.0,
block_length: 1,
gibbs_samples: 0,
gibbs_burnin: 0,
convergence: None,
warnings: Vec::new(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum PowerOutcome {
Pass {
leak_probability: f64,
max_effect: f64,
},
Fail {
leak_probability: f64,
max_effect: f64,
max_effect_ci95: (f64, f64),
},
Inconclusive {
reason: String,
leak_probability: f64,
},
}
impl PowerOutcome {
pub fn is_pass(&self) -> bool {
matches!(self, PowerOutcome::Pass { .. })
}
pub fn is_fail(&self) -> bool {
matches!(self, PowerOutcome::Fail { .. })
}
pub fn is_conclusive(&self) -> bool {
!matches!(self, PowerOutcome::Inconclusive { .. })
}
pub fn leak_probability(&self) -> f64 {
match self {
PowerOutcome::Pass {
leak_probability, ..
} => *leak_probability,
PowerOutcome::Fail {
leak_probability, ..
} => *leak_probability,
PowerOutcome::Inconclusive {
leak_probability, ..
} => *leak_probability,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Report {
pub outcome: PowerOutcome,
pub max_effect: f64,
pub max_effect_ci95: (f64, f64),
pub leak_probability: f64,
pub theta_floor: f64,
pub floor_multiplier: f64,
pub theta_eff: f64,
pub units: PowerUnits,
pub feature_family: FeatureFamily,
pub dimension: DimensionInfo,
pub top_features: Vec<FeatureHotspot>,
pub stages: Option<Vec<StageReport>>,
pub diagnostics: PowerDiagnostics,
}
impl Report {
pub fn new(outcome: PowerOutcome, dimension: DimensionInfo) -> Self {
let (leak_probability, max_effect, max_effect_ci95) = match &outcome {
PowerOutcome::Pass {
leak_probability,
max_effect,
} => (*leak_probability, *max_effect, (0.0, 0.0)),
PowerOutcome::Fail {
leak_probability,
max_effect,
max_effect_ci95,
} => (*leak_probability, *max_effect, *max_effect_ci95),
PowerOutcome::Inconclusive {
leak_probability, ..
} => (*leak_probability, 0.0, (0.0, 0.0)),
};
Self {
outcome,
max_effect,
max_effect_ci95,
leak_probability,
theta_floor: 0.0,
floor_multiplier: 5.0,
theta_eff: 0.0,
units: PowerUnits::default(),
feature_family: FeatureFamily::default(),
dimension,
top_features: Vec::new(),
stages: None,
diagnostics: PowerDiagnostics::default(),
}
}
pub fn is_leaky(&self) -> bool {
self.outcome.is_fail()
}
pub fn summary(&self) -> String {
match &self.outcome {
PowerOutcome::Pass {
leak_probability, ..
} => {
format!(
"PASS: No power leakage detected (P={:.1}%, max_effect={:.3} {})",
leak_probability * 100.0,
self.max_effect,
self.units
)
}
PowerOutcome::Fail {
leak_probability,
max_effect,
..
} => {
format!(
"FAIL: Power leakage detected (P={:.1}%, max_effect={:.3} {})",
leak_probability * 100.0,
max_effect,
self.units
)
}
PowerOutcome::Inconclusive {
reason,
leak_probability,
} => {
format!(
"INCONCLUSIVE: {} (P={:.1}%)",
reason,
leak_probability * 100.0
)
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_regime_from_ratio() {
assert_eq!(Regime::from_ratio(0.01), Regime::WellConditioned);
assert_eq!(Regime::from_ratio(0.05), Regime::WellConditioned);
assert_eq!(Regime::from_ratio(0.10), Regime::Stressed);
assert_eq!(Regime::from_ratio(0.15), Regime::Stressed);
assert_eq!(Regime::from_ratio(0.20), Regime::Overstressed);
}
#[test]
fn test_dimension_info() {
let info = DimensionInfo::new(32, 100.0);
assert_eq!(info.d, 32);
assert_eq!(info.n_eff, 100.0);
assert!((info.r - 0.32).abs() < 1e-6);
assert_eq!(info.regime, Regime::Overstressed);
}
#[test]
fn test_power_outcome() {
let pass = PowerOutcome::Pass {
leak_probability: 0.01,
max_effect: 0.5,
};
assert!(pass.is_pass());
assert!(!pass.is_fail());
assert!(pass.is_conclusive());
let fail = PowerOutcome::Fail {
leak_probability: 0.99,
max_effect: 10.0,
max_effect_ci95: (8.0, 12.0),
};
assert!(!fail.is_pass());
assert!(fail.is_fail());
assert!(fail.is_conclusive());
let inconclusive = PowerOutcome::Inconclusive {
reason: "Not enough data".to_string(),
leak_probability: 0.5,
};
assert!(!inconclusive.is_pass());
assert!(!inconclusive.is_fail());
assert!(!inconclusive.is_conclusive());
}
#[test]
fn test_report_summary() {
let dim = DimensionInfo::new(32, 1000.0);
let report = Report::new(
PowerOutcome::Fail {
leak_probability: 0.95,
max_effect: 5.0,
max_effect_ci95: (4.0, 6.0),
},
dim,
);
let summary = report.summary();
assert!(summary.contains("FAIL"));
assert!(summary.contains("95.0%"));
}
}