use crate::RuleAtom;
use anyhow::{anyhow, Result};
use once_cell::sync::Lazy;
use scirs2_core::metrics::{Counter, Timer};
use scirs2_core::random::{Distribution, Uniform};
use std::collections::{HashMap, HashSet};
use super::problog_inference::{apply_substitution_to_body, materialize, unify_atoms};
use super::problog_types::{
DerivationTree, EvaluationStrategy, ProbLogStats, ProbabilisticFact, ProbabilisticRule,
};
static PROBLOG_QUERIES: Lazy<Counter> = Lazy::new(|| Counter::new("problog_queries".to_string()));
static PROBLOG_INFERENCES: Lazy<Counter> =
Lazy::new(|| Counter::new("problog_inferences".to_string()));
static PROBLOG_QUERY_TIME: Lazy<Timer> = Lazy::new(|| Timer::new("problog_query_time".to_string()));
pub struct ProbLogEngine {
probabilistic_facts: HashMap<RuleAtom, f64>,
deterministic_facts: HashSet<RuleAtom>,
probabilistic_rules: Vec<ProbabilisticRule>,
query_cache: HashMap<RuleAtom, f64>,
recursion_stack: HashSet<RuleAtom>,
max_depth: usize,
current_depth: usize,
materialized_facts: HashMap<RuleAtom, f64>,
materialization_valid: bool,
strategy: EvaluationStrategy,
max_fixpoint_iterations: usize,
pub stats: ProbLogStats,
}
impl Default for ProbLogEngine {
fn default() -> Self {
Self::new()
}
}
impl ProbLogEngine {
pub fn new() -> Self {
Self {
probabilistic_facts: HashMap::new(),
deterministic_facts: HashSet::new(),
probabilistic_rules: Vec::new(),
query_cache: HashMap::new(),
recursion_stack: HashSet::new(),
max_depth: 100,
current_depth: 0,
materialized_facts: HashMap::new(),
materialization_valid: false,
strategy: EvaluationStrategy::Auto,
max_fixpoint_iterations: 1000,
stats: ProbLogStats::default(),
}
}
pub fn with_max_depth(mut self, max_depth: usize) -> Self {
self.max_depth = max_depth;
self
}
pub fn with_strategy(mut self, strategy: EvaluationStrategy) -> Self {
self.strategy = strategy;
self
}
pub fn with_max_fixpoint_iterations(mut self, max_iterations: usize) -> Self {
self.max_fixpoint_iterations = max_iterations;
self
}
pub fn add_probabilistic_fact(&mut self, fact: ProbabilisticFact) {
if (fact.probability - 1.0).abs() < 1e-10 {
self.deterministic_facts.insert(fact.fact);
} else {
self.probabilistic_facts.insert(fact.fact, fact.probability);
}
self.query_cache.clear();
self.materialization_valid = false;
}
pub fn add_fact(&mut self, fact: RuleAtom) {
self.deterministic_facts.insert(fact);
self.query_cache.clear();
self.materialization_valid = false;
}
pub fn add_rule(&mut self, rule: ProbabilisticRule) {
self.probabilistic_rules.push(rule);
self.query_cache.clear();
self.materialization_valid = false;
}
pub fn query_probability(&mut self, query: &RuleAtom) -> Result<f64> {
let _timer = PROBLOG_QUERY_TIME.start();
self.stats.queries += 1;
PROBLOG_QUERIES.inc();
let use_bottom_up = match self.strategy {
EvaluationStrategy::TopDown => false,
EvaluationStrategy::BottomUp => true,
EvaluationStrategy::Auto => self.has_recursive_rules(),
};
if use_bottom_up {
return self.query_materialized(query);
}
if self.current_depth > self.max_depth {
return Err(anyhow!(
"Maximum recursion depth exceeded: {}",
self.max_depth
));
}
if self.recursion_stack.contains(query) {
return self.query_materialized(query);
}
if let Some(&prob) = self.query_cache.get(query) {
self.stats.cache_hits += 1;
return Ok(prob);
}
self.stats.cache_misses += 1;
if self.deterministic_facts.contains(query) {
self.query_cache.insert(query.clone(), 1.0);
return Ok(1.0);
}
if let Some(&prob) = self.probabilistic_facts.get(query) {
self.query_cache.insert(query.clone(), prob);
return Ok(prob);
}
self.recursion_stack.insert(query.clone());
self.current_depth += 1;
let prob = self.derive_probability(query)?;
self.query_cache.insert(query.clone(), prob);
self.recursion_stack.remove(query);
self.current_depth -= 1;
Ok(prob)
}
fn has_recursive_rules(&self) -> bool {
for rule in &self.probabilistic_rules {
for head_atom in &rule.rule.head {
if let RuleAtom::Triple {
predicate: head_pred,
..
} = head_atom
{
for body_atom in &rule.rule.body {
if let RuleAtom::Triple {
predicate: body_pred,
..
} = body_atom
{
if head_pred == body_pred {
return true;
}
}
}
}
}
}
false
}
fn derive_probability(&mut self, query: &RuleAtom) -> Result<f64> {
let mut total_prob = 0.0;
for prob_rule in &self.probabilistic_rules.clone() {
for head_atom in &prob_rule.rule.head {
if let Some(substitution) = unify_atoms(head_atom, query) {
let instantiated_body =
apply_substitution_to_body(&prob_rule.rule.body, &substitution);
let body_prob = self.evaluate_body(&instantiated_body)?;
let derivation_prob = body_prob * prob_rule.probability.unwrap_or(1.0);
total_prob = total_prob + derivation_prob - (total_prob * derivation_prob);
self.stats.inferences += 1;
PROBLOG_INFERENCES.inc();
}
}
}
Ok(total_prob)
}
fn evaluate_body(&mut self, body: &[RuleAtom]) -> Result<f64> {
let mut prob = 1.0;
for atom in body {
let atom_prob = self.query_probability(atom)?;
prob *= atom_prob;
}
Ok(prob)
}
pub fn materialize(&mut self) -> Result<()> {
if self.materialization_valid {
return Ok(());
}
self.materialized_facts.clear();
let result = materialize(
&self.probabilistic_facts,
&self.deterministic_facts,
&self.probabilistic_rules,
self.max_fixpoint_iterations,
&mut self.stats,
)?;
self.materialized_facts = result;
self.materialization_valid = true;
Ok(())
}
pub fn query_materialized(&mut self, query: &RuleAtom) -> Result<f64> {
if !self.materialization_valid {
self.materialize()?;
}
let prob = self.materialized_facts.get(query).copied().unwrap_or(0.0);
Ok(prob)
}
pub fn sample(&mut self) -> HashSet<RuleAtom> {
use scirs2_core::random::rng;
let mut rng_instance = rng();
let uniform = Uniform::new(0.0, 1.0).expect("distribution parameters are valid");
let mut sampled_facts = HashSet::new();
for (fact, &prob) in &self.probabilistic_facts {
if uniform.sample(&mut rng_instance) < prob {
sampled_facts.insert(fact.clone());
}
}
sampled_facts.extend(self.deterministic_facts.iter().cloned());
sampled_facts
}
pub fn monte_carlo_query(&mut self, query: &RuleAtom, samples: usize) -> Result<f64> {
let mut successes = 0;
for _ in 0..samples {
let sampled_world = self.sample();
if sampled_world.contains(query) {
successes += 1;
}
}
Ok(successes as f64 / samples as f64)
}
pub fn explain(&mut self, query: &RuleAtom) -> Result<Option<DerivationTree>> {
if self.deterministic_facts.contains(query) {
return Ok(Some(DerivationTree::leaf(query.clone(), 1.0)));
}
if let Some(&prob) = self.probabilistic_facts.get(query) {
return Ok(Some(DerivationTree::leaf(query.clone(), prob)));
}
for prob_rule in &self.probabilistic_rules.clone() {
for head_atom in &prob_rule.rule.head {
if let Some(substitution) = unify_atoms(head_atom, query) {
let instantiated_body =
apply_substitution_to_body(&prob_rule.rule.body, &substitution);
let mut premises = Vec::new();
let mut body_prob = 1.0;
for body_atom in &instantiated_body {
if let Some(tree) = self.explain(body_atom)? {
body_prob *= tree.probability;
premises.push(tree);
} else {
body_prob = 0.0;
break;
}
}
if body_prob > 0.0 {
let total_prob = body_prob * prob_rule.probability.unwrap_or(1.0);
return Ok(Some(DerivationTree::node(
query.clone(),
total_prob,
premises,
)));
}
}
}
}
Ok(None)
}
pub fn clear_cache(&mut self) {
self.query_cache.clear();
}
pub fn reset_stats(&mut self) {
self.stats = ProbLogStats::default();
}
}