uncertain_rs/operations/
logical.rs1#![allow(clippy::cast_precision_loss)]
2
3use crate::Uncertain;
4use crate::traits::Shareable;
5
6pub trait LogicalOps {
12 #[must_use]
14 fn and(&self, other: &Self) -> Self;
15
16 #[must_use]
18 fn or(&self, other: &Self) -> Self;
19
20 #[must_use]
22 fn not(&self) -> Self;
23
24 #[must_use]
26 fn xor(&self, other: &Self) -> Self;
27
28 #[must_use]
30 fn nand(&self, other: &Self) -> Self;
31
32 #[must_use]
34 fn nor(&self, other: &Self) -> Self;
35}
36
37impl LogicalOps for Uncertain<bool> {
38 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 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 fn not(&self) -> Self {
91 let sample_fn = self.sample_fn.clone();
92 Uncertain::new(move || !sample_fn())
93 }
94
95 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 fn nand(&self, other: &Self) -> Self {
104 self.and(other).not()
105 }
106
107 fn nor(&self, other: &Self) -> Self {
109 self.or(other).not()
110 }
111}
112
113impl Uncertain<bool> {
115 #[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 #[must_use]
157 pub fn implies(&self, consequent: &Self) -> Uncertain<bool> {
158 self.not().or(consequent)
159 }
160
161 #[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 #[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
186use 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 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 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 let prob = implication.probability(1000);
299 assert!(prob > 0.9);
300 }
301
302 #[test]
303 fn test_shared_variable_semantics() {
304 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 let both = above.and(&below);
311 let prob_both = both.probability(1000);
312
313 assert!((0.0..=1.0).contains(&prob_both));
317 }
318}