Skip to main content

forge_reasoning/hypothesis/
confidence.rs

1//! Bounded confidence value with Bayesian update support
2//!
3//! Confidence is a newtype wrapper around f64 that enforces bounds [0.0, 1.0]
4//! and rejects NaN values. This provides type-safe probability values for
5//! hypothesis tracking.
6
7use serde::{Deserialize, Serialize};
8use thiserror::Error;
9
10/// Bounded confidence value [0.0, 1.0]
11#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
12pub struct Confidence(f64);
13
14#[derive(Error, Debug)]
15pub enum ConfidenceError {
16    #[error("Confidence value cannot be NaN")]
17    NaN,
18
19    #[error("Confidence out of bounds: {value} (must be {min} to {max})")]
20    OutOfBounds { value: f64, min: f64, max: f64 },
21
22    #[error("Evidence probability is zero, cannot compute Bayes update")]
23    ZeroEvidenceProbability,
24}
25
26impl Confidence {
27    const MIN: f64 = 0.0;
28    const MAX: f64 = 1.0;
29
30    /// Create a new confidence value with bounds validation
31    ///
32    /// # Errors
33    /// - Returns `ConfidenceError::NaN` if value is NaN
34    /// - Returns `ConfidenceError::OutOfBounds` if value < 0.0 or > 1.0
35    pub fn new(value: f64) -> Result<Self, ConfidenceError> {
36        if value.is_nan() {
37            return Err(ConfidenceError::NaN);
38        }
39        if value < Self::MIN || value > Self::MAX {
40            return Err(ConfidenceError::OutOfBounds {
41                value,
42                min: Self::MIN,
43                max: Self::MAX,
44            });
45        }
46        Ok(Self(value))
47    }
48
49    /// Get the underlying f64 value
50    pub fn get(self) -> f64 {
51        self.0
52    }
53
54    /// Update confidence using Bayes theorem
55    ///
56    /// P(H|E) = P(E|H) * P(H) / P(E)
57    ///
58    /// # Arguments
59    /// - `likelihood_h`: P(E|H) - probability of evidence given hypothesis is true
60    /// - `likelihood_not_h`: P(E|¬H) - probability of evidence given hypothesis is false
61    ///
62    /// # Returns
63    /// New confidence value (posterior) based on Bayes formula
64    ///
65    /// # Errors
66    /// - Returns error if the resulting posterior is invalid (NaN or out of bounds)
67    pub fn update_with_evidence(
68        self,
69        likelihood_h: f64,  // P(E|H)
70        likelihood_not_h: f64,  // P(E|¬H)
71    ) -> Result<Self, ConfidenceError> {
72        let prior = self.0;
73
74        // P(E) = P(E|H) * P(H) + P(E|¬H) * P(¬H)
75        let p_e = (likelihood_h * prior) + (likelihood_not_h * (1.0 - prior));
76
77        // Guard against division by zero
78        const MIN_PROB: f64 = 1e-10;
79        let p_e = p_e.max(MIN_PROB);
80
81        // P(H|E) = P(E|H) * P(H) / P(E)
82        let posterior = (likelihood_h * prior) / p_e;
83
84        Self::new(posterior)
85    }
86
87    /// Maximum uncertainty confidence (0.5)
88    pub fn max_uncertainty() -> Self {
89        Self(0.5)
90    }
91}
92
93impl TryFrom<f64> for Confidence {
94    type Error = ConfidenceError;
95    fn try_from(value: f64) -> Result<Self, Self::Error> {
96        Self::new(value)
97    }
98}
99
100impl Default for Confidence {
101    fn default() -> Self {
102        Self::max_uncertainty()
103    }
104}
105
106#[cfg(test)]
107mod tests {
108    use super::*;
109
110    #[test]
111    fn test_confidence_valid() {
112        assert!(Confidence::new(0.0).is_ok());
113        assert!(Confidence::new(0.5).is_ok());
114        assert!(Confidence::new(1.0).is_ok());
115    }
116
117    #[test]
118    fn test_confidence_rejects_nan() {
119        assert!(matches!(
120            Confidence::new(f64::NAN),
121            Err(ConfidenceError::NaN)
122        ));
123    }
124
125    #[test]
126    fn test_confidence_rejects_out_of_bounds() {
127        assert!(Confidence::new(-0.1).is_err());
128        assert!(Confidence::new(1.1).is_err());
129        assert!(Confidence::new(2.0).is_err());
130    }
131
132    #[test]
133    fn test_confidence_get() {
134        let c = Confidence::new(0.75).unwrap();
135        assert_eq!(c.get(), 0.75);
136    }
137
138    #[test]
139    fn test_bayes_update_supporting_evidence() {
140        // Prior: 0.5 (max uncertainty)
141        // Evidence: P(E|H) = 0.9, P(E|¬H) = 0.1
142        // Expected: Posterior > 0.8
143        let prior = Confidence::new(0.5).unwrap();
144        let posterior = prior.update_with_evidence(0.9, 0.1).unwrap();
145        assert!(posterior.get() > 0.8);
146    }
147
148    #[test]
149    fn test_bayes_update_contradictory_evidence() {
150        // Prior: 0.5
151        // Evidence: P(E|H) = 0.1, P(E|¬H) = 0.9
152        // Expected: Posterior < 0.2
153        let prior = Confidence::new(0.5).unwrap();
154        let posterior = prior.update_with_evidence(0.1, 0.9).unwrap();
155        assert!(posterior.get() < 0.2);
156    }
157
158    #[test]
159    fn test_bayes_update_neutral_evidence() {
160        // Prior: 0.5
161        // Evidence: P(E|H) = 0.5, P(E|¬H) = 0.5
162        // Expected: Posterior ≈ 0.5 (no change)
163        let prior = Confidence::new(0.5).unwrap();
164        let posterior = prior.update_with_evidence(0.5, 0.5).unwrap();
165        assert!((posterior.get() - 0.5).abs() < 0.01);
166    }
167
168    #[test]
169    fn test_max_uncertainty() {
170        let c = Confidence::max_uncertainty();
171        assert_eq!(c.get(), 0.5);
172    }
173
174    #[test]
175    fn test_default() {
176        let c = Confidence::default();
177        assert_eq!(c.get(), 0.5);
178    }
179
180    #[test]
181    fn test_try_from_f64() {
182        assert!(Confidence::try_from(0.5).is_ok());
183        assert!(Confidence::try_from(f64::NAN).is_err());
184        assert!(Confidence::try_from(1.5).is_err());
185    }
186}