deep_causality/traits/observable/
mod.rs

1/*
2 * SPDX-License-Identifier: MIT
3 * Copyright (c) "2025" . The DeepCausality Authors and Contributors. All Rights Reserved.
4 */
5
6use std::fmt::Debug;
7
8use crate::{Identifiable, NumericalValue};
9
10/// Observable trait for objects that can be observed.
11///
12/// Requires:
13///
14/// - Debug - for debug printing
15/// - Identifiable - for unique identification
16///
17/// Provides methods:
18///
19/// - observation() - gets the numerical observation value
20/// - observed_effect() - gets the observed effect value
21/// - effect_observed() - checks if observation meets threshold and matches effect
22///
23/// effect_observed() checks:
24///
25/// - observation >= target_threshold
26/// - observed_effect == target_effect
27///
28pub trait Observable: Debug + Identifiable {
29    fn observation(&self) -> NumericalValue;
30    fn observed_effect(&self) -> NumericalValue;
31
32    /// Checks if the observed effect meets the target threshold and effect.
33    ///
34    /// Returns true if:
35    ///
36    /// - observation() >= target_threshold
37    /// - observed_effect() == target_effect
38    ///
39    /// Otherwise returns false.
40    ///
41    fn effect_observed(
42        &self,
43        target_threshold: NumericalValue,
44        target_effect: NumericalValue,
45    ) -> bool {
46        (self.observation() >= target_threshold) && (self.observed_effect() == target_effect)
47    }
48}
49
50/// ObservableReasoning trait provides reasoning methods for collections of Observable items.
51///
52/// Where T: Observable
53///
54/// Provides methods:
55///
56/// - len() - number of items
57/// - is_empty() - checks if empty
58/// - get_all_items() - returns all items
59///
60/// - number_observation() - counts items meeting threshold and effect
61/// - number_non_observation() - counts items not meeting criteria
62/// - percent_observation() - % of items meeting criteria
63/// - percent_non_observation() - % of items not meeting criteria
64///
65/// Uses T's effect_observed() method to check criteria.
66///
67pub trait ObservableReasoning<T>
68where
69    T: Observable,
70{
71    // Compiler generated methods using macros.
72    fn len(&self) -> usize;
73    fn is_empty(&self) -> bool;
74    fn get_all_items(&self) -> Vec<&T>;
75
76    //
77    // Default implementations.
78    //
79
80    /// Counts the number of observations meeting the criteria.
81    ///
82    /// Iterates through all items and filters based on:
83    ///
84    /// - item.effect_observed(target_threshold, target_effect)
85    ///
86    /// Then returns the count.
87    ///
88    fn number_observation(
89        &self,
90        target_threshold: NumericalValue,
91        target_effect: NumericalValue,
92    ) -> NumericalValue {
93        self.get_all_items()
94            .iter()
95            .filter(|o| o.effect_observed(target_threshold, target_effect))
96            .count() as NumericalValue
97    }
98
99    /// Counts the number of non-observations based on the criteria.
100    ///
101    /// Calculates this by:
102    ///
103    /// - self.len() - total number of items
104    /// - minus number_observation() count
105    ///
106    /// Returns the number of items not meeting criteria.
107    ///
108    fn number_non_observation(
109        &self,
110        target_threshold: NumericalValue,
111        target_effect: NumericalValue,
112    ) -> NumericalValue {
113        self.len() as NumericalValue - self.number_observation(target_threshold, target_effect)
114    }
115
116    /// Calculates the percentage of observations meeting the criteria.
117    ///
118    /// Divides the number_observation count by the total number of items.
119    ///
120    /// Returns value between 0.0 and 1.0 as a percentage.
121    ///
122    fn percent_observation(
123        &self,
124        target_threshold: NumericalValue,
125        target_effect: NumericalValue,
126    ) -> NumericalValue {
127        if self.is_empty() {
128            return 0.0;
129        }
130
131        self.number_observation(target_threshold, target_effect) / self.len() as NumericalValue
132    }
133
134    /// Calculates the percentage of non-observations based on the criteria.
135    ///
136    /// Returns 1.0 minus the percent_observation.
137    ///
138    fn percent_non_observation(
139        &self,
140        target_threshold: NumericalValue,
141        target_effect: NumericalValue,
142    ) -> NumericalValue {
143        1.0 - self.percent_observation(target_threshold, target_effect)
144    }
145}