#[allow(unused_imports)]
use crate::prelude::*;
use oxiz_sat::Lit;
pub type TheoryId = u32;
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct Explanation {
pub literals: Vec<Lit>,
pub theory: TheoryId,
pub justification: Option<String>,
}
impl Explanation {
pub fn new(literals: Vec<Lit>, theory: TheoryId) -> Self {
Self {
literals,
theory,
justification: None,
}
}
pub fn with_justification(literals: Vec<Lit>, theory: TheoryId, justification: String) -> Self {
Self {
literals,
theory,
justification: Some(justification),
}
}
pub fn size(&self) -> usize {
self.literals.len()
}
pub fn is_empty(&self) -> bool {
self.literals.is_empty()
}
pub fn contains(&self, lit: Lit) -> bool {
self.literals.contains(&lit)
}
}
#[derive(Debug, Clone)]
pub struct ExplanationConfig {
pub minimize: bool,
pub enable_cache: bool,
pub max_cache_size: usize,
pub generate_proofs: bool,
}
impl Default for ExplanationConfig {
fn default() -> Self {
Self {
minimize: true,
enable_cache: true,
max_cache_size: 10000,
generate_proofs: false,
}
}
}
#[derive(Debug, Clone, Default)]
pub struct ExplanationStats {
pub explanations_generated: u64,
pub explanations_minimized: u64,
pub literals_removed: u64,
pub cache_hits: u64,
pub cache_misses: u64,
}
pub struct ExplanationGenerator {
config: ExplanationConfig,
stats: ExplanationStats,
cache: FxHashMap<Vec<Lit>, Explanation>,
}
impl ExplanationGenerator {
pub fn new(config: ExplanationConfig) -> Self {
Self {
config,
stats: ExplanationStats::default(),
cache: FxHashMap::default(),
}
}
pub fn default_config() -> Self {
Self::new(ExplanationConfig::default())
}
pub fn explain_conflict(&mut self, conflict: Vec<Lit>, theory: TheoryId) -> Explanation {
self.stats.explanations_generated += 1;
if self.config.enable_cache {
if let Some(cached) = self.cache.get(&conflict) {
self.stats.cache_hits += 1;
return cached.clone();
}
self.stats.cache_misses += 1;
}
let mut explanation = Explanation::new(conflict.clone(), theory);
if self.config.minimize {
explanation = self.minimize_explanation(explanation);
}
if self.config.enable_cache && self.cache.len() < self.config.max_cache_size {
self.cache.insert(conflict, explanation.clone());
}
explanation
}
fn minimize_explanation(&mut self, explanation: Explanation) -> Explanation {
let original_size = explanation.size();
let mut seen = FxHashSet::default();
let mut minimized_lits = Vec::new();
for lit in explanation.literals {
if seen.insert(lit) {
minimized_lits.push(lit);
}
}
let removed = original_size - minimized_lits.len();
if removed > 0 {
self.stats.explanations_minimized += 1;
self.stats.literals_removed += removed as u64;
}
Explanation {
literals: minimized_lits,
theory: explanation.theory,
justification: explanation.justification,
}
}
pub fn explain_propagation(
&mut self,
propagated_lit: Lit,
antecedents: Vec<Lit>,
theory: TheoryId,
) -> Explanation {
self.stats.explanations_generated += 1;
let mut explanation = Explanation::new(antecedents, theory);
if self.config.minimize {
explanation = self.minimize_explanation(explanation);
}
if self.config.generate_proofs {
explanation.justification =
Some(format!("Theory {} propagated {:?}", theory, propagated_lit));
}
explanation
}
pub fn merge_explanations(&mut self, explanations: Vec<Explanation>) -> Explanation {
let mut all_literals = Vec::new();
let mut all_theories = Vec::new();
let mut all_justifications = Vec::new();
for exp in explanations {
all_literals.extend(exp.literals);
all_theories.push(exp.theory);
if let Some(just) = exp.justification {
all_justifications.push(just);
}
}
let theory = all_theories.first().copied().unwrap_or(0);
let mut merged = Explanation::new(all_literals, theory);
if self.config.minimize {
merged = self.minimize_explanation(merged);
}
if self.config.generate_proofs && !all_justifications.is_empty() {
merged.justification = Some(all_justifications.join("; "));
}
merged
}
pub fn clear_cache(&mut self) {
self.cache.clear();
}
pub fn stats(&self) -> &ExplanationStats {
&self.stats
}
pub fn reset_stats(&mut self) {
self.stats = ExplanationStats::default();
}
}
#[cfg(test)]
mod tests {
use super::*;
use oxiz_sat::Var;
#[test]
fn test_explanation_creation() {
let lit1 = Lit::pos(Var::new(0));
let lit2 = Lit::neg(Var::new(1));
let exp = Explanation::new(vec![lit1, lit2], 0);
assert_eq!(exp.size(), 2);
assert!(exp.contains(lit1));
assert!(exp.contains(lit2));
assert_eq!(exp.theory, 0);
}
#[test]
fn test_generator_creation() {
let generator = ExplanationGenerator::default_config();
assert_eq!(generator.stats().explanations_generated, 0);
}
#[test]
fn test_explain_conflict() {
let mut generator = ExplanationGenerator::default_config();
let lit1 = Lit::pos(Var::new(0));
let lit2 = Lit::neg(Var::new(1));
let exp = generator.explain_conflict(vec![lit1, lit2], 0);
assert_eq!(exp.size(), 2);
assert_eq!(generator.stats().explanations_generated, 1);
}
#[test]
fn test_minimization() {
let mut generator = ExplanationGenerator::default_config();
let lit1 = Lit::pos(Var::new(0));
let lit2 = Lit::pos(Var::new(0));
let exp = generator.explain_conflict(vec![lit1, lit2], 0);
assert_eq!(exp.size(), 1);
assert!(generator.stats().explanations_minimized > 0);
}
#[test]
fn test_caching() {
let mut generator = ExplanationGenerator::default_config();
let lits = vec![Lit::pos(Var::new(0)), Lit::neg(Var::new(1))];
let exp1 = generator.explain_conflict(lits.clone(), 0);
assert_eq!(generator.stats().cache_misses, 1);
let exp2 = generator.explain_conflict(lits, 0);
assert_eq!(generator.stats().cache_hits, 1);
assert_eq!(exp1, exp2);
}
#[test]
fn test_explain_propagation() {
let config = ExplanationConfig {
generate_proofs: true,
..Default::default()
};
let mut generator = ExplanationGenerator::new(config);
let prop_lit = Lit::pos(Var::new(2));
let antecedents = vec![Lit::pos(Var::new(0)), Lit::neg(Var::new(1))];
let exp = generator.explain_propagation(prop_lit, antecedents, 0);
assert_eq!(exp.size(), 2);
assert!(exp.justification.is_some());
}
#[test]
fn test_merge_explanations() {
let mut generator = ExplanationGenerator::default_config();
let exp1 = Explanation::new(vec![Lit::pos(Var::new(0))], 0);
let exp2 = Explanation::new(vec![Lit::neg(Var::new(1))], 1);
let merged = generator.merge_explanations(vec![exp1, exp2]);
assert_eq!(merged.size(), 2);
}
#[test]
fn test_clear_cache() {
let mut generator = ExplanationGenerator::default_config();
generator.explain_conflict(vec![Lit::pos(Var::new(0))], 0);
assert!(!generator.cache.is_empty());
generator.clear_cache();
assert!(generator.cache.is_empty());
}
}