Skip to content

Commit a8fd18c

Browse files
committed
Finalized Programmatic Verification of Model Assumptions #275
Signed-off-by: Marvin Hansen <[email protected]>
1 parent a4afd15 commit a8fd18c

File tree

10 files changed

+268
-108
lines changed

10 files changed

+268
-108
lines changed

deep_causality/src/errors/assumption_error.rs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,14 @@
99
use std::error::Error;
1010
use std::fmt;
1111

12-
#[derive(Debug, PartialEq, Eq)]
12+
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
1313
pub enum AssumptionError {
1414
/// Error returned when verification is attempted on a model with no assumptions.
1515
NoAssumptionsDefined,
1616
///Error returned when verification is attempted without data i.e. empty collection.
1717
NoDataToTestDefined,
18+
///Error to capture the specific failed assumption
19+
AssumptionFailed(String),
1820
/// Wraps an error that occurred during the execution of an assumption function.
1921
EvaluationFailed(String),
2022
}
@@ -30,6 +32,9 @@ impl fmt::Display for AssumptionError {
3032
AssumptionError::NoDataToTestDefined => {
3133
write!(f, "No Data to test provided")
3234
}
35+
AssumptionError::AssumptionFailed(a) => {
36+
write!(f, "Assumption failed: {a}")
37+
}
3338
AssumptionError::EvaluationFailed(msg) => {
3439
write!(f, "Failed to evaluate assumption: {msg}")
3540
}

deep_causality/src/traits/transferable/mod.rs

Lines changed: 41 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,19 +4,53 @@
44
*/
55

66
//!
7-
//! Trait for model verification.
7+
//! Trait for assumption verification used in the Model type.
88
//!
9-
use crate::{AssumptionError, PropagatingEffect};
9+
use crate::{Assumable, Assumption, AssumptionError, PropagatingEffect};
10+
use std::sync::Arc;
1011

1112
pub trait Transferable {
13+
fn get_assumptions(&self) -> &Option<Arc<Vec<Assumption>>>;
14+
1215
/// Verifies the model's assumptions against a given PropagatingEffect.
1316
///
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+
///
1423
/// # Arguments
15-
/// * `effect` - The output of a model run or sample data to be tested.
24+
/// * `effect` - Sample data to be tested. Details on sampling should be documented in each assumption.
1625
///
1726
/// # Returns
18-
/// * `Ok(true)` if all assumptions hold true.
19-
/// * `Ok(false)` if any assumption fails evaluation.
20-
/// * `Err(AssumptionError)` if the model has no assumptions or an evaluation error occurs.
21-
fn verify_assumptions(&self, effect: &[PropagatingEffect]) -> Result<bool, AssumptionError>;
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+
}
2256
}

deep_causality/src/types/model_types/assumption/debug.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ impl Assumption {
2323
fn fmt_write(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
2424
write!(
2525
f,
26-
"Assumption: id: {}, description: {}, assumption_fn: fn(&[PropagatingEffect]) -> Result<bool, AssumptionError>;, assumption_tested: {},assumption_valid: {}",
26+
"Assumption: id: {}, description: {}, assumption_tested: {}, assumption_valid: {}",
2727
self.id,
2828
self.description,
2929
self.assumption_tested.read().unwrap().clone(),

deep_causality/src/types/model_types/model/transferable.rs

Lines changed: 8 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,10 @@
66
//!
77
//! Implementation of the Transferable trait for the Model struct.
88
//!
9+
910
use crate::types::model_types::model::Model;
10-
use crate::{
11-
Assumable, AssumptionError, Datable, PropagatingEffect, SpaceTemporal, Spatial, Symbolic,
12-
Temporal, Transferable,
13-
};
11+
use crate::{Assumption, Datable, SpaceTemporal, Spatial, Symbolic, Temporal, Transferable};
12+
use std::sync::Arc;
1413

1514
impl<D, S, T, ST, SYM, VS, VT> Transferable for Model<D, S, T, ST, SYM, VS, VT>
1615
where
@@ -22,23 +21,10 @@ where
2221
VS: Clone,
2322
VT: Clone,
2423
{
25-
fn verify_assumptions(&self, effect: &[PropagatingEffect]) -> Result<bool, AssumptionError> {
26-
if effect.is_empty() {
27-
return Err(AssumptionError::NoDataToTestDefined);
28-
}
29-
30-
if self.assumptions.is_none() {
31-
return Err(AssumptionError::NoAssumptionsDefined);
32-
}
33-
34-
let assumptions = self.assumptions.as_ref().unwrap();
35-
for assumption in assumptions.iter() {
36-
match assumption.verify_assumption(effect) {
37-
Ok(true) => continue, // Assumption holds, continue checking
38-
Ok(false) => return Ok(false), // Assumption failed
39-
Err(e) => return Err(e), // An error occurred during evaluation
40-
}
41-
}
42-
Ok(true) // All assumptions passed
24+
fn get_assumptions(&self) -> &Option<Arc<Vec<Assumption>>> {
25+
&self.assumptions
4326
}
27+
28+
// verify_assumptions is derived from the Transferable trait. Overwrite for customization.
29+
// fn verify_assumptions(&self, effect: &[PropagatingEffect]) -> Result<bool, AssumptionError> {}
4430
}

deep_causality/src/utils_test/test_utils.rs

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -194,15 +194,41 @@ pub fn get_test_observation() -> Observation {
194194
pub fn get_test_assumption() -> Assumption {
195195
let id: IdentificationValue = 1;
196196
let description: String = "Test assumption that data are there".to_string() as DescriptionValue;
197-
let assumption_fn: EvalFn = test_has_data;
197+
let assumption_fn: EvalFn = test_fn_has_data;
198198

199199
Assumption::new(id, description, assumption_fn)
200200
}
201201

202-
fn test_has_data(data: &[PropagatingEffect]) -> Result<bool, AssumptionError> {
202+
fn test_fn_has_data(data: &[PropagatingEffect]) -> Result<bool, AssumptionError> {
203203
Ok(!data.is_empty()) // Data is NOT empty i.e. true when it is
204204
}
205205

206+
pub fn get_test_assumption_false() -> Assumption {
207+
let id: IdentificationValue = 2;
208+
let description: String =
209+
"Test assumption that is always false".to_string() as DescriptionValue;
210+
let assumption_fn: EvalFn = test_fn_is_false;
211+
Assumption::new(id, description, assumption_fn)
212+
}
213+
214+
fn test_fn_is_false(_data: &[PropagatingEffect]) -> Result<bool, AssumptionError> {
215+
Ok(false)
216+
}
217+
218+
pub fn get_test_assumption_error() -> Assumption {
219+
let id: IdentificationValue = 2;
220+
let description: String =
221+
"Test assumption that raises an error".to_string() as DescriptionValue;
222+
let assumption_fn: EvalFn = test_fn_is_error;
223+
Assumption::new(id, description, assumption_fn)
224+
}
225+
226+
fn test_fn_is_error(_data: &[PropagatingEffect]) -> Result<bool, AssumptionError> {
227+
Err(AssumptionError::AssumptionFailed(String::from(
228+
"Test error",
229+
)))
230+
}
231+
206232
pub fn get_test_num_array() -> [NumericalValue; 10] {
207233
[8.4, 8.5, 9.1, 9.3, 9.4, 9.5, 9.7, 9.7, 9.9, 9.9]
208234
}

deep_causality/tests/errors/assumption_error_tests.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
*/
55

66
use deep_causality::AssumptionError;
7+
use deep_causality::utils_test::test_utils;
78

89
#[test]
910
fn test_no_assumptions_defined_error() {
@@ -17,6 +18,16 @@ fn test_no_data_error() {
1718
assert_eq!(error.to_string(), "No Data to test provided");
1819
}
1920

21+
#[test]
22+
fn test_assumption_failed_error() {
23+
let assumption = test_utils::get_test_assumption();
24+
let error = AssumptionError::AssumptionFailed(assumption.to_string());
25+
assert_eq!(
26+
error.to_string(),
27+
"Assumption failed: Assumption: id: 1, description: Test assumption that data are there, assumption_tested: false, assumption_valid: false"
28+
);
29+
}
30+
2031
#[test]
2132
fn test_evaluation_failed_error() {
2233
let error = AssumptionError::EvaluationFailed("Test Error".to_string());

deep_causality/tests/types/model_types/assumption/assumption_tests.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,7 @@ fn test_assumption_debug() {
124124

125125
// 1. Test initial state (before verification)
126126
let expected_initial = format!(
127-
"Assumption: id: {}, description: {}, assumption_fn: fn(&[PropagatingEffect]) -> Result<bool, AssumptionError>;, assumption_tested: {},assumption_valid: {}",
127+
"Assumption: id: {}, description: {}, assumption_tested: {}, assumption_valid: {}",
128128
id, description, false, false
129129
);
130130

@@ -142,7 +142,7 @@ fn test_assumption_debug() {
142142

143143
// 3. Test final state (after verification)
144144
let expected_after_verify = format!(
145-
"Assumption: id: {}, description: {}, assumption_fn: fn(&[PropagatingEffect]) -> Result<bool, AssumptionError>;, assumption_tested: {},assumption_valid: {}",
145+
"Assumption: id: {}, description: {}, assumption_tested: {}, assumption_valid: {}",
146146
id, description, true, true
147147
);
148148

@@ -162,7 +162,7 @@ fn test_assumption_to_string() {
162162
assert_eq!(assumption.description(), description);
163163

164164
let expected = format!(
165-
"Assumption: id: {}, description: {}, assumption_fn: fn(&[PropagatingEffect]) -> Result<bool, AssumptionError>;, assumption_tested: {},assumption_valid: {}",
165+
"Assumption: id: {}, description: {}, assumption_tested: {}, assumption_valid: {}",
166166
id, description, false, false
167167
);
168168
let actual = assumption.to_string();

deep_causality/tests/types/model_types/model/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,6 @@
22
* SPDX-License-Identifier: MIT
33
* Copyright (c) "2025" . The DeepCausality Authors and Contributors. All Rights Reserved.
44
*/
5+
mod model_assumptions_tests;
56
#[cfg(test)]
67
mod model_tests;

0 commit comments

Comments
 (0)