use crate::{Rule, RuleAtom, Term};
use anyhow::{anyhow, Result};
use std::collections::HashMap;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum UncertaintyModel {
Probabilistic,
Fuzzy,
DempsterShafer,
Possibilistic,
Hybrid,
}
#[derive(Debug, Clone)]
pub struct UncertaintyValue {
pub model: UncertaintyModel,
pub value: f64,
pub secondary: Option<f64>,
pub metadata: HashMap<String, String>,
}
impl UncertaintyValue {
pub fn new(model: UncertaintyModel, value: f64) -> Self {
Self {
model,
value: value.clamp(0.0, 1.0),
secondary: None,
metadata: HashMap::new(),
}
}
pub fn with_secondary(model: UncertaintyModel, value: f64, secondary: f64) -> Self {
Self {
model,
value: value.clamp(0.0, 1.0),
secondary: Some(secondary.clamp(0.0, 1.0)),
metadata: HashMap::new(),
}
}
pub fn convert_to(&self, target_model: UncertaintyModel) -> Self {
if self.model == target_model {
return self.clone();
}
match (self.model, target_model) {
(UncertaintyModel::Probabilistic, UncertaintyModel::Fuzzy) => {
UncertaintyValue::new(target_model, self.value)
}
(UncertaintyModel::Fuzzy, UncertaintyModel::Probabilistic) => {
UncertaintyValue::new(target_model, self.value)
}
(UncertaintyModel::Probabilistic, UncertaintyModel::DempsterShafer) => {
UncertaintyValue::with_secondary(target_model, self.value, 1.0)
}
(UncertaintyModel::DempsterShafer, UncertaintyModel::Probabilistic) => {
UncertaintyValue::new(target_model, self.value)
}
(UncertaintyModel::Probabilistic, UncertaintyModel::Possibilistic) => {
UncertaintyValue::with_secondary(target_model, self.value, 0.0)
}
(UncertaintyModel::Possibilistic, UncertaintyModel::Probabilistic) => {
UncertaintyValue::new(target_model, self.value)
}
(UncertaintyModel::Fuzzy, UncertaintyModel::Possibilistic) => {
UncertaintyValue::with_secondary(target_model, self.value, 1.0 - self.value)
}
_ => UncertaintyValue::new(target_model, self.value),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CombinationOperator {
Product,
Minimum,
Maximum,
WeightedSum,
DempsterRule,
PossibilisticConjunction,
PossibilisticDisjunction,
}
#[derive(Debug)]
pub struct UncertaintyPropagator {
model: UncertaintyModel,
facts: HashMap<String, UncertaintyValue>,
provenance: HashMap<String, Vec<String>>,
operator: CombinationOperator,
threshold: f64,
random_state: u64,
}
impl UncertaintyPropagator {
pub fn new(model: UncertaintyModel) -> Self {
let operator = match model {
UncertaintyModel::Probabilistic => CombinationOperator::Product,
UncertaintyModel::Fuzzy => CombinationOperator::Minimum,
UncertaintyModel::DempsterShafer => CombinationOperator::DempsterRule,
UncertaintyModel::Possibilistic => CombinationOperator::PossibilisticConjunction,
UncertaintyModel::Hybrid => CombinationOperator::WeightedSum,
};
Self {
model,
facts: HashMap::new(),
provenance: HashMap::new(),
operator,
threshold: 0.0,
random_state: 42,
}
}
pub fn set_operator(&mut self, operator: CombinationOperator) {
self.operator = operator;
}
pub fn set_threshold(&mut self, threshold: f64) {
self.threshold = threshold.clamp(0.0, 1.0);
}
pub fn add_fact(&mut self, fact: RuleAtom, uncertainty: f64) {
let key = self.fact_to_key(&fact);
let value = UncertaintyValue::new(self.model, uncertainty);
self.facts.insert(key, value);
}
pub fn add_fact_with_value(&mut self, fact: RuleAtom, value: UncertaintyValue) {
let key = self.fact_to_key(&fact);
self.facts.insert(key, value);
}
pub fn get_uncertainty(&self, fact: &RuleAtom) -> Option<&UncertaintyValue> {
let key = self.fact_to_key(fact);
self.facts.get(&key)
}
pub fn propagate_through_rule(
&mut self,
rule: &Rule,
rule_confidence: f64,
) -> Result<Vec<(RuleAtom, UncertaintyValue)>> {
let mut body_uncertainties = Vec::new();
for atom in &rule.body {
if let Some(uncertainty) = self.get_uncertainty(atom) {
body_uncertainties.push(uncertainty.clone());
} else {
return Ok(Vec::new());
}
}
let combined_body = self.combine_uncertainties(&body_uncertainties)?;
let rule_uncertainty = UncertaintyValue::new(self.model, rule_confidence);
let final_uncertainty = self.combine_two(&combined_body, &rule_uncertainty)?;
let mut results = Vec::new();
for atom in &rule.head {
let key = self.fact_to_key(atom);
self.provenance
.entry(key.clone())
.or_default()
.push(rule.name.clone());
if let Some(existing) = self.facts.get(&key) {
if existing.value >= final_uncertainty.value {
continue;
}
}
self.facts.insert(key, final_uncertainty.clone());
results.push((atom.clone(), final_uncertainty.clone()));
}
Ok(results)
}
pub fn combine_uncertainties(&self, values: &[UncertaintyValue]) -> Result<UncertaintyValue> {
if values.is_empty() {
return Err(anyhow!("Cannot combine empty uncertainty values"));
}
if values.len() == 1 {
return Ok(values[0].clone());
}
let mut result = values[0].clone();
for value in &values[1..] {
result = self.combine_two(&result, value)?;
}
Ok(result)
}
pub fn combine_two(
&self,
a: &UncertaintyValue,
b: &UncertaintyValue,
) -> Result<UncertaintyValue> {
let b_converted = if a.model != b.model {
b.convert_to(a.model)
} else {
b.clone()
};
let combined_value = match self.operator {
CombinationOperator::Product => a.value * b_converted.value,
CombinationOperator::Minimum => a.value.min(b_converted.value),
CombinationOperator::Maximum => a.value.max(b_converted.value),
CombinationOperator::WeightedSum => (a.value + b_converted.value) / 2.0,
CombinationOperator::DempsterRule => self.dempster_combination(a, &b_converted)?,
CombinationOperator::PossibilisticConjunction => {
let na = a.secondary.unwrap_or(1.0 - a.value);
let nb = b_converted.secondary.unwrap_or(1.0 - b_converted.value);
1.0 - na.min(nb)
}
CombinationOperator::PossibilisticDisjunction => {
a.value.max(b_converted.value)
}
};
Ok(UncertaintyValue::new(
a.model,
combined_value.clamp(0.0, 1.0),
))
}
fn dempster_combination(&self, a: &UncertaintyValue, b: &UncertaintyValue) -> Result<f64> {
let m1 = a.value; let m2 = b.value;
let conflict = (1.0 - m1) * m2 + m1 * (1.0 - m2);
if conflict >= 1.0 {
return Err(anyhow!("Total conflict in Dempster combination"));
}
let combined = (m1 * m2) / (1.0 - conflict);
Ok(combined.clamp(0.0, 1.0))
}
pub fn filter_by_threshold(&self) -> Vec<(RuleAtom, UncertaintyValue)> {
let mut results = Vec::new();
for (key, value) in &self.facts {
if value.value >= self.threshold {
if let Some(atom) = self.key_to_fact(key) {
results.push((atom, value.clone()));
}
}
}
results
}
pub fn monte_carlo_propagate(
&mut self,
rule: &Rule,
rule_confidence: f64,
samples: usize,
) -> Result<Vec<(RuleAtom, UncertaintyValue)>> {
let mut success_counts: HashMap<String, usize> = HashMap::new();
for _ in 0..samples {
let mut all_satisfied = true;
for atom in &rule.body {
let uncertainty_val = if let Some(uncertainty) = self.get_uncertainty(atom) {
uncertainty.value
} else {
all_satisfied = false;
break;
};
self.random_state = self
.random_state
.wrapping_mul(1103515245)
.wrapping_add(12345);
let rand_val = ((self.random_state >> 16) & 0xFFFF) as f64 / 65536.0;
if rand_val > uncertainty_val {
all_satisfied = false;
break;
}
}
if all_satisfied {
self.random_state = self
.random_state
.wrapping_mul(1103515245)
.wrapping_add(12345);
let rand_val = ((self.random_state >> 16) & 0xFFFF) as f64 / 65536.0;
if rand_val <= rule_confidence {
for atom in &rule.head {
let key = self.fact_to_key(atom);
*success_counts.entry(key).or_insert(0) += 1;
}
}
}
}
let mut results = Vec::new();
for atom in &rule.head {
let key = self.fact_to_key(atom);
let count = success_counts.get(&key).copied().unwrap_or(0);
let probability = count as f64 / samples as f64;
let uncertainty = UncertaintyValue::new(self.model, probability);
self.facts.insert(key, uncertainty.clone());
results.push((atom.clone(), uncertainty));
}
Ok(results)
}
pub fn batch_propagate(
&mut self,
rules: &[Rule],
rule_confidences: &[f64],
) -> Result<Vec<(RuleAtom, UncertaintyValue)>> {
if rules.len() != rule_confidences.len() {
return Err(anyhow!("Rules and confidences length mismatch"));
}
let mut all_results = Vec::new();
let batch_size = 100;
for chunk_start in (0..rules.len()).step_by(batch_size) {
let chunk_end = (chunk_start + batch_size).min(rules.len());
for i in chunk_start..chunk_end {
let results = self.propagate_through_rule(&rules[i], rule_confidences[i])?;
all_results.extend(results);
}
}
Ok(all_results)
}
pub fn get_provenance(&self, fact: &RuleAtom) -> Option<&Vec<String>> {
let key = self.fact_to_key(fact);
self.provenance.get(&key)
}
pub fn apply_decay(&mut self, decay_factor: f64) {
let decay = decay_factor.clamp(0.0, 1.0);
for (key, value) in &mut self.facts {
if let Some(chain) = self.provenance.get(key) {
let depth = chain.len() as f64;
let decayed = value.value * decay.powf(depth);
value.value = decayed.clamp(0.0, 1.0);
}
}
}
fn fact_to_key(&self, fact: &RuleAtom) -> String {
match fact {
RuleAtom::Triple {
subject,
predicate,
object,
} => {
format!("{:?}|{:?}|{:?}", subject, predicate, object)
}
RuleAtom::Builtin { name, args } => {
format!("builtin:{}({:?})", name, args)
}
RuleAtom::NotEqual { left, right } => {
format!("neq:{:?}!={:?}", left, right)
}
RuleAtom::GreaterThan { left, right } => {
format!("gt:{:?}>{:?}", left, right)
}
RuleAtom::LessThan { left, right } => {
format!("lt:{:?}<{:?}", left, right)
}
}
}
fn key_to_fact(&self, key: &str) -> Option<RuleAtom> {
if key.starts_with("builtin:")
|| key.starts_with("neq:")
|| key.starts_with("gt:")
|| key.starts_with("lt:")
{
None } else {
let parts: Vec<&str> = key.split('|').collect();
if parts.len() == 3 {
Some(RuleAtom::Triple {
subject: Term::Constant(parts[0].to_string()),
predicate: Term::Constant(parts[1].to_string()),
object: Term::Constant(parts[2].to_string()),
})
} else {
None
}
}
}
}
impl Default for UncertaintyPropagator {
fn default() -> Self {
Self::new(UncertaintyModel::Probabilistic)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn create_test_fact(s: &str, p: &str, o: &str) -> RuleAtom {
RuleAtom::Triple {
subject: Term::Constant(s.to_string()),
predicate: Term::Constant(p.to_string()),
object: Term::Constant(o.to_string()),
}
}
#[test]
fn test_uncertainty_value_creation() {
let uv = UncertaintyValue::new(UncertaintyModel::Probabilistic, 0.8);
assert_eq!(uv.model, UncertaintyModel::Probabilistic);
assert!((uv.value - 0.8).abs() < 1e-6);
assert!(uv.secondary.is_none());
}
#[test]
fn test_uncertainty_value_clamping() {
let uv = UncertaintyValue::new(UncertaintyModel::Probabilistic, 1.5);
assert!((uv.value - 1.0).abs() < 1e-6);
let uv2 = UncertaintyValue::new(UncertaintyModel::Probabilistic, -0.5);
assert!((uv2.value - 0.0).abs() < 1e-6);
}
#[test]
fn test_uncertainty_conversion() {
let prob = UncertaintyValue::new(UncertaintyModel::Probabilistic, 0.7);
let fuzzy = prob.convert_to(UncertaintyModel::Fuzzy);
assert_eq!(fuzzy.model, UncertaintyModel::Fuzzy);
assert!((fuzzy.value - 0.7).abs() < 1e-6);
}
#[test]
fn test_propagator_creation() {
let prop = UncertaintyPropagator::new(UncertaintyModel::Probabilistic);
assert_eq!(prop.operator, CombinationOperator::Product);
assert!((prop.threshold - 0.0).abs() < 1e-6);
}
#[test]
fn test_add_and_get_fact() -> Result<(), Box<dyn std::error::Error>> {
let mut prop = UncertaintyPropagator::new(UncertaintyModel::Probabilistic);
let fact = create_test_fact("john", "likes", "coffee");
prop.add_fact(fact.clone(), 0.9);
let uncertainty = prop.get_uncertainty(&fact);
assert!(uncertainty.is_some());
assert!((uncertainty.ok_or("expected Some value")?.value - 0.9).abs() < 1e-6);
Ok(())
}
#[test]
fn test_product_combination() -> Result<(), Box<dyn std::error::Error>> {
let mut prop = UncertaintyPropagator::new(UncertaintyModel::Probabilistic);
prop.set_operator(CombinationOperator::Product);
let uv1 = UncertaintyValue::new(UncertaintyModel::Probabilistic, 0.8);
let uv2 = UncertaintyValue::new(UncertaintyModel::Probabilistic, 0.9);
let result = prop.combine_two(&uv1, &uv2)?;
assert!((result.value - 0.72).abs() < 1e-6);
Ok(())
}
#[test]
fn test_minimum_combination() -> Result<(), Box<dyn std::error::Error>> {
let mut prop = UncertaintyPropagator::new(UncertaintyModel::Fuzzy);
prop.set_operator(CombinationOperator::Minimum);
let uv1 = UncertaintyValue::new(UncertaintyModel::Fuzzy, 0.8);
let uv2 = UncertaintyValue::new(UncertaintyModel::Fuzzy, 0.6);
let result = prop.combine_two(&uv1, &uv2)?;
assert!((result.value - 0.6).abs() < 1e-6);
Ok(())
}
#[test]
fn test_maximum_combination() -> Result<(), Box<dyn std::error::Error>> {
let mut prop = UncertaintyPropagator::new(UncertaintyModel::Fuzzy);
prop.set_operator(CombinationOperator::Maximum);
let uv1 = UncertaintyValue::new(UncertaintyModel::Fuzzy, 0.8);
let uv2 = UncertaintyValue::new(UncertaintyModel::Fuzzy, 0.6);
let result = prop.combine_two(&uv1, &uv2)?;
assert!((result.value - 0.8).abs() < 1e-6);
Ok(())
}
#[test]
fn test_propagate_through_rule() -> Result<(), Box<dyn std::error::Error>> {
let mut prop = UncertaintyPropagator::new(UncertaintyModel::Probabilistic);
let fact1 = create_test_fact("john", "parent", "mary");
prop.add_fact(fact1.clone(), 0.9);
let rule = Rule {
name: "ancestor".to_string(),
body: vec![fact1],
head: vec![create_test_fact("john", "ancestor", "mary")],
};
let results = prop.propagate_through_rule(&rule, 0.8)?;
assert_eq!(results.len(), 1);
assert!((results[0].1.value - 0.72).abs() < 1e-6); Ok(())
}
#[test]
fn test_propagate_with_multiple_body_atoms() -> Result<(), Box<dyn std::error::Error>> {
let mut prop = UncertaintyPropagator::new(UncertaintyModel::Probabilistic);
let fact1 = create_test_fact("john", "parent", "mary");
let fact2 = create_test_fact("mary", "parent", "sue");
prop.add_fact(fact1.clone(), 0.9);
prop.add_fact(fact2.clone(), 0.8);
let rule = Rule {
name: "grandparent".to_string(),
body: vec![fact1, fact2],
head: vec![create_test_fact("john", "grandparent", "sue")],
};
let results = prop.propagate_through_rule(&rule, 1.0)?;
assert_eq!(results.len(), 1);
assert!((results[0].1.value - 0.72).abs() < 1e-6); Ok(())
}
#[test]
fn test_filter_by_threshold() {
let mut prop = UncertaintyPropagator::new(UncertaintyModel::Probabilistic);
prop.add_fact(create_test_fact("a", "p", "b"), 0.9);
prop.add_fact(create_test_fact("c", "p", "d"), 0.5);
prop.add_fact(create_test_fact("e", "p", "f"), 0.2);
prop.set_threshold(0.6);
let filtered = prop.filter_by_threshold();
assert_eq!(filtered.len(), 1);
}
#[test]
fn test_monte_carlo_propagation() -> Result<(), Box<dyn std::error::Error>> {
let mut prop = UncertaintyPropagator::new(UncertaintyModel::Probabilistic);
let fact1 = create_test_fact("john", "smart", "true");
prop.add_fact(fact1.clone(), 0.9);
let rule = Rule {
name: "genius".to_string(),
body: vec![fact1],
head: vec![create_test_fact("john", "genius", "true")],
};
let results = prop.monte_carlo_propagate(&rule, 0.8, 1000)?;
assert_eq!(results.len(), 1);
let expected = 0.72;
let actual = results[0].1.value;
assert!((actual - expected).abs() < 0.1); Ok(())
}
#[test]
fn test_provenance_tracking() -> Result<(), Box<dyn std::error::Error>> {
let mut prop = UncertaintyPropagator::new(UncertaintyModel::Probabilistic);
let fact1 = create_test_fact("john", "parent", "mary");
prop.add_fact(fact1.clone(), 0.9);
let rule = Rule {
name: "ancestor_rule".to_string(),
body: vec![fact1],
head: vec![create_test_fact("john", "ancestor", "mary")],
};
prop.propagate_through_rule(&rule, 0.8)?;
let head_fact = create_test_fact("john", "ancestor", "mary");
let provenance = prop.get_provenance(&head_fact);
assert!(provenance.is_some());
assert_eq!(provenance.ok_or("expected Some value")?.len(), 1);
assert_eq!(provenance.ok_or("expected Some value")?[0], "ancestor_rule");
Ok(())
}
#[test]
fn test_uncertainty_decay() -> Result<(), Box<dyn std::error::Error>> {
let mut prop = UncertaintyPropagator::new(UncertaintyModel::Probabilistic);
let fact1 = create_test_fact("john", "ancestor", "mary");
prop.add_fact(fact1.clone(), 0.9);
let key = prop.fact_to_key(&fact1);
prop.provenance
.insert(key.clone(), vec!["rule1".to_string(), "rule2".to_string()]);
prop.apply_decay(0.95);
let uncertainty = prop.get_uncertainty(&fact1).ok_or("expected Some value")?;
let expected = 0.9 * 0.95_f64.powi(2); assert!((uncertainty.value - expected).abs() < 1e-6);
Ok(())
}
#[test]
fn test_batch_propagation() -> Result<(), Box<dyn std::error::Error>> {
let mut prop = UncertaintyPropagator::new(UncertaintyModel::Probabilistic);
let fact1 = create_test_fact("john", "parent", "mary");
let fact2 = create_test_fact("alice", "parent", "bob");
prop.add_fact(fact1.clone(), 0.9);
prop.add_fact(fact2.clone(), 0.8);
let rules = vec![
Rule {
name: "rule1".to_string(),
body: vec![fact1],
head: vec![create_test_fact("john", "ancestor", "mary")],
},
Rule {
name: "rule2".to_string(),
body: vec![fact2],
head: vec![create_test_fact("alice", "ancestor", "bob")],
},
];
let confidences = vec![0.8, 0.9];
let results = prop.batch_propagate(&rules, &confidences)?;
assert_eq!(results.len(), 2);
Ok(())
}
#[test]
fn test_dempster_combination() -> Result<(), Box<dyn std::error::Error>> {
let mut prop = UncertaintyPropagator::new(UncertaintyModel::DempsterShafer);
prop.set_operator(CombinationOperator::DempsterRule);
let uv1 = UncertaintyValue::new(UncertaintyModel::DempsterShafer, 0.7);
let uv2 = UncertaintyValue::new(UncertaintyModel::DempsterShafer, 0.8);
let result = prop.combine_two(&uv1, &uv2)?;
assert!((result.value - 0.903).abs() < 0.01);
Ok(())
}
#[test]
fn test_possibilistic_conjunction() -> Result<(), Box<dyn std::error::Error>> {
let mut prop = UncertaintyPropagator::new(UncertaintyModel::Possibilistic);
prop.set_operator(CombinationOperator::PossibilisticConjunction);
let uv1 = UncertaintyValue::with_secondary(
UncertaintyModel::Possibilistic,
0.8,
0.3, );
let uv2 = UncertaintyValue::with_secondary(
UncertaintyModel::Possibilistic,
0.9,
0.5, );
let result = prop.combine_two(&uv1, &uv2)?;
assert!((result.value - 0.7).abs() < 1e-6);
Ok(())
}
#[test]
fn test_weighted_sum_combination() -> Result<(), Box<dyn std::error::Error>> {
let mut prop = UncertaintyPropagator::new(UncertaintyModel::Hybrid);
prop.set_operator(CombinationOperator::WeightedSum);
let uv1 = UncertaintyValue::new(UncertaintyModel::Hybrid, 0.6);
let uv2 = UncertaintyValue::new(UncertaintyModel::Hybrid, 0.8);
let result = prop.combine_two(&uv1, &uv2)?;
assert!((result.value - 0.7).abs() < 1e-6);
Ok(())
}
}