Skip to content

Commit b485102

Browse files
Merge pull request #277 from marvin-hansen/main
feat(deep_causality): Added Programmatic Verification of Model Assumptions
2 parents abea5c3 + a8fd18c commit b485102

39 files changed

+998
-442
lines changed

deep_causality/benches/benchmarks/bench_collection.rs

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,10 @@ fn small_causality_collection_benchmark(criterion: &mut Criterion) {
1919
let evidence = PropagatingEffect::Numerical(0.99);
2020

2121
criterion.bench_function("small_causality_collection_propagation", |bencher| {
22-
bencher.iter(|| coll.evaluate_deterministic_propagation(&evidence).unwrap())
22+
bencher.iter(|| {
23+
coll.evaluate_deterministic_propagation(&evidence, &AggregateLogic::All)
24+
.unwrap()
25+
})
2326
});
2427
}
2528

@@ -28,7 +31,10 @@ fn medium_causality_collection_benchmark(criterion: &mut Criterion) {
2831
let evidence = PropagatingEffect::Numerical(0.99);
2932

3033
criterion.bench_function("medium_causality_collection_propagation", |bencher| {
31-
bencher.iter(|| coll.evaluate_deterministic_propagation(&evidence).unwrap())
34+
bencher.iter(|| {
35+
coll.evaluate_deterministic_propagation(&evidence, &AggregateLogic::All)
36+
.unwrap()
37+
})
3238
});
3339
}
3440

@@ -37,7 +43,10 @@ fn large_causality_collection_benchmark(criterion: &mut Criterion) {
3743
let evidence = PropagatingEffect::Numerical(0.99);
3844

3945
criterion.bench_function("large_causality_collection_propagation", |bencher| {
40-
bencher.iter(|| coll.evaluate_deterministic_propagation(&evidence).unwrap())
46+
bencher.iter(|| {
47+
coll.evaluate_deterministic_propagation(&evidence, &AggregateLogic::All)
48+
.unwrap()
49+
})
4150
});
4251
}
4352

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
/*
2+
* SPDX-License-Identifier: MIT
3+
* Copyright (c) "2025" . The DeepCausality Authors and Contributors. All Rights Reserved.
4+
*/
5+
6+
//!
7+
//! Error type for assumption checking.
8+
//!
9+
use std::error::Error;
10+
use std::fmt;
11+
12+
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
13+
pub enum AssumptionError {
14+
/// Error returned when verification is attempted on a model with no assumptions.
15+
NoAssumptionsDefined,
16+
///Error returned when verification is attempted without data i.e. empty collection.
17+
NoDataToTestDefined,
18+
///Error to capture the specific failed assumption
19+
AssumptionFailed(String),
20+
/// Wraps an error that occurred during the execution of an assumption function.
21+
EvaluationFailed(String),
22+
}
23+
24+
impl Error for AssumptionError {}
25+
26+
impl fmt::Display for AssumptionError {
27+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
28+
match self {
29+
AssumptionError::NoAssumptionsDefined => {
30+
write!(f, "Model has no assumptions to verify")
31+
}
32+
AssumptionError::NoDataToTestDefined => {
33+
write!(f, "No Data to test provided")
34+
}
35+
AssumptionError::AssumptionFailed(a) => {
36+
write!(f, "Assumption failed: {a}")
37+
}
38+
AssumptionError::EvaluationFailed(msg) => {
39+
write!(f, "Failed to evaluate assumption: {msg}")
40+
}
41+
}
42+
}
43+
}

deep_causality/src/errors/mod.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
mod action_error;
77
mod adjustment_error;
8+
mod assumption_error;
89
mod build_error;
910
mod causal_graph_index_error;
1011
mod causality_error;
@@ -13,11 +14,12 @@ mod context_index_error;
1314
mod index_error;
1415
mod model_build_error;
1516
mod model_generation_error;
16-
mod model_validation_error;
17+
pub mod model_validation_error;
1718
mod update_error;
1819

1920
pub use action_error::*;
2021
pub use adjustment_error::*;
22+
pub use assumption_error::*;
2123
pub use build_error::*;
2224
pub use causal_graph_index_error::*;
2325
pub use causality_error::*;

deep_causality/src/extensions/causable/mod.rs

Lines changed: 19 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -8,21 +8,24 @@ use std::hash::Hash;
88

99
// Extension trait http://xion.io/post/code/rust-extension-traits.html
1010
use deep_causality_macros::{
11-
make_array_to_vec, make_get_all_items, make_get_all_map_items, make_is_empty, make_len,
12-
make_map_to_vec, make_vec_to_vec,
11+
make_array_to_vec, make_find_from_iter_values, make_find_from_map_values, make_get_all_items,
12+
make_get_all_map_items, make_is_empty, make_len, make_map_to_vec, make_vec_deq_to_vec,
13+
make_vec_to_vec,
1314
};
1415

15-
use crate::Causable;
1616
use crate::traits::causable::causable_reasoning::CausableReasoning;
17+
use crate::{Causable, IdentificationValue};
1718

18-
impl<T> CausableReasoning<T> for [T]
19+
impl<K, V> CausableReasoning<V> for HashMap<K, V>
1920
where
20-
T: Causable + Clone,
21+
K: Eq + Hash,
22+
V: Causable + Clone,
2123
{
2224
make_len!();
2325
make_is_empty!();
24-
make_get_all_items!();
25-
make_array_to_vec!();
26+
make_map_to_vec!();
27+
make_get_all_map_items!();
28+
make_find_from_map_values!();
2629
}
2730

2831
impl<K, V> CausableReasoning<V> for BTreeMap<K, V>
@@ -34,17 +37,18 @@ where
3437
make_is_empty!();
3538
make_map_to_vec!();
3639
make_get_all_map_items!();
40+
make_find_from_map_values!();
3741
}
3842

39-
impl<K, V> CausableReasoning<V> for HashMap<K, V>
43+
impl<T> CausableReasoning<T> for [T]
4044
where
41-
K: Eq + Hash,
42-
V: Causable + Clone,
45+
T: Causable + Clone,
4346
{
4447
make_len!();
4548
make_is_empty!();
46-
make_map_to_vec!();
47-
make_get_all_map_items!();
49+
make_get_all_items!();
50+
make_array_to_vec!();
51+
make_find_from_iter_values!();
4852
}
4953

5054
impl<T> CausableReasoning<T> for Vec<T>
@@ -55,6 +59,7 @@ where
5559
make_is_empty!();
5660
make_vec_to_vec!();
5761
make_get_all_items!();
62+
make_find_from_iter_values!();
5863
}
5964

6065
impl<T> CausableReasoning<T> for VecDeque<T>
@@ -64,18 +69,6 @@ where
6469
make_len!();
6570
make_is_empty!();
6671
make_get_all_items!();
67-
// VecDeque can't be turned into a vector hence the custom implementation
68-
// https://github.com/rust-lang/rust/issues/23308
69-
// Also, make_contiguous requires self to be mutable, which would violate the API, hence the clone.
70-
// https://doc.rust-lang.org/std/collections/struct.VecDeque.html#method.make_contiguous
71-
fn to_vec(&self) -> Vec<T> {
72-
let mut v = Vec::with_capacity(self.len());
73-
let mut deque = self.clone(); // clone to avoid mutating the original
74-
75-
for item in deque.make_contiguous().iter() {
76-
v.push(item.clone());
77-
}
78-
79-
v
80-
}
72+
make_vec_deq_to_vec!();
73+
make_find_from_iter_values!();
8174
}

deep_causality/src/lib.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,8 @@ pub use crate::traits::observable::ObservableReasoning;
7474
// Scalar Traits
7575
pub use crate::traits::scalar::scalar_projector::ScalarProjector;
7676
pub use crate::traits::scalar::scalar_value::ScalarValue;
77+
// Transferable Trait
78+
pub use crate::traits::transferable::Transferable;
7779
//
7880
// Types
7981
//
@@ -139,6 +141,7 @@ pub use crate::types::model_types::inference::Inference;
139141
pub use crate::types::model_types::model::Model;
140142
pub use crate::types::model_types::observation::Observation;
141143
// Reasoning types
144+
pub use crate::types::reasoning_types::aggregate_logic::AggregateLogic;
142145
pub use crate::types::reasoning_types::propagating_effect::PropagatingEffect;
143146
//
144147
//Symbolic types

deep_causality/src/traits/assumable/mod.rs

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
* Copyright (c) "2025" . The DeepCausality Authors and Contributors. All Rights Reserved.
44
*/
55

6-
use crate::{DescriptionValue, EvalFn, Identifiable, NumericalValue};
6+
use crate::{AssumptionError, DescriptionValue, Identifiable, NumericalValue, PropagatingEffect};
77

88
/// The Assumable trait defines the interface for objects that represent
99
/// assumptions that can be tested and verified. Assumable types must also
@@ -25,10 +25,9 @@ use crate::{DescriptionValue, EvalFn, Identifiable, NumericalValue};
2525
///
2626
pub trait Assumable: Identifiable {
2727
fn description(&self) -> DescriptionValue;
28-
fn assumption_fn(&self) -> EvalFn;
2928
fn assumption_tested(&self) -> bool;
3029
fn assumption_valid(&self) -> bool;
31-
fn verify_assumption(&self, data: &[NumericalValue]) -> bool;
30+
fn verify_assumption(&self, data: &[PropagatingEffect]) -> Result<bool, AssumptionError>;
3231
}
3332

3433
/// The AssumableReasoning trait provides default implementations for common
@@ -119,10 +118,19 @@ where
119118
/// (from `number_assumption_valid()`) by the total number of assumptions
120119
/// (from `len()`) and multiplying by 100.
121120
///
122-
/// Returns the percentage as a NumericalValue.
121+
/// # Errors
123122
///
124-
fn percent_assumption_valid(&self) -> NumericalValue {
125-
(self.number_assumption_valid() / self.len() as NumericalValue) * 100.0
123+
/// Returns `AssumptionError::EvaluationFailed` if the number of assumptions is zero,
124+
/// as percentage calculation would lead to a division by zero.
125+
///
126+
fn percent_assumption_valid(&self) -> Result<NumericalValue, AssumptionError> {
127+
if self.is_empty() {
128+
return Err(AssumptionError::EvaluationFailed(
129+
"Cannot calculate percentage with zero assumptions".to_string(),
130+
));
131+
}
132+
let percentage = (self.number_assumption_valid() / self.len() as NumericalValue) * 100.0;
133+
Ok(percentage)
126134
}
127135

128136
/// Verifies all assumptions in the collection against the provided data.
@@ -133,10 +141,17 @@ where
133141
/// This will test each assumption against the data and update the
134142
/// `assumption_valid` and `assumption_tested` flags accordingly.
135143
///
136-
fn verify_all_assumptions(&self, data: &[NumericalValue]) {
144+
/// # Errors
145+
///
146+
/// Returns an `AssumptionError` if any of the assumption functions fail during execution.
147+
///
148+
fn verify_all_assumptions(&self, data: &[PropagatingEffect]) -> Result<(), AssumptionError> {
137149
for a in self.get_all_items() {
138-
a.verify_assumption(data);
150+
// We are interested in the side effect of updating the assumption state,
151+
// but we must handle the potential error.
152+
let _ = a.verify_assumption(data)?;
139153
}
154+
Ok(())
140155
}
141156

142157
/// Returns a vector containing references to all invalid assumptions.

deep_causality/src/traits/causable/causable_reasoning.rs

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,10 @@
33
* Copyright (c) "2025" . The DeepCausality Authors and Contributors. All Rights Reserved.
44
*/
55

6-
use crate::{Causable, CausalityError, NumericalValue, PropagatingEffect};
6+
use crate::{
7+
AggregateLogic, Causable, CausalityError, IdentificationValue, NumericalValue,
8+
PropagatingEffect,
9+
};
710

811
/// Provides default implementations for reasoning over collections of `Causable` items.
912
///
@@ -16,6 +19,7 @@ where
1619
{
1720
//
1821
// These methods must be implemented by the collection type.
22+
// See deep_causality/src/extensions/causable/mod.rs
1923
//
2024

2125
/// Returns the total number of `Causable` items in the collection.
@@ -31,6 +35,9 @@ where
3135
/// This is the primary accessor used by the trait's default methods.
3236
fn get_all_items(&self) -> Vec<&T>;
3337

38+
/// Returns a reference to a `Causable` item by its ID, if found.
39+
fn get_item_by_id(&self, id: IdentificationValue) -> Option<&T>;
40+
3441
//
3542
// Default implementations for all other methods are provided below.
3643
//
@@ -49,6 +56,7 @@ where
4956
fn evaluate_deterministic_propagation(
5057
&self,
5158
effect: &PropagatingEffect,
59+
_logic: &AggregateLogic,
5260
) -> Result<PropagatingEffect, CausalityError> {
5361
for cause in self.get_all_items() {
5462
let effect = cause.evaluate(effect)?;
@@ -90,6 +98,7 @@ where
9098
fn evaluate_probabilistic_propagation(
9199
&self,
92100
effect: &PropagatingEffect,
101+
_logic: &AggregateLogic,
93102
) -> Result<PropagatingEffect, CausalityError> {
94103
let mut cumulative_prob: NumericalValue = 1.0;
95104

@@ -140,6 +149,7 @@ where
140149
fn evaluate_mixed_propagation(
141150
&self,
142151
effect: &PropagatingEffect,
152+
_logic: &AggregateLogic,
143153
) -> Result<PropagatingEffect, CausalityError> {
144154
// The chain starts as deterministically true. It can transition to probabilistic.
145155
let mut aggregated_effect = PropagatingEffect::Deterministic(true);

deep_causality/src/traits/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,4 @@ pub mod indexable;
1515
pub mod inferable;
1616
pub mod observable;
1717
pub mod scalar;
18+
pub mod transferable;
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
/*
2+
* SPDX-License-Identifier: MIT
3+
* Copyright (c) "2025" . The DeepCausality Authors and Contributors. All Rights Reserved.
4+
*/
5+
6+
//!
7+
//! Trait for assumption verification used in the Model type.
8+
//!
9+
use crate::{Assumable, Assumption, AssumptionError, PropagatingEffect};
10+
use std::sync::Arc;
11+
12+
pub trait Transferable {
13+
fn get_assumptions(&self) -> &Option<Arc<Vec<Assumption>>>;
14+
15+
/// Verifies the model's assumptions against a given PropagatingEffect.
16+
///
17+
/// The function iterates through all defined assumptions and checks them against
18+
/// the provided data. It short-circuits and returns immediately on the first
19+
/// failure or error.
20+
///
21+
/// Overwrite the default implementation if you need customization.
22+
///
23+
/// # Arguments
24+
/// * `effect` - Sample data to be tested. Details on sampling should be documented in each assumption.
25+
///
26+
/// # Returns
27+
/// * `Ok(())` if all assumptions hold true.
28+
/// * `Err(AssumptionError::AssumptionFailed(String))` if an assumption is not met.
29+
/// * `Err(AssumptionError::NoAssumptionsDefined)` if the model has no assumptions.
30+
/// * `Err(AssumptionError::NoDataToTestDefined)` if the effect slice is empty.
31+
/// * `Err(AssumptionError::EvaluationError(...))` if an error occurs during evaluation.
32+
///
33+
fn verify_assumptions(&self, effect: &[PropagatingEffect]) -> Result<(), AssumptionError> {
34+
if effect.is_empty() {
35+
return Err(AssumptionError::NoDataToTestDefined);
36+
}
37+
38+
if self.get_assumptions().is_none() {
39+
return Err(AssumptionError::NoAssumptionsDefined);
40+
}
41+
42+
let assumptions = self.get_assumptions().as_ref().unwrap();
43+
44+
for assumption in assumptions.iter() {
45+
// The `?` operator propagates any evaluation errors.
46+
if !assumption.verify_assumption(effect)? {
47+
// If an assumption returns `Ok(false)`, the check has failed.
48+
// We now return an error containing the specific assumption that failed.
49+
return Err(AssumptionError::AssumptionFailed(assumption.to_string()));
50+
}
51+
}
52+
53+
// If the loop completes, all assumptions passed.
54+
Ok(())
55+
}
56+
}

deep_causality/src/types/alias_types/alias_function.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,13 @@
22
* SPDX-License-Identifier: MIT
33
* Copyright (c) "2025" . The DeepCausality Authors and Contributors. All Rights Reserved.
44
*/
5-
use crate::{CausalityError, Context, NumericalValue, PropagatingEffect};
5+
use crate::{AssumptionError, CausalityError, Context, PropagatingEffect};
66
use std::sync::Arc;
77

88
// Fn aliases for assumable, assumption, & assumption collection
99
/// Function type for evaluating numerical values and returning a boolean result.
1010
/// This remains unchanged as it serves a different purpose outside the core causal reasoning.
11-
pub type EvalFn = fn(&[NumericalValue]) -> bool;
11+
pub type EvalFn = fn(&[PropagatingEffect]) -> Result<bool, AssumptionError>;
1212

1313
/// The unified function signature for all singleton causaloids that do not require an external context.
1414
///

0 commit comments

Comments
 (0)