1use oxiz_sat::Lit;
28use rustc_hash::FxHashSet;
29
30#[derive(Debug, Clone, Copy, PartialEq, Eq)]
32pub enum ExplanationType {
33 Equality,
35 Bounds,
37 Disequality,
39 Arithmetic,
41 Array,
43 BitVector,
45}
46
47#[derive(Debug, Clone)]
49pub struct TheoryExplanation {
50 pub explanation_type: ExplanationType,
52 pub literals: Vec<Lit>,
54 pub description: String,
56 pub proof_trace: Option<Vec<ProofStep>>,
58}
59
60#[derive(Debug, Clone)]
62pub struct ProofStep {
63 pub description: String,
65 pub premises: Vec<Lit>,
67 pub conclusion: String,
69}
70
71#[derive(Debug, Clone)]
73pub struct ExplainerConfig {
74 pub generate_proofs: bool,
76 pub minimize: bool,
78 pub include_descriptions: bool,
80 pub max_size: usize,
82}
83
84impl Default for ExplainerConfig {
85 fn default() -> Self {
86 Self {
87 generate_proofs: false,
88 minimize: true,
89 include_descriptions: false,
90 max_size: 100,
91 }
92 }
93}
94
95#[derive(Debug, Clone, Default)]
97pub struct ExplainerStats {
98 pub explanations_generated: u64,
100 pub total_literals: u64,
102 pub avg_size: f64,
104 pub minimized: u64,
106 pub time_us: u64,
108}
109
110pub struct TheoryExplainer {
112 config: ExplainerConfig,
113 stats: ExplainerStats,
114}
115
116impl TheoryExplainer {
117 pub fn new() -> Self {
119 Self::with_config(ExplainerConfig::default())
120 }
121
122 pub fn with_config(config: ExplainerConfig) -> Self {
124 Self {
125 config,
126 stats: ExplainerStats::default(),
127 }
128 }
129
130 pub fn stats(&self) -> &ExplainerStats {
132 &self.stats
133 }
134
135 pub fn reset_stats(&mut self) {
137 self.stats = ExplainerStats::default();
138 }
139
140 pub fn explain_equality_conflict(
145 &mut self,
146 lits: Vec<Lit>,
147 description: Option<String>,
148 ) -> TheoryExplanation {
149 let start = std::time::Instant::now();
150
151 let minimized_lits = if self.config.minimize {
152 self.minimize_explanation(&lits)
153 } else {
154 lits.clone()
155 };
156
157 let proof_trace = if self.config.generate_proofs {
158 Some(self.generate_proof_trace(&minimized_lits, ExplanationType::Equality))
159 } else {
160 None
161 };
162
163 let desc = if self.config.include_descriptions {
164 description.unwrap_or_else(|| {
165 format!("Equality conflict with {} literals", minimized_lits.len())
166 })
167 } else {
168 String::new()
169 };
170
171 self.stats.explanations_generated += 1;
172 self.stats.total_literals += minimized_lits.len() as u64;
173 self.update_avg_size();
174 self.stats.time_us += start.elapsed().as_micros() as u64;
175
176 TheoryExplanation {
177 explanation_type: ExplanationType::Equality,
178 literals: minimized_lits,
179 description: desc,
180 proof_trace,
181 }
182 }
183
184 pub fn explain_bounds_conflict(
188 &mut self,
189 lits: Vec<Lit>,
190 description: Option<String>,
191 ) -> TheoryExplanation {
192 let start = std::time::Instant::now();
193
194 let minimized_lits = if self.config.minimize {
195 self.minimize_explanation(&lits)
196 } else {
197 lits.clone()
198 };
199
200 let proof_trace = if self.config.generate_proofs {
201 Some(self.generate_proof_trace(&minimized_lits, ExplanationType::Bounds))
202 } else {
203 None
204 };
205
206 let desc = if self.config.include_descriptions {
207 description.unwrap_or_else(|| {
208 format!("Bounds conflict with {} constraints", minimized_lits.len())
209 })
210 } else {
211 String::new()
212 };
213
214 self.stats.explanations_generated += 1;
215 self.stats.total_literals += minimized_lits.len() as u64;
216 self.update_avg_size();
217 self.stats.time_us += start.elapsed().as_micros() as u64;
218
219 TheoryExplanation {
220 explanation_type: ExplanationType::Bounds,
221 literals: minimized_lits,
222 description: desc,
223 proof_trace,
224 }
225 }
226
227 pub fn explain_arithmetic_conflict(
232 &mut self,
233 lits: Vec<Lit>,
234 _farkas_coefficients: Option<Vec<i64>>,
235 description: Option<String>,
236 ) -> TheoryExplanation {
237 let start = std::time::Instant::now();
238
239 let minimized_lits = if self.config.minimize {
243 self.minimize_explanation(&lits)
244 } else {
245 lits.clone()
246 };
247
248 let proof_trace = if self.config.generate_proofs {
249 Some(self.generate_proof_trace(&minimized_lits, ExplanationType::Arithmetic))
250 } else {
251 None
252 };
253
254 let desc = if self.config.include_descriptions {
255 description.unwrap_or_else(|| "Arithmetic conflict via linear combination".to_string())
256 } else {
257 String::new()
258 };
259
260 self.stats.explanations_generated += 1;
261 self.stats.total_literals += minimized_lits.len() as u64;
262 self.update_avg_size();
263 self.stats.time_us += start.elapsed().as_micros() as u64;
264
265 TheoryExplanation {
266 explanation_type: ExplanationType::Arithmetic,
267 literals: minimized_lits,
268 description: desc,
269 proof_trace,
270 }
271 }
272
273 fn minimize_explanation(&mut self, lits: &[Lit]) -> Vec<Lit> {
275 let mut minimized = Vec::new();
281 let _lit_set: FxHashSet<Lit> = lits.iter().copied().collect();
282
283 for &lit in lits {
284 minimized.push(lit);
287 }
288
289 if minimized.len() < lits.len() {
290 self.stats.minimized += 1;
291 }
292
293 if minimized.len() > self.config.max_size {
295 minimized.truncate(self.config.max_size);
296 }
297
298 minimized
299 }
300
301 fn generate_proof_trace(&self, lits: &[Lit], exp_type: ExplanationType) -> Vec<ProofStep> {
303 let mut trace = Vec::new();
304
305 trace.push(ProofStep {
307 description: format!("Theory conflict detected: {:?}", exp_type),
308 premises: lits.to_vec(),
309 conclusion: "Contradiction".to_string(),
310 });
311
312 trace
315 }
316
317 fn update_avg_size(&mut self) {
319 if self.stats.explanations_generated > 0 {
320 self.stats.avg_size =
321 self.stats.total_literals as f64 / self.stats.explanations_generated as f64;
322 }
323 }
324}
325
326impl Default for TheoryExplainer {
327 fn default() -> Self {
328 Self::new()
329 }
330}
331
332#[cfg(test)]
333mod tests {
334 use super::*;
335 use oxiz_sat::Var;
336
337 fn lit(var: u32, positive: bool) -> Lit {
338 let v = Var::new(var);
339 if positive { Lit::pos(v) } else { Lit::neg(v) }
340 }
341
342 #[test]
343 fn test_explainer_creation() {
344 let explainer = TheoryExplainer::new();
345 assert_eq!(explainer.stats().explanations_generated, 0);
346 }
347
348 #[test]
349 fn test_equality_explanation() {
350 let mut explainer = TheoryExplainer::new();
351 let lits = vec![lit(0, true), lit(1, false), lit(2, true)];
352
353 let explanation = explainer.explain_equality_conflict(lits.clone(), None);
354
355 assert_eq!(explanation.explanation_type, ExplanationType::Equality);
356 assert!(!explanation.literals.is_empty());
357 assert_eq!(explainer.stats().explanations_generated, 1);
358 }
359
360 #[test]
361 fn test_bounds_explanation() {
362 let mut explainer = TheoryExplainer::new();
363 let lits = vec![lit(0, true), lit(1, true)];
364
365 let explanation =
366 explainer.explain_bounds_conflict(lits, Some("x > 10 and x < 5".to_string()));
367
368 assert_eq!(explanation.explanation_type, ExplanationType::Bounds);
369 assert_eq!(explainer.stats().explanations_generated, 1);
370 }
371
372 #[test]
373 fn test_arithmetic_explanation() {
374 let mut explainer = TheoryExplainer::new();
375 let lits = vec![lit(0, false), lit(1, false), lit(2, false)];
376
377 let explanation = explainer.explain_arithmetic_conflict(lits, None, None);
378
379 assert_eq!(explanation.explanation_type, ExplanationType::Arithmetic);
380 assert_eq!(explainer.stats().explanations_generated, 1);
381 }
382
383 #[test]
384 fn test_minimization() {
385 let config = ExplainerConfig {
386 minimize: true,
387 ..Default::default()
388 };
389 let mut explainer = TheoryExplainer::with_config(config);
390
391 let lits = vec![lit(0, true), lit(1, false), lit(2, true), lit(3, false)];
392
393 let explanation = explainer.explain_equality_conflict(lits, None);
394
395 assert!(explanation.literals.len() <= 4);
397 }
398
399 #[test]
400 fn test_proof_generation() {
401 let config = ExplainerConfig {
402 generate_proofs: true,
403 ..Default::default()
404 };
405 let mut explainer = TheoryExplainer::with_config(config);
406
407 let lits = vec![lit(0, true), lit(1, false)];
408
409 let explanation = explainer.explain_equality_conflict(lits, None);
410
411 assert!(explanation.proof_trace.is_some());
412 let trace = explanation.proof_trace.unwrap();
413 assert!(!trace.is_empty());
414 }
415
416 #[test]
417 fn test_max_size_enforcement() {
418 let config = ExplainerConfig {
419 max_size: 2,
420 ..Default::default()
421 };
422 let mut explainer = TheoryExplainer::with_config(config);
423
424 let lits = vec![lit(0, true), lit(1, false), lit(2, true), lit(3, false)];
425
426 let explanation = explainer.explain_bounds_conflict(lits, None);
427
428 assert!(explanation.literals.len() <= 2);
429 }
430}