uncertain_rs/operations/
logical.rs

1#![allow(clippy::cast_precision_loss)]
2
3use crate::Uncertain;
4use crate::traits::Shareable;
5
6/// Trait for logical operations on uncertain boolean values
7///
8/// **Note**: For most use cases, these trait methods provide the primary API for logical
9/// operations on uncertain boolean values. Unlike comparison operations, logical operations
10/// don't have method-based equivalents on `Uncertain<bool>`.
11pub trait LogicalOps {
12    /// Logical AND operation
13    #[must_use]
14    fn and(&self, other: &Self) -> Self;
15
16    /// Logical OR operation
17    #[must_use]
18    fn or(&self, other: &Self) -> Self;
19
20    /// Logical NOT operation
21    #[must_use]
22    fn not(&self) -> Self;
23
24    /// Logical XOR operation
25    #[must_use]
26    fn xor(&self, other: &Self) -> Self;
27
28    /// Logical NAND operation
29    #[must_use]
30    fn nand(&self, other: &Self) -> Self;
31
32    /// Logical NOR operation
33    #[must_use]
34    fn nor(&self, other: &Self) -> Self;
35}
36
37impl LogicalOps for Uncertain<bool> {
38    /// Logical AND: both conditions must be true
39    ///
40    /// # Example
41    /// ```rust
42    /// use uncertain_rs::{Uncertain, operations::LogicalOps};
43    ///
44    /// let temp = Uncertain::normal(20.0, 2.0);
45    /// let humidity = Uncertain::normal(50.0, 5.0);
46    ///
47    /// let temp_ok = temp.ge(18.0).and(&temp.le(25.0));
48    /// let humidity_ok = humidity.ge(40.0).and(&humidity.le(60.0));
49    ///
50    /// let comfortable = temp_ok.and(&humidity_ok);
51    /// if comfortable.probability_exceeds(0.8) {
52    ///     println!("Environment is comfortable");
53    /// }
54    /// ```
55    fn and(&self, other: &Self) -> Self {
56        let sample_fn1 = self.sample_fn.clone();
57        let sample_fn2 = other.sample_fn.clone();
58        Uncertain::new(move || sample_fn1() && sample_fn2())
59    }
60
61    /// Logical OR: at least one condition must be true
62    ///
63    /// # Example
64    /// ```rust
65    /// use uncertain_rs::{Uncertain, operations::{LogicalOps, Comparison}};
66    ///
67    /// let temperature = Uncertain::normal(25.0, 5.0);
68    /// let high_temp = Comparison::gt(&temperature, 30.0);
69    /// let humidity = Uncertain::normal(75.0, 10.0);
70    /// let high_humidity = Comparison::gt(&humidity, 80.0);
71    ///
72    /// let uncomfortable = LogicalOps::or(&high_temp, &high_humidity);
73    /// ```
74    fn or(&self, other: &Self) -> Self {
75        let sample_fn1 = self.sample_fn.clone();
76        let sample_fn2 = other.sample_fn.clone();
77        Uncertain::new(move || sample_fn1() || sample_fn2())
78    }
79
80    /// Logical NOT: negation of the condition
81    ///
82    /// # Example
83    /// ```rust
84    /// use uncertain_rs::{Uncertain, operations::{LogicalOps, Comparison}};
85    ///
86    /// let speed = Uncertain::normal(55.0, 5.0);
87    /// let speeding = Comparison::gt(&speed, 60.0);
88    /// let not_speeding = LogicalOps::not(&speeding);
89    /// ```
90    fn not(&self) -> Self {
91        let sample_fn = self.sample_fn.clone();
92        Uncertain::new(move || !sample_fn())
93    }
94
95    /// Logical XOR: exactly one condition must be true
96    fn xor(&self, other: &Self) -> Self {
97        let sample_fn1 = self.sample_fn.clone();
98        let sample_fn2 = other.sample_fn.clone();
99        Uncertain::new(move || sample_fn1() ^ sample_fn2())
100    }
101
102    /// Logical NAND: NOT (both conditions true)
103    fn nand(&self, other: &Self) -> Self {
104        self.and(other).not()
105    }
106
107    /// Logical NOR: NOT (either condition true)
108    fn nor(&self, other: &Self) -> Self {
109        self.or(other).not()
110    }
111}
112
113// Additional logical operations for convenience
114impl Uncertain<bool> {
115    /// Conditional logic: if-then-else for uncertain booleans
116    ///
117    /// # Example
118    /// ```rust
119    /// use uncertain_rs::Uncertain;
120    ///
121    /// let condition = Uncertain::bernoulli(0.7);
122    /// let result = condition.if_then_else(
123    ///     || Uncertain::point(10.0),
124    ///     || Uncertain::point(5.0)
125    /// );
126    /// ```
127    #[must_use]
128    pub fn if_then_else<T, F1, F2>(&self, if_true: F1, if_false: F2) -> Uncertain<T>
129    where
130        T: Shareable,
131        F1: Fn() -> Uncertain<T> + Send + Sync + 'static,
132        F2: Fn() -> Uncertain<T> + Send + Sync + 'static,
133    {
134        let sample_fn = self.sample_fn.clone();
135        Uncertain::new(move || {
136            if sample_fn() {
137                if_true().sample()
138            } else {
139                if_false().sample()
140            }
141        })
142    }
143
144    /// Implication: if A then B (equivalent to !A || B)
145    ///
146    /// # Example
147    /// ```rust
148    /// use uncertain_rs::Uncertain;
149    ///
150    /// let raining = Uncertain::bernoulli(0.3);
151    /// let umbrella = Uncertain::bernoulli(0.8);
152    ///
153    /// // If it's raining, then I should have an umbrella
154    /// let implication = raining.implies(&umbrella);
155    /// ```
156    #[must_use]
157    pub fn implies(&self, consequent: &Self) -> Uncertain<bool> {
158        self.not().or(consequent)
159    }
160
161    /// Bi-conditional: A if and only if B (equivalent to (A && B) || (!A && !B))
162    #[must_use]
163    pub fn if_and_only_if(&self, other: &Self) -> Uncertain<bool> {
164        let both_true = self.and(other);
165        let both_false = self.not().and(&other.not());
166        both_true.or(&both_false)
167    }
168
169    /// Probability that this condition is true
170    ///
171    /// # Example
172    /// ```rust
173    /// use uncertain_rs::Uncertain;
174    ///
175    /// let condition = Uncertain::bernoulli(0.7);
176    /// let prob = condition.probability(1000);
177    /// // Should be approximately 0.7
178    /// ```
179    #[must_use]
180    pub fn probability(&self, sample_count: usize) -> f64 {
181        let samples: Vec<bool> = self.take_samples(sample_count);
182        samples.iter().filter(|&&x| x).count() as f64 / samples.len() as f64
183    }
184}
185
186// Operator overloading for convenience (alternative to trait methods)
187use std::ops::{BitAnd, BitOr, Not};
188
189impl BitAnd for Uncertain<bool> {
190    type Output = Uncertain<bool>;
191
192    fn bitand(self, rhs: Self) -> Self::Output {
193        self.and(&rhs)
194    }
195}
196
197impl BitOr for Uncertain<bool> {
198    type Output = Uncertain<bool>;
199
200    fn bitor(self, rhs: Self) -> Self::Output {
201        self.or(&rhs)
202    }
203}
204
205impl Not for Uncertain<bool> {
206    type Output = Uncertain<bool>;
207
208    fn not(self) -> Self::Output {
209        LogicalOps::not(&self)
210    }
211}
212
213#[cfg(test)]
214mod tests {
215    use super::*;
216    use crate::operations::Comparison;
217
218    #[test]
219    fn test_logical_and() {
220        let always_true = Uncertain::point(true);
221        let always_false = Uncertain::point(false);
222
223        assert!(always_true.and(&always_true).sample());
224        assert!(!always_true.and(&always_false).sample());
225        assert!(!always_false.and(&always_false).sample());
226    }
227
228    #[test]
229    fn test_logical_or() {
230        let always_true = Uncertain::point(true);
231        let always_false = Uncertain::point(false);
232
233        assert!(always_true.or(&always_true).sample());
234        assert!(always_true.or(&always_false).sample());
235        assert!(!always_false.or(&always_false).sample());
236    }
237
238    #[test]
239    fn test_logical_not() {
240        let always_true = Uncertain::point(true);
241        let always_false = Uncertain::point(false);
242
243        assert!(!always_true.not().sample());
244        assert!(always_false.not().sample());
245    }
246
247    #[test]
248    fn test_operator_overloading() {
249        let a = Uncertain::point(true);
250        let b = Uncertain::point(false);
251
252        assert!(!((a.clone() & b.clone()).sample()));
253        assert!((a.clone() | b.clone()).sample());
254        assert!(!(!a).sample());
255    }
256
257    #[test]
258    fn test_complex_logical_expression() {
259        let temp = Uncertain::normal(22.0, 2.0);
260        let humidity = Uncertain::normal(50.0, 5.0);
261
262        let temp_ok = temp.within_range(20.0, 25.0);
263        let humidity_ok = humidity.within_range(40.0, 60.0);
264
265        let comfortable = temp_ok.and(&humidity_ok);
266        let uncomfortable = temp_ok.not().or(&humidity_ok.not());
267
268        // These should be negatives of each other (approximately)
269        let comfortable_prob = comfortable.probability(1000);
270        let uncomfortable_prob = uncomfortable.probability(1000);
271
272        assert!((comfortable_prob + uncomfortable_prob - 1.0).abs() < 0.1);
273    }
274
275    #[test]
276    #[allow(clippy::float_cmp)]
277    fn test_if_then_else() {
278        let condition = Uncertain::bernoulli(0.8);
279        let result = condition.if_then_else(|| Uncertain::point(10.0), || Uncertain::point(5.0));
280
281        // Should mostly return 10.0 since probability is 0.8
282        let samples: Vec<f64> = result.take_samples(1000);
283        let ten_count = samples.iter().filter(|&&x| x == 10.0).count();
284        let ten_ratio = ten_count as f64 / samples.len() as f64;
285
286        assert!((ten_ratio - 0.8).abs() < 0.1);
287    }
288
289    #[test]
290    fn test_implication() {
291        let raining = Uncertain::bernoulli(0.3);
292        let umbrella = Uncertain::bernoulli(0.9);
293
294        let implication = raining.implies(&umbrella);
295
296        // If raining (30%), then umbrella (90%)
297        // !raining (70%) || umbrella (90%) should be very high
298        let prob = implication.probability(1000);
299        assert!(prob > 0.9);
300    }
301
302    #[test]
303    fn test_shared_variable_semantics() {
304        // Test that logical operations work (shared variable semantics need further development)
305        let x = Uncertain::normal(0.0, 1.0);
306        let above = Comparison::gt(&x, 0.0);
307        let below = Comparison::lt(&x, 0.0);
308
309        // These should be mutually exclusive for the same sample in a perfect implementation
310        let both = above.and(&below);
311        let prob_both = both.probability(1000);
312
313        // Note: Current implementation doesn't fully preserve shared variable semantics
314        // In the future, this should be close to 0 for a proper implementation
315        // For now, just verify the logical operations execute without error
316        assert!((0.0..=1.0).contains(&prob_both));
317    }
318}