#[allow(unused_imports)]
use crate::prelude::*;
use oxiz_sat::Lit;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ExplanationType {
Equality,
Bounds,
Disequality,
Arithmetic,
Array,
BitVector,
}
#[derive(Debug, Clone)]
pub struct TheoryExplanation {
pub explanation_type: ExplanationType,
pub literals: Vec<Lit>,
pub description: String,
pub proof_trace: Option<Vec<ProofStep>>,
}
#[derive(Debug, Clone)]
pub struct ProofStep {
pub description: String,
pub premises: Vec<Lit>,
pub conclusion: String,
}
#[derive(Debug, Clone)]
pub struct ExplainerConfig {
pub generate_proofs: bool,
pub minimize: bool,
pub include_descriptions: bool,
pub max_size: usize,
}
impl Default for ExplainerConfig {
fn default() -> Self {
Self {
generate_proofs: false,
minimize: true,
include_descriptions: false,
max_size: 100,
}
}
}
#[derive(Debug, Clone, Default)]
pub struct ExplainerStats {
pub explanations_generated: u64,
pub total_literals: u64,
pub avg_size: f64,
pub minimized: u64,
pub time_us: u64,
}
pub struct TheoryExplainer {
config: ExplainerConfig,
stats: ExplainerStats,
}
impl TheoryExplainer {
pub fn new() -> Self {
Self::with_config(ExplainerConfig::default())
}
pub fn with_config(config: ExplainerConfig) -> Self {
Self {
config,
stats: ExplainerStats::default(),
}
}
pub fn stats(&self) -> &ExplainerStats {
&self.stats
}
pub fn reset_stats(&mut self) {
self.stats = ExplainerStats::default();
}
pub fn explain_equality_conflict(
&mut self,
lits: Vec<Lit>,
description: Option<String>,
) -> TheoryExplanation {
#[cfg(feature = "std")]
let start = std::time::Instant::now();
let minimized_lits = if self.config.minimize {
self.minimize_explanation(&lits)
} else {
lits.clone()
};
let proof_trace = if self.config.generate_proofs {
Some(self.generate_proof_trace(&minimized_lits, ExplanationType::Equality))
} else {
None
};
let desc = if self.config.include_descriptions {
description.unwrap_or_else(|| {
format!("Equality conflict with {} literals", minimized_lits.len())
})
} else {
String::new()
};
self.stats.explanations_generated += 1;
self.stats.total_literals += minimized_lits.len() as u64;
self.update_avg_size();
#[cfg(feature = "std")]
{
self.stats.time_us += start.elapsed().as_micros() as u64;
}
TheoryExplanation {
explanation_type: ExplanationType::Equality,
literals: minimized_lits,
description: desc,
proof_trace,
}
}
pub fn explain_bounds_conflict(
&mut self,
lits: Vec<Lit>,
description: Option<String>,
) -> TheoryExplanation {
#[cfg(feature = "std")]
let start = std::time::Instant::now();
let minimized_lits = if self.config.minimize {
self.minimize_explanation(&lits)
} else {
lits.clone()
};
let proof_trace = if self.config.generate_proofs {
Some(self.generate_proof_trace(&minimized_lits, ExplanationType::Bounds))
} else {
None
};
let desc = if self.config.include_descriptions {
description.unwrap_or_else(|| {
format!("Bounds conflict with {} constraints", minimized_lits.len())
})
} else {
String::new()
};
self.stats.explanations_generated += 1;
self.stats.total_literals += minimized_lits.len() as u64;
self.update_avg_size();
#[cfg(feature = "std")]
{
self.stats.time_us += start.elapsed().as_micros() as u64;
}
TheoryExplanation {
explanation_type: ExplanationType::Bounds,
literals: minimized_lits,
description: desc,
proof_trace,
}
}
pub fn explain_arithmetic_conflict(
&mut self,
lits: Vec<Lit>,
_farkas_coefficients: Option<Vec<i64>>,
description: Option<String>,
) -> TheoryExplanation {
#[cfg(feature = "std")]
let start = std::time::Instant::now();
let minimized_lits = if self.config.minimize {
self.minimize_explanation(&lits)
} else {
lits.clone()
};
let proof_trace = if self.config.generate_proofs {
Some(self.generate_proof_trace(&minimized_lits, ExplanationType::Arithmetic))
} else {
None
};
let desc = if self.config.include_descriptions {
description.unwrap_or_else(|| "Arithmetic conflict via linear combination".to_string())
} else {
String::new()
};
self.stats.explanations_generated += 1;
self.stats.total_literals += minimized_lits.len() as u64;
self.update_avg_size();
#[cfg(feature = "std")]
{
self.stats.time_us += start.elapsed().as_micros() as u64;
}
TheoryExplanation {
explanation_type: ExplanationType::Arithmetic,
literals: minimized_lits,
description: desc,
proof_trace,
}
}
fn minimize_explanation(&mut self, lits: &[Lit]) -> Vec<Lit> {
let mut minimized = Vec::new();
let _lit_set: FxHashSet<Lit> = lits.iter().copied().collect();
for &lit in lits {
minimized.push(lit);
}
if minimized.len() < lits.len() {
self.stats.minimized += 1;
}
if minimized.len() > self.config.max_size {
minimized.truncate(self.config.max_size);
}
minimized
}
fn generate_proof_trace(&self, lits: &[Lit], exp_type: ExplanationType) -> Vec<ProofStep> {
let mut trace = Vec::new();
trace.push(ProofStep {
description: format!("Theory conflict detected: {:?}", exp_type),
premises: lits.to_vec(),
conclusion: "Contradiction".to_string(),
});
trace
}
fn update_avg_size(&mut self) {
if self.stats.explanations_generated > 0 {
self.stats.avg_size =
self.stats.total_literals as f64 / self.stats.explanations_generated as f64;
}
}
}
impl Default for TheoryExplainer {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use oxiz_sat::Var;
fn lit(var: u32, positive: bool) -> Lit {
let v = Var::new(var);
if positive { Lit::pos(v) } else { Lit::neg(v) }
}
#[test]
fn test_explainer_creation() {
let explainer = TheoryExplainer::new();
assert_eq!(explainer.stats().explanations_generated, 0);
}
#[test]
fn test_equality_explanation() {
let mut explainer = TheoryExplainer::new();
let lits = vec![lit(0, true), lit(1, false), lit(2, true)];
let explanation = explainer.explain_equality_conflict(lits.clone(), None);
assert_eq!(explanation.explanation_type, ExplanationType::Equality);
assert!(!explanation.literals.is_empty());
assert_eq!(explainer.stats().explanations_generated, 1);
}
#[test]
fn test_bounds_explanation() {
let mut explainer = TheoryExplainer::new();
let lits = vec![lit(0, true), lit(1, true)];
let explanation =
explainer.explain_bounds_conflict(lits, Some("x > 10 and x < 5".to_string()));
assert_eq!(explanation.explanation_type, ExplanationType::Bounds);
assert_eq!(explainer.stats().explanations_generated, 1);
}
#[test]
fn test_arithmetic_explanation() {
let mut explainer = TheoryExplainer::new();
let lits = vec![lit(0, false), lit(1, false), lit(2, false)];
let explanation = explainer.explain_arithmetic_conflict(lits, None, None);
assert_eq!(explanation.explanation_type, ExplanationType::Arithmetic);
assert_eq!(explainer.stats().explanations_generated, 1);
}
#[test]
fn test_minimization() {
let config = ExplainerConfig {
minimize: true,
..Default::default()
};
let mut explainer = TheoryExplainer::with_config(config);
let lits = vec![lit(0, true), lit(1, false), lit(2, true), lit(3, false)];
let explanation = explainer.explain_equality_conflict(lits, None);
assert!(explanation.literals.len() <= 4);
}
#[test]
fn test_proof_generation() {
let config = ExplainerConfig {
generate_proofs: true,
..Default::default()
};
let mut explainer = TheoryExplainer::with_config(config);
let lits = vec![lit(0, true), lit(1, false)];
let explanation = explainer.explain_equality_conflict(lits, None);
assert!(explanation.proof_trace.is_some());
let trace = explanation
.proof_trace
.expect("test operation should succeed");
assert!(!trace.is_empty());
}
#[test]
fn test_max_size_enforcement() {
let config = ExplainerConfig {
max_size: 2,
..Default::default()
};
let mut explainer = TheoryExplainer::with_config(config);
let lits = vec![lit(0, true), lit(1, false), lit(2, true), lit(3, false)];
let explanation = explainer.explain_bounds_conflict(lits, None);
assert!(explanation.literals.len() <= 2);
}
}