use crate::coherence::CoherenceEnergy;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CoherenceConfidence {
pub energy_scale: f32,
pub threshold: f32,
}
impl Default for CoherenceConfidence {
fn default() -> Self {
Self {
energy_scale: 1.0,
threshold: 1.0,
}
}
}
impl CoherenceConfidence {
#[must_use]
pub fn new(energy_scale: f32, threshold: f32) -> Self {
assert!(
energy_scale > 0.0,
"energy_scale must be positive, got {energy_scale}"
);
assert!(
threshold >= 0.0,
"threshold must be non-negative, got {threshold}"
);
Self {
energy_scale,
threshold,
}
}
#[must_use]
pub fn strict() -> Self {
Self {
energy_scale: 3.0,
threshold: 0.5,
}
}
#[must_use]
pub fn lenient() -> Self {
Self {
energy_scale: 0.5,
threshold: 2.0,
}
}
#[inline]
#[must_use]
pub fn confidence_from_energy(&self, energy: f32) -> f32 {
let exponent = self.energy_scale * (energy - self.threshold);
if exponent > 20.0 {
return 0.0; }
if exponent < -20.0 {
return 1.0; }
1.0 / (1.0 + exponent.exp())
}
#[must_use]
pub fn compute_confidence(&self, coherence_energy: &CoherenceEnergy) -> ConfidenceScore {
let value = self.confidence_from_energy(coherence_energy.total_energy);
let witness_backed = !coherence_energy.edge_energies.is_empty();
let explanation = self.build_explanation(coherence_energy, value);
ConfidenceScore {
value,
explanation,
witness_backed,
total_energy: coherence_energy.total_energy,
edge_count: coherence_energy.edge_count,
threshold_used: self.threshold,
scale_used: self.energy_scale,
}
}
#[must_use]
pub fn explain_confidence(
&self,
coherence_energy: &CoherenceEnergy,
top_k: usize,
) -> Vec<EnergyContributor> {
let hotspots = coherence_energy.hotspots(top_k);
hotspots
.into_iter()
.map(|h| EnergyContributor {
edge_id: h.edge_id,
source: h.source,
target: h.target,
energy: h.energy,
percentage: h.percentage,
contribution_to_confidence_drop: self.compute_contribution_effect(h.energy),
})
.collect()
}
fn build_explanation(&self, coherence_energy: &CoherenceEnergy, confidence: f32) -> String {
let energy = coherence_energy.total_energy;
let edge_count = coherence_energy.edge_count;
let confidence_level = if confidence >= 0.9 {
"very high"
} else if confidence >= 0.7 {
"high"
} else if confidence >= 0.5 {
"moderate"
} else if confidence >= 0.3 {
"low"
} else {
"very low"
};
let energy_assessment = if energy < self.threshold * 0.5 {
"well below threshold"
} else if energy < self.threshold {
"below threshold"
} else if energy < self.threshold * 1.5 {
"near threshold"
} else if energy < self.threshold * 2.0 {
"above threshold"
} else {
"significantly above threshold"
};
format!(
"Confidence is {} ({:.1}%) based on total energy {:.4} ({}) \
computed from {} edges. Threshold: {:.2}, Scale: {:.2}.",
confidence_level,
confidence * 100.0,
energy,
energy_assessment,
edge_count,
self.threshold,
self.energy_scale
)
}
fn compute_contribution_effect(&self, edge_energy: f32) -> f32 {
let max_derivative = 0.25 * self.energy_scale;
(max_derivative * edge_energy).min(1.0)
}
#[inline]
#[must_use]
pub fn confidence_at_threshold(&self) -> f32 {
0.5
}
#[must_use]
pub fn energy_for_confidence(&self, confidence: f32) -> Option<f32> {
if confidence <= 0.0 || confidence >= 1.0 {
return None;
}
let odds = (1.0 - confidence) / confidence;
Some(self.threshold + odds.ln() / self.energy_scale)
}
#[must_use]
pub fn batch_confidence(&self, energies: &[f32]) -> Vec<f32> {
energies
.iter()
.map(|&e| self.confidence_from_energy(e))
.collect()
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ConfidenceScore {
pub value: f32,
pub explanation: String,
pub witness_backed: bool,
pub total_energy: f32,
pub edge_count: usize,
pub threshold_used: f32,
pub scale_used: f32,
}
impl ConfidenceScore {
#[must_use]
pub fn from_value(value: f32) -> Self {
Self {
value: value.clamp(0.0, 1.0),
explanation: format!("Direct confidence value: {:.1}%", value * 100.0),
witness_backed: false,
total_energy: f32::NAN,
edge_count: 0,
threshold_used: f32::NAN,
scale_used: f32::NAN,
}
}
#[inline]
#[must_use]
pub fn is_confident(&self, min_confidence: f32) -> bool {
self.value >= min_confidence
}
#[inline]
#[must_use]
pub fn is_high_confidence(&self) -> bool {
self.value >= 0.7
}
#[inline]
#[must_use]
pub fn is_low_confidence(&self) -> bool {
self.value < 0.3
}
#[inline]
#[must_use]
pub fn as_percentage(&self) -> f32 {
self.value * 100.0
}
#[must_use]
pub fn level(&self) -> ConfidenceLevel {
if self.value >= 0.9 {
ConfidenceLevel::VeryHigh
} else if self.value >= 0.7 {
ConfidenceLevel::High
} else if self.value >= 0.5 {
ConfidenceLevel::Moderate
} else if self.value >= 0.3 {
ConfidenceLevel::Low
} else {
ConfidenceLevel::VeryLow
}
}
}
impl std::fmt::Display for ConfidenceScore {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{:.1}% confidence", self.as_percentage())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum ConfidenceLevel {
VeryHigh,
High,
Moderate,
Low,
VeryLow,
}
impl ConfidenceLevel {
#[must_use]
pub fn allows_action(&self) -> bool {
matches!(self, Self::VeryHigh | Self::High | Self::Moderate)
}
#[must_use]
pub fn requires_escalation(&self) -> bool {
matches!(self, Self::Low | Self::VeryLow)
}
}
impl std::fmt::Display for ConfidenceLevel {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let s = match self {
Self::VeryHigh => "Very High",
Self::High => "High",
Self::Moderate => "Moderate",
Self::Low => "Low",
Self::VeryLow => "Very Low",
};
write!(f, "{s}")
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EnergyContributor {
pub edge_id: String,
pub source: String,
pub target: String,
pub energy: f32,
pub percentage: f32,
pub contribution_to_confidence_drop: f32,
}
impl EnergyContributor {
#[inline]
#[must_use]
pub fn is_significant(&self) -> bool {
self.percentage > 10.0
}
#[inline]
#[must_use]
pub fn is_dominant(&self) -> bool {
self.percentage > 50.0
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::coherence::EdgeEnergy;
use std::collections::HashMap;
fn create_test_energy(total: f32, edge_count: usize) -> CoherenceEnergy {
let mut edge_energies = HashMap::new();
let energy_per_edge = if edge_count > 0 {
total / edge_count as f32
} else {
0.0
};
for i in 0..edge_count {
let edge_id = format!("e{i}");
edge_energies.insert(
edge_id.clone(),
EdgeEnergy::new(
edge_id,
format!("n{i}"),
format!("n{}", i + 1),
vec![(energy_per_edge / 1.0).sqrt()], 1.0,
),
);
}
CoherenceEnergy::new(edge_energies, &HashMap::new(), edge_count + 1, "test")
}
#[test]
fn test_confidence_at_threshold() {
let mapper = CoherenceConfidence::default();
let conf = mapper.confidence_from_energy(mapper.threshold);
assert!(
(conf - 0.5).abs() < 0.001,
"Confidence at threshold should be 0.5, got {conf}"
);
}
#[test]
fn test_low_energy_high_confidence() {
let mapper = CoherenceConfidence::default();
let conf = mapper.confidence_from_energy(0.1);
assert!(conf > 0.7, "Low energy should give high confidence, got {conf}");
let conf = mapper.confidence_from_energy(0.0);
assert!(conf > 0.9, "Zero energy should give very high confidence, got {conf}");
}
#[test]
fn test_high_energy_low_confidence() {
let mapper = CoherenceConfidence::default();
let conf = mapper.confidence_from_energy(3.0);
assert!(conf < 0.3, "High energy should give low confidence, got {conf}");
let conf = mapper.confidence_from_energy(10.0);
assert!(conf < 0.01, "Very high energy should give near-zero confidence, got {conf}");
}
#[test]
fn test_sigmoid_monotonicity() {
let mapper = CoherenceConfidence::default();
let energies = [0.0, 0.5, 1.0, 1.5, 2.0, 3.0, 5.0];
let confidences: Vec<f32> = energies.iter().map(|&e| mapper.confidence_from_energy(e)).collect();
for i in 1..confidences.len() {
assert!(
confidences[i] < confidences[i - 1],
"Confidence should decrease: {} should be < {}",
confidences[i],
confidences[i - 1]
);
}
}
#[test]
fn test_scale_affects_steepness() {
let steep = CoherenceConfidence::new(3.0, 1.0);
let gentle = CoherenceConfidence::new(0.5, 1.0);
assert!((steep.confidence_from_energy(1.0) - 0.5).abs() < 0.001);
assert!((gentle.confidence_from_energy(1.0) - 0.5).abs() < 0.001);
let steep_conf = steep.confidence_from_energy(1.5);
let gentle_conf = gentle.confidence_from_energy(1.5);
assert!(
steep_conf < gentle_conf,
"Steep scale should drop faster: {} vs {}",
steep_conf,
gentle_conf
);
}
#[test]
fn test_strict_vs_lenient() {
let strict = CoherenceConfidence::strict();
let lenient = CoherenceConfidence::lenient();
let strict_conf = strict.confidence_from_energy(1.0);
let lenient_conf = lenient.confidence_from_energy(1.0);
assert!(
strict_conf < lenient_conf,
"Strict should be less confident at same energy"
);
}
#[test]
fn test_compute_confidence_full() {
let mapper = CoherenceConfidence::default();
let energy = create_test_energy(0.5, 3);
let score = mapper.compute_confidence(&energy);
assert!(score.value > 0.5, "Low energy should give >0.5 confidence");
assert!(score.witness_backed, "Should be witness-backed with edge data");
assert_eq!(score.edge_count, 3);
assert!(!score.explanation.is_empty());
}
#[test]
fn test_explain_confidence() {
let mapper = CoherenceConfidence::default();
let energy = create_test_energy(2.0, 5);
let contributors = mapper.explain_confidence(&energy, 3);
assert!(contributors.len() <= 3);
for contrib in &contributors {
assert!(contrib.energy >= 0.0);
assert!(contrib.percentage >= 0.0);
}
}
#[test]
fn test_energy_for_confidence_inverse() {
let mapper = CoherenceConfidence::default();
let original_conf = 0.75;
if let Some(energy) = mapper.energy_for_confidence(original_conf) {
let recovered_conf = mapper.confidence_from_energy(energy);
assert!(
(recovered_conf - original_conf).abs() < 0.001,
"Round-trip failed: {} vs {}",
original_conf,
recovered_conf
);
}
assert!(mapper.energy_for_confidence(0.0).is_none());
assert!(mapper.energy_for_confidence(1.0).is_none());
}
#[test]
fn test_confidence_score_levels() {
assert_eq!(ConfidenceScore::from_value(0.95).level(), ConfidenceLevel::VeryHigh);
assert_eq!(ConfidenceScore::from_value(0.75).level(), ConfidenceLevel::High);
assert_eq!(ConfidenceScore::from_value(0.55).level(), ConfidenceLevel::Moderate);
assert_eq!(ConfidenceScore::from_value(0.35).level(), ConfidenceLevel::Low);
assert_eq!(ConfidenceScore::from_value(0.15).level(), ConfidenceLevel::VeryLow);
}
#[test]
fn test_confidence_level_actions() {
assert!(ConfidenceLevel::VeryHigh.allows_action());
assert!(ConfidenceLevel::High.allows_action());
assert!(ConfidenceLevel::Moderate.allows_action());
assert!(!ConfidenceLevel::Low.allows_action());
assert!(!ConfidenceLevel::VeryLow.allows_action());
assert!(!ConfidenceLevel::VeryHigh.requires_escalation());
assert!(ConfidenceLevel::Low.requires_escalation());
assert!(ConfidenceLevel::VeryLow.requires_escalation());
}
#[test]
fn test_batch_confidence() {
let mapper = CoherenceConfidence::default();
let energies = vec![0.0, 0.5, 1.0, 2.0, 5.0];
let confidences = mapper.batch_confidence(&energies);
assert_eq!(confidences.len(), energies.len());
for (i, &conf) in confidences.iter().enumerate() {
let expected = mapper.confidence_from_energy(energies[i]);
assert!((conf - expected).abs() < 1e-6);
}
}
#[test]
fn test_numerical_stability() {
let mapper = CoherenceConfidence::default();
let conf = mapper.confidence_from_energy(1000.0);
assert!(conf >= 0.0 && conf <= 1.0, "Large energy gave invalid confidence: {conf}");
assert!(conf < 0.001, "Large energy should give near-zero confidence");
let conf = mapper.confidence_from_energy(-100.0);
assert!(conf >= 0.0 && conf <= 1.0, "Negative energy gave invalid confidence: {conf}");
assert!(conf > 0.999, "Negative energy should give near-one confidence");
}
#[test]
fn test_energy_contributor() {
let contrib = EnergyContributor {
edge_id: "e1".to_string(),
source: "a".to_string(),
target: "b".to_string(),
energy: 0.5,
percentage: 25.0,
contribution_to_confidence_drop: 0.125,
};
assert!(contrib.is_significant());
assert!(!contrib.is_dominant());
let dominant = EnergyContributor {
edge_id: "e2".to_string(),
source: "c".to_string(),
target: "d".to_string(),
energy: 1.5,
percentage: 60.0,
contribution_to_confidence_drop: 0.375,
};
assert!(dominant.is_significant());
assert!(dominant.is_dominant());
}
}