From d35493f719e03e93c80c3565c77517a66889109f Mon Sep 17 00:00:00 2001 From: Marvin Hansen Date: Thu, 25 Jan 2024 09:45:59 +0800 Subject: [PATCH] Documented all protocols in deep causality. Signed-off-by: Marvin Hansen --- deep_causality/src/protocols/assumable/mod.rs | 102 ++++++++- deep_causality/src/protocols/causable/mod.rs | 87 +++++++- .../src/protocols/causable_graph/graph.rs | 41 +++- .../causable_graph/graph_explaining.rs | 98 ++++++++- .../causable_graph/graph_reasoning.rs | 25 ++- .../causable_graph/graph_reasoning_utils.rs | 11 + .../src/protocols/causable_graph/mod.rs | 40 +++- .../src/protocols/contextuable/mod.rs | 54 +++++ .../src/protocols/contextuable_graph/mod.rs | 42 ++++ .../src/protocols/identifiable/mod.rs | 5 + deep_causality/src/protocols/indexable/mod.rs | 15 ++ deep_causality/src/protocols/inferable/mod.rs | 193 +++++++++++++++++- .../src/protocols/observable/mod.rs | 73 +++++++ 13 files changed, 761 insertions(+), 25 deletions(-) diff --git a/deep_causality/src/protocols/assumable/mod.rs b/deep_causality/src/protocols/assumable/mod.rs index 54c14201..bae9c159 100644 --- a/deep_causality/src/protocols/assumable/mod.rs +++ b/deep_causality/src/protocols/assumable/mod.rs @@ -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; @@ -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 where T: Assumable, @@ -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() { @@ -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() { @@ -40,6 +98,12 @@ 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() @@ -47,16 +111,37 @@ where .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() @@ -64,6 +149,11 @@ where .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() @@ -71,6 +161,11 @@ where .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() @@ -78,6 +173,11 @@ where .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() diff --git a/deep_causality/src/protocols/causable/mod.rs b/deep_causality/src/protocols/causable/mod.rs index 7ef09e88..76e89181 100644 --- a/deep_causality/src/protocols/causable/mod.rs +++ b/deep_causality/src/protocols/causable/mod.rs @@ -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; fn is_active(&self) -> bool; @@ -20,6 +35,24 @@ pub trait Causable: Identifiable { ) -> Result; } +/// 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 where T: Causable, @@ -30,8 +63,17 @@ where fn to_vec(&self) -> Vec; 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() { @@ -42,6 +84,11 @@ 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() @@ -49,6 +96,11 @@ where .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() @@ -56,6 +108,11 @@ where .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() @@ -63,12 +120,33 @@ where .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 { if self.is_empty() { return Err(CausalityError("Causality collection is empty".into())); @@ -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() { diff --git a/deep_causality/src/protocols/causable_graph/graph.rs b/deep_causality/src/protocols/causable_graph/graph.rs index 9026b8c3..30c33d9b 100644 --- a/deep_causality/src/protocols/causable_graph/graph.rs +++ b/deep_causality/src/protocols/causable_graph/graph.rs @@ -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 where T: Causable + PartialEq, @@ -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): 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, diff --git a/deep_causality/src/protocols/causable_graph/graph_explaining.rs b/deep_causality/src/protocols/causable_graph/graph_explaining.rs index a1c2c35a..07c63a90 100644 --- a/deep_causality/src/protocols/causable_graph/graph_explaining.rs +++ b/deep_causality/src/protocols/causable_graph/graph_explaining.rs @@ -5,10 +5,51 @@ use ultragraph::prelude::*; use crate::prelude::{Causable, CausableGraph, CausalityGraphError}; +/// The CausableGraphExplaining trait provides methods to generate +/// natural language explanations from a causal graph. +/// +/// It requires the graph to implement CausableGraph, where nodes +/// are Causable (can explain themselves). +/// +/// Provides methods to: +/// +/// - Explain between two node indices +/// - Explain the full graph +/// - Explain a subgraph +/// - Explain the shortest path between nodes +/// +/// Uses a depth-first search to traverse the graph and collect +/// explanations. +/// +/// The explain_from_to_cause() method is the core implementation +/// that supports the other methods. +/// pub trait CausableGraphExplaining: CausableGraph where T: Causable + PartialEq, { + /// Generates an explanation by traversing the graph from start_index to stop_index. + /// + /// Uses a depth-first search to visit all nodes on the path. + /// + /// start_index: The index of the starting node + /// stop_index: The index of the target node + /// + /// Returns: + /// - Ok(String): The concatenated explanation if successful + /// - Err(CausalityGraphError): If indices are invalid or traversal fails + /// + /// Gets the explanation from start node. + /// Gets start node's neighbors and adds them to stack. + /// + /// While stack is not empty: + /// - Get next node from stack top + /// - Get its explanation and append + /// - If node is stop_index, return result + /// - Else get node's neighbors and push to stack + /// + /// This traverses all nodes from start to stop depth-first. + /// fn explain_from_to_cause( &self, start_index: usize, @@ -76,8 +117,19 @@ where Ok(explanation) } - /// Explains the line of reasoning across the entire graph. - /// Returns: String representing the explanation or an error + /// Explains the full causal graph from the root node to the last node. + /// + /// Checks that the graph is not empty and contains a root node. + /// + /// Gets the root node index and last node index. + /// + /// Calls explain_from_to_cause() with the root and last node indices + /// to generate the full explanation. + /// + /// Returns: + /// - Ok(String): The full graph explanation if successful + /// - Err(CausalityGraphError): If graph is empty or lacks a root node + /// fn explain_all_causes(&self) -> Result { if self.is_empty() { return Err(CausalityGraphError("Graph is empty".to_string())); @@ -101,8 +153,18 @@ where /// Explains the line of reasoning across a subgraph starting from a given node index until /// the end of the graph. - /// index: NodeIndex - index of the starting node - /// Returns: String representing the explanation or an error + /// + /// start_index: The index of the starting node + /// + /// Gets the index of the last node. + /// + /// Calls explain_from_to_cause() with the start index and last index + /// to generate the subgraph explanation. + /// + /// Returns: + /// - Ok(String): The subgraph explanation if successful + /// - Err(CausalityGraphError): If graph is empty + /// fn explain_subgraph_from_cause( &self, start_index: usize, @@ -119,11 +181,21 @@ where } } - /// Explains the line of reasoning of the shortest sub-graph + /// Explains the line of reasoning of the shortest path /// between a start and stop cause. - /// start_index: NodeIndex - index of the start cause - /// stop_index: NodeIndex - index of the stop cause - /// Returns: String representing the explanation or an error + /// + /// start_index: The start node index + /// stop_index: The target node index + /// + /// Gets the shortest path between the indices using get_shortest_path(). + /// + /// Iterates the path indices: + /// - Get each node + /// - Append its explanation to the result + /// + /// Returns: + /// - Ok(String): The concatenated shortest path explanation + /// - Err(CausalityGraphError): If indices invalid or no path found fn explain_shortest_path_between_causes( &self, start_index: usize, @@ -162,6 +234,16 @@ where } } +/// Appends a string to another string with newlines before and after. +/// +/// s1: The string to append to +/// s2: The string to append +/// +/// Inserts a newline, then the s2 string formatted with a bullet point, +/// then another newline before returning the modified s1. +/// +/// This allows cleanly appending explain() strings with spacing. +/// fn append_string<'l>(s1: &'l mut String, s2: &'l str) -> &'l str { s1.push('\n'); s1.push_str(format!(" * {}", s2).as_str()); diff --git a/deep_causality/src/protocols/causable_graph/graph_reasoning.rs b/deep_causality/src/protocols/causable_graph/graph_reasoning.rs index 64fcf32e..7b5fa857 100644 --- a/deep_causality/src/protocols/causable_graph/graph_reasoning.rs +++ b/deep_causality/src/protocols/causable_graph/graph_reasoning.rs @@ -125,7 +125,30 @@ where } } - // Algo inspired by simple path https://github.com/petgraph/petgraph/blob/master/src/algo/simple_paths.rs + /// Reasons over the graph from start_index to stop_index. + /// + /// start_index: Node index to start reasoning from + /// stop_index: Node index to end reasoning + /// data: Observations to apply to nodes + /// data_index: Optional index map if data indices differ from node indices + /// + /// Gets start node and verifies it. If false, returns false. + /// + /// Uses a stack to traverse nodes depth-first: + /// - Get node's children and push to stack + /// - Pop next node and get observations + /// - Verify node and if false, return false + /// - If node is stop_index, return true + /// - Else, push node's children to stack + /// + /// Returns: + /// - Ok(bool): True if all nodes verify, False if any node fails + /// - Err(CausalityGraphError): On invalid indices or empty data + /// + /// Traverses nodes depth-first, verifying each one. + /// If any node fails, returns false. If all pass, returns true. + /// + /// Algo inspired by simple path https://github.com/petgraph/petgraph/blob/master/src/algo/simple_paths.rs fn reason_from_to_cause( &self, start_index: usize, diff --git a/deep_causality/src/protocols/causable_graph/graph_reasoning_utils.rs b/deep_causality/src/protocols/causable_graph/graph_reasoning_utils.rs index cf98f265..e8ad513d 100644 --- a/deep_causality/src/protocols/causable_graph/graph_reasoning_utils.rs +++ b/deep_causality/src/protocols/causable_graph/graph_reasoning_utils.rs @@ -5,6 +5,17 @@ use std::collections::HashMap; use crate::prelude::{IdentificationValue, NumericalValue}; +/// Gets the observation value for a cause from the given data. +/// +/// cause_id: The ID of the cause to get observations for +/// data: Array of observation values +/// data_index: Optional map from node IDs to indices into data +/// +/// If data_index is provided, uses it to lookup index for cause_id. +/// Else assumes cause_id maps directly to index in data. +/// +/// Returns the observation value for the cause. +/// pub(crate) fn get_obs<'a>( cause_id: IdentificationValue, data: &'a [NumericalValue], diff --git a/deep_causality/src/protocols/causable_graph/mod.rs b/deep_causality/src/protocols/causable_graph/mod.rs index 51f732d6..6c2eb90d 100644 --- a/deep_causality/src/protocols/causable_graph/mod.rs +++ b/deep_causality/src/protocols/causable_graph/mod.rs @@ -15,6 +15,28 @@ mod graph_reasoning_utils; // Type alias is shared between trait and implementation pub(crate) type CausalGraph = UltraGraph; +/// The CausableGraph trait defines the interface for a causal graph data structure. +/// +/// It operates on generic type T which must implement the Causable trait. +/// +/// 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 root node is a special "start" node for causal reasoning. +/// +/// 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 where T: Causable + PartialEq, @@ -54,8 +76,22 @@ where fn number_nodes(&self) -> usize; } -/// Describes signatures for causal reasoning and explaining -/// in causality hyper graph. +/// The CausableGraphReasoning trait extends CausableGraph with reasoning methods. +/// +/// Provides explain and reason methods for: +/// - The entire graph +/// - Subgraphs starting from a given node +/// - Shortest path between two nodes +/// - Single nodes +/// +/// The explain methods return a string explanation. +/// +/// The reason methods take input data and return a Result indicating +/// if reasoning succeeded or failed. +/// +/// An optional data_index can be provided to map data to nodes when the indices +/// differ. +/// pub trait CausableGraphReasoning: CausableGraph where T: Causable + PartialEq, diff --git a/deep_causality/src/protocols/contextuable/mod.rs b/deep_causality/src/protocols/contextuable/mod.rs index 7ba9a0fb..a4174b51 100644 --- a/deep_causality/src/protocols/contextuable/mod.rs +++ b/deep_causality/src/protocols/contextuable/mod.rs @@ -7,6 +7,18 @@ use crate::prelude::{ContextoidType, Identifiable, TimeScale}; pub trait Datable: Identifiable {} +/// Trait for types that have temporal properties. +/// +/// V: Numeric type for time unit value +/// +/// Requires: +/// - Identifiable: Has a unique ID +/// - V implements math ops: Add, Sub, Mul +/// +/// Provides: +/// - time_scale(): Get the time scale (e.g. seconds, minutes) +/// - time_unit(): Get the time unit value for this item +/// pub trait Temporable: Identifiable where V: Default + Add + Sub + Mul, @@ -15,6 +27,19 @@ where fn time_unit(&self) -> &V; } +/// Trait for types that have spatial properties. +/// +/// V: Numeric type for spatial unit values +/// +/// Requires: +/// - Identifiable: Has a unique ID +/// - V implements math ops: Add, Sub, Mul +/// +/// Provides: +/// - x(): Get x spatial dimension value +/// - y(): Get y spatial dimension value +/// - z(): Get z spatial dimension value +/// pub trait Spatial: Identifiable where V: Default + Add + Sub + Mul, @@ -24,6 +49,19 @@ where fn z(&self) -> &V; } +/// Trait for types with spatial and temporal properties. +/// +/// V: Numeric type for dimension values +/// +/// Requires: +/// - Identifiable: Has unique ID +/// - Spatial: Provides x, y, z spatial dims +/// - Temporable: Provides time scale and unit +/// - V implements math ops: Add, Sub, Mul +/// +/// Provides: +/// - t(): Get value for 4th (temporal) dimension +/// pub trait SpaceTemporal: Identifiable + Spatial + Temporable where V: Default + Add + Sub + Mul, @@ -31,6 +69,22 @@ where fn t(&self) -> &V; // returns 4th dimension, t } +/// Trait for context-aware types with spatial, temporal, and datable properties. +/// +/// D: Datable trait object +/// S: Spatial trait object +/// T: Temporable trait object +/// ST: SpaceTemporal trait object +/// V: Numeric type for dimension values +/// +/// Requires: +/// - Identifiable: Has unique ID +/// - D, S, T, ST implement respective traits +/// - V implements math ops: Add, Sub, Mul +/// +/// Provides: +/// - vertex_type(): Get the vertex type (D, S, T, ST) +/// pub trait Contextuable: Identifiable where D: Datable, diff --git a/deep_causality/src/protocols/contextuable_graph/mod.rs b/deep_causality/src/protocols/contextuable_graph/mod.rs index 37c37057..3b70ac7b 100644 --- a/deep_causality/src/protocols/contextuable_graph/mod.rs +++ b/deep_causality/src/protocols/contextuable_graph/mod.rs @@ -6,6 +6,25 @@ use std::ops::{Add, Mul, Sub}; use crate::errors::ContextIndexError; use crate::prelude::{Contextoid, Datable, RelationKind, SpaceTemporal, Spatial, Temporable}; +/// Trait for graph containing context-aware nodes. +/// +/// D: Datable trait object +/// S: Spatial trait object +/// T: Temporable trait object +/// ST: SpaceTemporal trait object +/// V: Numeric type for dimension values +/// +/// Provides methods for: +/// - Adding/removing nodes and edges +/// - Checking if nodes/edges exist +/// - Getting node references +/// - Getting graph size and counts +/// +/// Nodes are Contextoid objects implementing required traits. +/// Edges have a relation kind weight. +/// +/// Methods return Result or Option types for error handling. +/// pub trait ContextuableGraph<'l, D, S, T, ST, V> where D: Datable, @@ -32,6 +51,29 @@ where fn edge_count(&self) -> usize; } +/// Trait for poly-contextuable causal graphs. +/// By default, the context graph is assumed to be a single-context graph. +/// +/// This trait supports multiple contexts by extending the ContextuableGraph trait. +/// +/// Extends ContextuableGraph trait with methods for: +/// +/// - Creating and managing additional "contexts" +/// - Setting a current context ID +/// - Context-specific node/edge methods +/// +/// Provides methods for: +/// +/// - Creating new contexts +/// - Checking if a context ID exists +/// - Getting/setting current context ID +/// - Context-specific node and edge methods +/// +/// Nodes are Contextoid objects implementing required traits. +/// Edges have a relation kind weight. +/// +/// Methods return Result or Option types for error handling. +/// pub trait ExtendableContextuableGraph<'l, D, S, T, ST, V> where D: Datable, diff --git a/deep_causality/src/protocols/identifiable/mod.rs b/deep_causality/src/protocols/identifiable/mod.rs index bb8233c7..3e2fb178 100644 --- a/deep_causality/src/protocols/identifiable/mod.rs +++ b/deep_causality/src/protocols/identifiable/mod.rs @@ -1,6 +1,11 @@ // SPDX-License-Identifier: MIT // Copyright (c) "2023" . The DeepCausality Authors. All Rights Reserved. +/// Trait for types that have a unique identifier. +/// +/// Provides: +/// - id(): Get the unique ID for this item +/// pub trait Identifiable { fn id(&self) -> u64; } diff --git a/deep_causality/src/protocols/indexable/mod.rs b/deep_causality/src/protocols/indexable/mod.rs index 97145ef0..f1df44d3 100644 --- a/deep_causality/src/protocols/indexable/mod.rs +++ b/deep_causality/src/protocols/indexable/mod.rs @@ -1,6 +1,21 @@ // SPDX-License-Identifier: MIT // Copyright (c) "2024" . The DeepCausality Authors. All Rights Reserved. +/// Trait for types that can be indexed. +/// +/// Provides methods for: +/// +/// - get_index(): Lookup an index by key +/// - set_index(): Insert/update a key-index mapping +/// +/// Maintains separate current and previous index maps. +/// +/// get_index() and set_index() take a `current` arg to +/// specify which index map to use. +/// +/// Allows indexing items by an usized integer key. Enables mapping +/// between item IDs and indices. +/// pub trait Indexable { /// Gets the index for the provided key from either the current or previous /// index map, depending on the value of `current`. diff --git a/deep_causality/src/protocols/inferable/mod.rs b/deep_causality/src/protocols/inferable/mod.rs index 30bbfa69..ea8482f5 100644 --- a/deep_causality/src/protocols/inferable/mod.rs +++ b/deep_causality/src/protocols/inferable/mod.rs @@ -7,6 +7,24 @@ use std::fmt::Debug; use crate::prelude::{DescriptionValue, Identifiable, NumericalValue}; use crate::utils::math_utils::abs_num; +/// Trait for inferable types with causal reasoning properties. +/// +/// Provides properties for: +/// +/// - question: Text description +/// - observation: Numerical observation value +/// - threshold: Minimum observation value +/// - effect: Expected effect value +/// - target: Target value to compare effect against +/// +/// Provides methods for: +/// +/// - conjoint_delta(): Estimate of unobserved factors +/// - is_inferable(): Check if inference is valid +/// - is_inverse_inferable(): Check inverse inference +/// +/// Requires approximate float equality util function. +/// pub trait Inferable: Debug + Identifiable { fn question(&self) -> DescriptionValue; fn observation(&self) -> NumericalValue; @@ -14,15 +32,47 @@ pub trait Inferable: Debug + Identifiable { fn effect(&self) -> NumericalValue; fn target(&self) -> NumericalValue; + /// Calculates the conjoint delta for this item. + /// + /// The conjoint delta estimates the effect of unobserved factors. + /// + /// It is calculated as: + /// + /// 1.0 - observation + /// + /// Where: + /// + /// - observation is the numerical observation value + /// + /// Finally, the absolute value is taken. + /// fn conjoint_delta(&self) -> NumericalValue { abs_num((1.0) - self.observation()) } + /// Checks if inference is valid for this item. + /// + /// Returns true if: + /// + /// - Observation is greater than threshold + /// - Effect is approximately equal to target + /// + /// Uses 4 decimal places for float comparison. + /// fn is_inferable(&self) -> bool { (self.observation().total_cmp(&self.threshold()) == Ordering::Greater) && approx_equal(self.effect(), self.target(), 4) } + /// Checks if inverse inference is valid for this item. + /// + /// Returns true if: + /// + /// - Observation is less than threshold + /// - Effect is approximately equal to target + /// + /// Uses 4 decimal places for float comparison. + /// fn is_inverse_inferable(&self) -> bool { (self.observation().total_cmp(&self.threshold()) == Ordering::Less) && approx_equal(self.effect(), self.target(), 4) @@ -39,6 +89,24 @@ fn approx_equal(a: f64, b: f64, decimal_places: u8) -> bool { a == b } +/// Trait providing reasoning methods for collections of Inferable items. +/// +/// Provides methods for: +/// +/// - Filtering inferable/non-inferable items +/// - Checking if all items are inferable +/// - Calculating inferability metrics +/// - conjoint_delta +/// - Counts +/// - Percentages +/// +/// Requires Inferable items that implement: +/// +/// - is_inferable() +/// - is_inverse_inferable() +/// +/// Provides default implementations using those methods. +/// pub trait InferableReasoning where T: Inferable, @@ -48,7 +116,14 @@ where fn is_empty(&self) -> bool; fn get_all_items(&self) -> Vec<&T>; + // // Default implementations. + // + + /// Returns a vector containing all inferable items. + /// + /// Filters the full set of items based on is_inferable(). + /// fn get_all_inferable(&self) -> Vec<&T> { self.get_all_items() .into_iter() @@ -56,6 +131,10 @@ where .collect() } + /// Returns a vector containing all inverse inferable items. + /// + /// Filters the full set of items based on is_inverse_inferable(). + /// fn get_all_inverse_inferable(&self) -> Vec<&T> { self.get_all_items() .into_iter() @@ -63,6 +142,16 @@ where .collect() } + /// Returns a vector containing all non-inferable items. + /// + /// An item is non-inferable if it is both inferable and inverse inferable, + /// which makes it undecidable. + /// + /// Filters the full set of items based on: + /// + /// - is_inferable() + /// - is_inverse_inferable() + /// fn get_all_non_inferable(&self) -> Vec<&T> { // must be either or, but cannot be both b/c that would be undecidable hence non-inferable self.get_all_items() @@ -71,7 +160,15 @@ where .collect() } - /// returns true if all elements are inferable + /// Checks if all items in the collection are inferable. + /// + /// Iterates through all items and checks is_inferable() on each. + /// + /// Returns: + /// + /// - true if all items are inferable + /// - false if any item is not inferable + /// fn all_inferable(&self) -> bool { for element in self.get_all_items() { if !element.is_inferable() { @@ -81,7 +178,15 @@ where true } - /// returns true if all elements are inverse inferable + /// Checks if all items in the collection are inverse inferable. + /// + /// Iterates through all items and checks is_inverse_inferable() on each. + /// + /// Returns: + /// + /// - true if all items are inverse inferable + /// - false if any item is not inverse inferable + /// fn all_inverse_inferable(&self) -> bool { for element in self.get_all_items() { if !element.is_inverse_inferable() { @@ -91,7 +196,20 @@ where true } - /// returns true if all elements are NON-inferable + /// Checks if all items in the collection are non-inferable. + /// + /// An item is non-inferable if it is both inferable and inverse inferable. + /// + /// Iterates through all items and checks: + /// + /// - is_inferable() + /// - is_inverse_inferable() + /// + /// Returns: + /// + /// - true if any item is both inferable and inverse inferable + /// - false if no items meet that criteria + /// fn all_non_inferable(&self) -> bool { for element in self.get_all_items() { // must be either or, but cannot be both b/c that would be undecidable hence non-inferable @@ -102,8 +220,33 @@ where false } - /// The conjoint delta estimates the effect of those unobserverd conjoint factors. - /// conjoint_delta = abs(sum_cbservation/total)) + /// Estimates the ConJointDelta for this collection. + /// + /// The conjoint delta represents the combined effect of + /// unobserved factors and is used to determine the strength of the joint causal relationship. + /// + /// It is calculated as the difference (delta) between + /// the combined (joint) observation (conjecture) and a theoretical 100% + /// if the conjecture were to explain all observations, hence the name ConJointDelta: + /// + /// 1.0 - (sum of observations / total items) + /// + /// Where: + /// + /// - sum of observations = total items - non-inferable items + /// - total items = length of the collection + /// - non-inferable items = count of non-inferable items + /// + /// Finally, the absolute value is taken. + /// + /// The resulting value is interpreted as following: + /// + /// Higher: THe higher the conjoint delta, the more unobserved factors are present and + /// observed factors are not explaining the observation. Therefore, the joint causal relationship is weaker + /// and doesn't explain much of the observation. + /// + /// Lower: The lower the conjoint delta, the more the observed factors explain the observation. + /// Therefore, the joint causal relationship is stronger as it explains more of the observation. fn conjoint_delta(&self) -> NumericalValue { let one = 1.0; let total = self.len() as NumericalValue; @@ -113,7 +256,10 @@ where abs_num(one - (cum_conjoint / total)) } - /// numbers inferable observations + /// Counts the number of inferable items in the collection. + /// + /// Filters all items based on is_inferable() and returns the count. + /// fn number_inferable(&self) -> NumericalValue { self.get_all_items() .into_iter() @@ -121,7 +267,10 @@ where .count() as NumericalValue } - /// numbers inverse-inferable observations + /// Counts the number of inverse inferable items in the collection. + /// + /// Filters all items based on is_inverse_inferable() and returns the count. + /// fn number_inverse_inferable(&self) -> NumericalValue { self.get_all_items() .into_iter() @@ -129,7 +278,17 @@ where .count() as NumericalValue } - /// numbers non-inferable observations + /// Counts the number of non-inferable items in the collection. + /// + /// An item is non-inferable if it is both inferable and inverse inferable. + /// + /// Filters all items based on: + /// + /// - is_inferable() + /// - is_inverse_inferable() + /// + /// And returns the count. + /// fn number_non_inferable(&self) -> NumericalValue { self.get_all_items() .into_iter() @@ -137,17 +296,29 @@ where .count() as NumericalValue } - /// percentage of observations that are inferable + /// Calculates the percentage of inferable items in the collection. + /// + /// Divides the number of inferable items by the total length. + /// Then multiplies by 100 to get a percentage. + /// fn percent_inferable(&self) -> NumericalValue { (self.number_inferable() / self.len() as NumericalValue) * (100 as NumericalValue) } - /// percentage of observations that are inverse inferable + /// Calculates the percentage of inverse inferable items in the collection. + /// + /// Divides the number of inverse inferable items by the total length. + /// Then multiplies by 100 to get a percentage. + /// fn percent_inverse_inferable(&self) -> NumericalValue { (self.number_inverse_inferable() / self.len() as NumericalValue) * (100 as NumericalValue) } - /// percentage of observations that are neither inferable nor inverse inferable + /// Calculates the percentage of non-inferable items in the collection. + /// + /// Divides the number of non-inferable items by the total length. + /// Then multiplies by 100 to get a percentage. + /// fn percent_non_inferable(&self) -> NumericalValue { (self.number_non_inferable() / self.len() as NumericalValue) * (100 as NumericalValue) } diff --git a/deep_causality/src/protocols/observable/mod.rs b/deep_causality/src/protocols/observable/mod.rs index d94b0a29..2f38488b 100644 --- a/deep_causality/src/protocols/observable/mod.rs +++ b/deep_causality/src/protocols/observable/mod.rs @@ -5,10 +5,37 @@ use std::fmt::Debug; use crate::prelude::{Identifiable, NumericalValue}; +/// Observable trait for objects that can be observed. +/// +/// Requires: +/// +/// - Debug - for debug printing +/// - Identifiable - for unique identification +/// +/// Provides methods: +/// +/// - observation() - gets the numerical observation value +/// - observed_effect() - gets the observed effect value +/// - effect_observed() - checks if observation meets threshold and matches effect +/// +/// effect_observed() checks: +/// +/// - observation >= target_threshold +/// - observed_effect == target_effect +/// pub trait Observable: Debug + Identifiable { fn observation(&self) -> NumericalValue; fn observed_effect(&self) -> NumericalValue; + /// Checks if the observed effect meets the target threshold and effect. + /// + /// Returns true if: + /// + /// - observation() >= target_threshold + /// - observed_effect() == target_effect + /// + /// Otherwise returns false. + /// fn effect_observed( &self, target_threshold: NumericalValue, @@ -18,6 +45,23 @@ pub trait Observable: Debug + Identifiable { } } +/// ObservableReasoning trait provides reasoning methods for collections of Observable items. +/// +/// Where T: Observable +/// +/// Provides methods: +/// +/// - len() - number of items +/// - is_empty() - checks if empty +/// - get_all_items() - returns all items +/// +/// - number_observation() - counts items meeting threshold and effect +/// - number_non_observation() - counts items not meeting criteria +/// - percent_observation() - % of items meeting criteria +/// - percent_non_observation() - % of items not meeting criteria +/// +/// Uses T's effect_observed() method to check criteria. +/// pub trait ObservableReasoning where T: Observable, @@ -27,8 +71,18 @@ where fn is_empty(&self) -> bool; fn get_all_items(&self) -> Vec<&T>; + // // Default implementations. + // + /// Counts the number of observations meeting the criteria. + /// + /// Iterates through all items and filters based on: + /// + /// - item.effect_observed(target_threshold, target_effect) + /// + /// Then returns the count. + /// fn number_observation( &self, target_threshold: NumericalValue, @@ -40,6 +94,15 @@ where .count() as NumericalValue } + /// Counts the number of non-observations based on the criteria. + /// + /// Calculates this by: + /// + /// - self.len() - total number of items + /// - minus number_observation() count + /// + /// Returns the number of items not meeting criteria. + /// fn number_non_observation( &self, target_threshold: NumericalValue, @@ -48,6 +111,12 @@ where self.len() as NumericalValue - self.number_observation(target_threshold, target_effect) } + /// Calculates the percentage of observations meeting the criteria. + /// + /// Divides the number_observation count by the total number of items. + /// + /// Returns value between 0.0 and 1.0 as a percentage. + /// fn percent_observation( &self, target_threshold: NumericalValue, @@ -57,6 +126,10 @@ where // * (100 as NumericalValue) } + /// Calculates the percentage of non-observations based on the criteria. + /// + /// Returns 1.0 minus the percent_observation. + /// fn percent_non_observation( &self, target_threshold: NumericalValue,