Skip to content

Commit

Permalink
Documented all protocols in deep causality.
Browse files Browse the repository at this point in the history
Signed-off-by: Marvin Hansen <marvin.hansen@gmail.com>
  • Loading branch information
marvin-hansen committed Jan 25, 2024
1 parent 274dc50 commit d35493f
Show file tree
Hide file tree
Showing 13 changed files with 761 additions and 25 deletions.
102 changes: 101 additions & 1 deletion deep_causality/src/protocols/assumable/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,24 @@

use crate::prelude::{DescriptionValue, EvalFn, Identifiable, NumericalValue};

/// The Assumable trait defines the interface for objects that represent
/// assumptions that can be tested and verified. Assumable types must also
/// implement Identifiable.
///
/// # Trait Methods
///
/// * `description` - Returns a description of the assumption as a
/// DescriptionValue
/// * `assumption_fn` - Returns the function that will evaluate the assumption
/// as an EvalFn
/// * `assumption_tested` - Returns whether this assumption has been tested
/// * `assumption_valid` - Returns whether this assumption is valid
/// * `verify_assumption` - Tests the assumption against the provided data and
/// returns whether it is valid
///
/// The AssumableReasoning trait provides default implementations for common
/// operations over collections of Assumable types.
///
pub trait Assumable: Identifiable {
fn description(&self) -> DescriptionValue;
fn assumption_fn(&self) -> EvalFn;
Expand All @@ -11,6 +29,29 @@ pub trait Assumable: Identifiable {
fn verify_assumption(&self, data: &[NumericalValue]) -> bool;
}

/// The AssumableReasoning trait provides default implementations for common
/// operations over collections of Assumable types.
///
/// It requires the associated type T to implement Assumable.
///
/// # Trait Methods
///
/// * `len` - Returns the number of items in the collection.
/// * `is_empty` - Returns true if the collection is empty.
/// * `get_all_items` - Returns a vector containing references to all items.
///
/// It also provides default implementations for:
///
/// * `all_assumptions_tested` - Checks if all assumptions have been tested.
/// * `all_assumptions_valid` - Checks if all assumptions are valid.
/// * `number_assumption_valid` - Returns the number of valid assumptions.
/// * `percent_assumption_valid` - Returns the percentage of valid assumptions.
/// * `verify_all_assumptions` - Verifies all assumptions against provided data.
/// * `get_all_invalid_assumptions` - Filters for invalid assumptions.
/// * `get_all_valid_assumptions` - Filters for valid assumptions.
/// * `get_all_tested_assumptions` - Filters for tested assumptions.
/// * `get_all_untested_assumptions` - Filters for untested assumptions.
///
pub trait AssumableReasoning<T>
where
T: Assumable,
Expand All @@ -20,8 +61,18 @@ where
fn is_empty(&self) -> bool;
fn get_all_items(&self) -> Vec<&T>;

// Default implementations for all other methods below.
//
// Default implementations for all other trait methods.
//

/// Checks if all assumptions in the collection have been tested.
///
/// Iterates through all items returned by `get_all_items()` and checks if
/// `assumption_tested()` returns true for each one.
///
/// Returns false if any assumption has not been tested, otherwise returns
/// true.
///
fn all_assumptions_tested(&self) -> bool {
for elem in self.get_all_items() {
if !elem.assumption_tested() {
Expand All @@ -31,6 +82,13 @@ where
true
}

/// Checks if all assumptions in the collection are valid.
///
/// Iterates through all items returned by `get_all_items()` and checks if
/// `assumption_valid()` returns true for each one.
///
/// Returns false if any assumption is invalid, otherwise returns true.
///
fn all_assumptions_valid(&self) -> bool {
for a in self.get_all_items() {
if !a.assumption_valid() {
Expand All @@ -40,44 +98,86 @@ where
true
}

/// Returns the number of valid assumptions in the collection.
///
/// Gets all items via `get_all_items()`, filters to keep only those where
/// `assumption_valid()` returns true, and returns the count as a
/// NumericalValue.
///
fn number_assumption_valid(&self) -> NumericalValue {
self.get_all_items()
.iter()
.filter(|a| a.assumption_valid())
.count() as NumericalValue
}

/// Returns the percentage of valid assumptions in the collection.
///
/// Calculates the percentage by dividing the number of valid assumptions
/// (from `number_assumption_valid()`) by the total number of assumptions
/// (from `len()`) and multiplying by 100.
///
/// Returns the percentage as a NumericalValue.
///
fn percent_assumption_valid(&self) -> NumericalValue {
(self.number_assumption_valid() / self.len() as NumericalValue) * 100.0
}

/// Verifies all assumptions in the collection against the provided data.
///
/// Iterates through all items returned by `get_all_items()` and calls
/// `verify_assumption()` on each one, passing the `data`.
///
/// This will test each assumption against the data and update the
/// `assumption_valid` and `assumption_tested` flags accordingly.
///
fn verify_all_assumptions(&self, data: &[NumericalValue]) {
for a in self.get_all_items() {
a.verify_assumption(data);
}
}

/// Returns a vector containing references to all invalid assumptions.
///
/// Gets all items via `get_all_items()`, filters to keep only those where
/// `assumption_valid()` returns false, and collects into a vector.
///
fn get_all_invalid_assumptions(&self) -> Vec<&T> {
self.get_all_items()
.into_iter()
.filter(|a| !a.assumption_valid())
.collect()
}

/// Returns a vector containing references to all valid assumptions.
///
/// Gets all items via `get_all_items()`, filters to keep only those where
/// `assumption_valid()` returns true, and collects into a vector.
///
fn get_all_valid_assumptions(&self) -> Vec<&T> {
self.get_all_items()
.into_iter()
.filter(|a| a.assumption_valid())
.collect()
}

/// Returns a vector containing references to all tested assumptions.
///
/// Gets all items via `get_all_items()`, filters to keep only those where
/// `assumption_tested()` returns true, and collects into a vector.
///
fn get_all_tested_assumptions(&self) -> Vec<&T> {
self.get_all_items()
.into_iter()
.filter(|a| a.assumption_tested())
.collect()
}

/// Returns a vector containing references to all untested assumptions.
///
/// Gets all items via `get_all_items()`, filters to keep only those where
/// `assumption_tested()` returns false, and collects into a vector.
///
fn get_all_untested_assumptions(&self) -> Vec<&T> {
self.get_all_items()
.into_iter()
Expand Down
87 changes: 86 additions & 1 deletion deep_causality/src/protocols/causable/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,21 @@ use std::collections::HashMap;
use crate::errors::CausalityError;
use crate::prelude::{Identifiable, IdentificationValue, NumericalValue};

/// The Causable trait defines the core behavior for causal reasoning.
///
/// It requires implementing the Identifiable trait.
///
/// # Trait Methods
///
/// * `explain` - Returns an explanation of the cause as a String.
/// * `is_active` - Returns true if this cause is currently active.
/// * `is_singleton` - Returns true if this cause acts on a single data point.
/// * `verify_single_cause` - Verifies this cause against a single data point.
/// * `verify_all_causes` - Verifies this cause against multiple data points.
///
/// `verify_single_cause` and `verify_all_causes` return a Result indicating
/// if the cause was validated or not.
///
pub trait Causable: Identifiable {
fn explain(&self) -> Result<String, CausalityError>;
fn is_active(&self) -> bool;
Expand All @@ -20,6 +35,24 @@ pub trait Causable: Identifiable {
) -> Result<bool, CausalityError>;
}

/// The CausableReasoning trait provides default implementations for reasoning over collections of Causable items.
///
/// It requires the generic type T to implement the Causable trait.
///
/// The trait provides default methods for:
///
/// - Getting active/inactive causes
/// - Counting active causes
/// - Calculating percentage of active causes
/// - Explaining all causes
/// - Verifying causes against data
///
/// The `reason_all_causes` method verifies all causes in the collection against the provided data,
/// using the cause's `is_singleton` method to determine whether to call `verify_single_cause` or
/// `verify_all_causes`.
///
/// An index is emulated for the data to enable singleton cause verification.
///
pub trait CausableReasoning<T>
where
T: Causable,
Expand All @@ -30,8 +63,17 @@ where
fn to_vec(&self) -> Vec<T>;
fn get_all_items(&self) -> Vec<&T>;

//
// Default implementations for all other methods are provided below.

//

/// Checks if all causes in the collection are active.
///
/// Iterates through all causes via `get_all_items()` and returns false
/// if any cause's `is_active()` method returns false.
///
/// If all causes are active, returns true.
///
fn get_all_causes_true(&self) -> bool {
for cause in self.get_all_items() {
if !cause.is_active() {
Expand All @@ -42,33 +84,69 @@ where
true
}

/// Returns a vector containing references to all active causes.
///
/// Gets all causes via `get_all_items()`, filters to keep only those where
/// `is_active()` returns true, and collects into a vector.
///
fn get_all_active_causes(&self) -> Vec<&T> {
self.get_all_items()
.into_iter()
.filter(|cause| cause.is_active())
.collect()
}

/// Returns a vector containing references to all inactive causes.
///
/// Gets all causes via `get_all_items()`, filters to keep only those where
/// `is_active()` returns false, and collects into a vector.
///
fn get_all_inactive_causes(&self) -> Vec<&T> {
self.get_all_items()
.into_iter()
.filter(|cause| !cause.is_active())
.collect()
}

/// Returns the number of active causes.
///
/// Gets all causes via `get_all_items()`, filters to keep only active ones,
/// counts them, and returns the count as a NumericalValue.
///
fn number_active(&self) -> NumericalValue {
self.get_all_items()
.iter()
.filter(|c| c.is_active())
.count() as NumericalValue
}

/// Calculates the percentage of active causes.
///
/// Gets the number of active causes via `number_active()`.
/// Gets the total number of causes via `len()`.
/// Divides the active count by the total.
/// Multiplies by 100 to get a percentage.
/// Returns the result as a NumericalValue.
///
fn percent_active(&self) -> NumericalValue {
let count = self.number_active();
let total = self.len() as NumericalValue;
(count / total) * (100 as NumericalValue)
}

/// Verifies all causes in the collection against the provided data.
///
/// Returns an error if the collection is empty.
///
/// Iterates through all causes, using the cause's `is_singleton()` method
/// to determine whether to call `verify_single_cause()` or `verify_all_causes()`.
///
/// For singleton causes, the data index is emulated to enable lookup by index.
///
/// If any cause fails verification, returns Ok(false).
///
/// If all causes pass verification, returns Ok(true).
///
fn reason_all_causes(&self, data: &[NumericalValue]) -> Result<bool, CausalityError> {
if self.is_empty() {
return Err(CausalityError("Causality collection is empty".into()));
Expand Down Expand Up @@ -97,6 +175,13 @@ where
Ok(true)
}

/// Generates an explanation by concatenating the explain() text of all causes.
///
/// Calls explain() on each cause and unwraps the result.
/// Concatenates the explanations by inserting newlines between each one.
///
/// Returns the concatenated explanation string.
///
fn explain(&self) -> String {
let mut explanation = String::new();
for cause in self.get_all_items() {
Expand Down
41 changes: 40 additions & 1 deletion deep_causality/src/protocols/causable_graph/graph.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,32 @@ use crate::errors::{CausalGraphIndexError, CausalityGraphError};
use crate::prelude::{Causable, NumericalValue};
use crate::protocols::causable_graph::CausalGraph;

/// The CausableGraph trait defines the core interface for a causal graph.
///
/// It builds on the CausalGraph data structure.
///
/// Provides methods for:
///
/// - Adding a root node
/// - Adding/removing nodes
/// - Adding/removing edges
/// - Accessing nodes/edges
/// - Getting graph metrics like size and active nodes
///
/// The get_graph() method returns the underlying CausalGraph instance.
/// This enables default implementations for reasoning and explaining.
///
/// Also includes a default implementation of shortest_path() using the
/// underlying CausalGraph.
///
/// Nodes are indexed by usize.
///
/// Edges are added by specifying the node indices.
///
/// Nodes must be unique. Edges can be duplicated.
///
/// Errors on invalid node/edge indices.
///
pub trait CausableGraph<T>
where
T: Causable + PartialEq,
Expand Down Expand Up @@ -48,7 +74,20 @@ where
fn number_edges(&self) -> usize;
fn number_nodes(&self) -> usize;

/// Default implementation for shortest path algorithm
/// Default implementation for shortest path algorithm.
///
/// Finds the shortest path between two node indices in the graph.
///
/// start_index: The start node index
/// stop_index: The target node index
///
/// Returns:
/// - Ok(Vec<usize>): The node indices of the shortest path
/// - Err(CausalityGraphError): If no path exists
///
/// Checks if start and stop nodes are identical and early returns error.
/// Otherwise calls shortest_path() on the underlying CausalGraph.
///
fn get_shortest_path(
&self,
start_index: usize,
Expand Down
Loading

0 comments on commit d35493f

Please sign in to comment.