Skip to main content

oxiz_solver/conflict/
implication_graph.rs

1//! Implication Graph for CDCL Conflict Analysis.
2//!
3//! This module provides graph data structures and algorithms for analyzing
4//! Boolean constraint propagation and identifying conflict causes.
5//!
6//! ## Key Concepts
7//!
8//! 1. **Implication Graph**: Directed graph where nodes are literals and edges
9//!    represent propagation (antecedent → consequent)
10//! 2. **UIP (Unique Implication Point)**: Cut point in graph that all paths
11//!    from decision to conflict pass through
12//! 3. **Conflict Side**: Literals at current decision level reachable from conflict
13//!
14//! ## Applications
15//!
16//! - First UIP identification for clause learning
17//! - Decision level computation
18//! - Conflict-driven clause learning (CDCL)
19//! - Backjump level calculation
20//!
21//! ## References
22//!
23//! - Zhang et al.: "Efficient Conflict Driven Learning in a Boolean Satisfiability Solver" (ICCAD 2001)
24//! - Z3's `sat/sat_cut_simplifier.cpp`
25
26use oxiz_sat::Lit;
27use rustc_hash::{FxHashMap, FxHashSet};
28
29/// Variable type (absolute value of literal).
30pub type Var = u32;
31
32/// Decision level.
33pub type Level = u32;
34
35/// Node in implication graph.
36#[derive(Debug, Clone)]
37pub struct ImplicationNode {
38    /// The literal at this node.
39    pub literal: Lit,
40    /// Decision level when this literal was assigned.
41    pub level: Level,
42    /// Antecedent literals (reasons for propagation).
43    pub antecedents: Vec<Lit>,
44    /// Consequents (literals this one implies).
45    pub consequents: Vec<Lit>,
46    /// Is this a decision literal?
47    pub is_decision: bool,
48}
49
50impl ImplicationNode {
51    /// Create decision node.
52    pub fn decision(literal: Lit, level: Level) -> Self {
53        Self {
54            literal,
55            level,
56            antecedents: Vec::new(),
57            consequents: Vec::new(),
58            is_decision: true,
59        }
60    }
61
62    /// Create propagated node.
63    pub fn propagated(literal: Lit, level: Level, antecedents: Vec<Lit>) -> Self {
64        Self {
65            literal,
66            level,
67            antecedents,
68            consequents: Vec::new(),
69            is_decision: false,
70        }
71    }
72}
73
74/// Configuration for implication graph.
75#[derive(Debug, Clone)]
76pub struct ImplicationGraphConfig {
77    /// Enable graph compaction.
78    pub enable_compaction: bool,
79    /// Maximum graph size before compaction.
80    pub max_size: usize,
81}
82
83impl Default for ImplicationGraphConfig {
84    fn default() -> Self {
85        Self {
86            enable_compaction: true,
87            max_size: 100000,
88        }
89    }
90}
91
92/// Statistics for implication graph.
93#[derive(Debug, Clone, Default)]
94pub struct ImplicationGraphStats {
95    /// UIPs computed.
96    pub uips_computed: u64,
97    /// Graph traversals.
98    pub traversals: u64,
99    /// Nodes added.
100    pub nodes_added: u64,
101    /// Compactions performed.
102    pub compactions: u64,
103}
104
105/// Implication graph for CDCL.
106pub struct ImplicationGraph {
107    config: ImplicationGraphConfig,
108    stats: ImplicationGraphStats,
109    /// Nodes indexed by literal.
110    nodes: FxHashMap<Lit, ImplicationNode>,
111    /// Current decision level.
112    current_level: Level,
113    /// Literals at each decision level.
114    level_lits: FxHashMap<Level, Vec<Lit>>,
115}
116
117impl ImplicationGraph {
118    /// Create new implication graph.
119    pub fn new() -> Self {
120        Self::with_config(ImplicationGraphConfig::default())
121    }
122
123    /// Create with configuration.
124    pub fn with_config(config: ImplicationGraphConfig) -> Self {
125        Self {
126            config,
127            stats: ImplicationGraphStats::default(),
128            nodes: FxHashMap::default(),
129            current_level: 0,
130            level_lits: FxHashMap::default(),
131        }
132    }
133
134    /// Get statistics.
135    pub fn stats(&self) -> &ImplicationGraphStats {
136        &self.stats
137    }
138
139    /// Add decision literal.
140    pub fn add_decision(&mut self, lit: Lit, level: Level) {
141        let node = ImplicationNode::decision(lit, level);
142        self.nodes.insert(lit, node);
143        self.level_lits.entry(level).or_default().push(lit);
144        self.current_level = level;
145        self.stats.nodes_added += 1;
146
147        self.check_compaction();
148    }
149
150    /// Add propagated literal.
151    pub fn add_propagation(&mut self, lit: Lit, level: Level, antecedents: Vec<Lit>) {
152        // Update antecedent consequents
153        for &ant in &antecedents {
154            if let Some(ant_node) = self.nodes.get_mut(&ant) {
155                ant_node.consequents.push(lit);
156            }
157        }
158
159        let node = ImplicationNode::propagated(lit, level, antecedents);
160        self.nodes.insert(lit, node);
161        self.level_lits.entry(level).or_default().push(lit);
162        self.stats.nodes_added += 1;
163
164        self.check_compaction();
165    }
166
167    /// Find first UIP (Unique Implication Point) for conflict.
168    ///
169    /// Returns the literal that is the first UIP cut point.
170    pub fn find_first_uip(&mut self, conflict_lits: &[Lit], decision_level: Level) -> Option<Lit> {
171        self.stats.uips_computed += 1;
172
173        if conflict_lits.is_empty() {
174            return None;
175        }
176
177        // Collect literals at current decision level involved in conflict
178        let conflict_side = self.compute_conflict_side(conflict_lits, decision_level);
179
180        if conflict_side.is_empty() {
181            return conflict_lits.first().copied();
182        }
183
184        // Find the last literal assigned at current level in conflict side
185        // This is the first UIP (closest to conflict)
186        let mut uip = None;
187        let mut max_order = 0;
188
189        for &lit in &conflict_side {
190            // In real implementation, would track assignment order
191            // For now, use simple heuristic
192            if self.is_at_level(lit, decision_level) {
193                let order = self.get_assignment_order(lit);
194                if order > max_order {
195                    max_order = order;
196                    uip = Some(lit);
197                }
198            }
199        }
200
201        uip
202    }
203
204    /// Compute conflict side of cut.
205    ///
206    /// Returns all literals at decision level reachable from conflict.
207    fn compute_conflict_side(&mut self, conflict_lits: &[Lit], level: Level) -> FxHashSet<Lit> {
208        self.stats.traversals += 1;
209
210        let mut conflict_side = FxHashSet::default();
211        let mut frontier: Vec<Lit> = conflict_lits.to_vec();
212        let mut visited = FxHashSet::default();
213
214        // Backward reachability from conflict
215        while let Some(lit) = frontier.pop() {
216            if !visited.insert(lit) {
217                continue;
218            }
219
220            if let Some(node) = self.nodes.get(&lit)
221                && node.level == level
222            {
223                conflict_side.insert(lit);
224
225                // Add antecedents to frontier
226                for &ant in &node.antecedents {
227                    frontier.push(ant);
228                }
229            }
230        }
231
232        conflict_side
233    }
234
235    /// Check if literal is at given decision level.
236    fn is_at_level(&self, lit: Lit, level: Level) -> bool {
237        self.nodes
238            .get(&lit)
239            .map(|n| n.level == level)
240            .unwrap_or(false)
241    }
242
243    /// Get assignment order (simplified - would track actual order).
244    fn get_assignment_order(&self, lit: Lit) -> usize {
245        // Simplified: use hash value as proxy for order
246        // Real implementation would maintain assignment trail
247        lit.var().0 as usize
248    }
249
250    /// Get decision level of literal.
251    pub fn get_level(&self, lit: Lit) -> Option<Level> {
252        self.nodes.get(&lit).map(|n| n.level)
253    }
254
255    /// Get antecedents of literal.
256    pub fn get_antecedents(&self, lit: Lit) -> Option<&[Lit]> {
257        self.nodes.get(&lit).map(|n| n.antecedents.as_slice())
258    }
259
260    /// Compute backjump level for learned clause.
261    ///
262    /// Returns the second-highest decision level in the clause.
263    pub fn compute_backjump_level(&self, clause: &[Lit]) -> Level {
264        if clause.len() <= 1 {
265            return 0;
266        }
267
268        let mut levels: Vec<Level> = clause
269            .iter()
270            .filter_map(|&lit| self.get_level(lit))
271            .collect();
272
273        levels.sort_unstable();
274        levels.dedup();
275
276        // Return second-highest level (or 0 if only one level)
277        if levels.len() >= 2 {
278            levels[levels.len() - 2]
279        } else {
280            0
281        }
282    }
283
284    /// Backtrack to decision level.
285    pub fn backtrack(&mut self, level: Level) {
286        // Remove all literals above this level
287        self.nodes.retain(|_, node| node.level <= level);
288        self.level_lits.retain(|&l, _| l <= level);
289        self.current_level = level;
290    }
291
292    /// Clear graph.
293    pub fn clear(&mut self) {
294        self.nodes.clear();
295        self.level_lits.clear();
296        self.current_level = 0;
297    }
298
299    /// Check if compaction is needed.
300    fn check_compaction(&mut self) {
301        if self.config.enable_compaction && self.nodes.len() > self.config.max_size {
302            self.compact();
303        }
304    }
305
306    /// Compact graph by removing old literals.
307    fn compact(&mut self) {
308        // Remove literals from lowest levels
309        if let Some(&min_level) = self.level_lits.keys().min() {
310            self.nodes.retain(|_, node| node.level > min_level);
311            self.level_lits.remove(&min_level);
312            self.stats.compactions += 1;
313        }
314    }
315
316    /// Get number of nodes.
317    pub fn num_nodes(&self) -> usize {
318        self.nodes.len()
319    }
320
321    /// Get current decision level.
322    pub fn current_level(&self) -> Level {
323        self.current_level
324    }
325}
326
327impl Default for ImplicationGraph {
328    fn default() -> Self {
329        Self::new()
330    }
331}
332
333#[cfg(test)]
334mod tests {
335    use super::*;
336
337    fn lit(n: i32) -> Lit {
338        Lit::from_dimacs(n)
339    }
340
341    #[test]
342    fn test_graph_creation() {
343        let graph = ImplicationGraph::new();
344        assert_eq!(graph.num_nodes(), 0);
345        assert_eq!(graph.current_level(), 0);
346    }
347
348    #[test]
349    fn test_add_decision() {
350        let mut graph = ImplicationGraph::new();
351
352        graph.add_decision(lit(1), 1);
353
354        assert_eq!(graph.num_nodes(), 1);
355        assert_eq!(graph.get_level(lit(1)), Some(1));
356        assert_eq!(graph.current_level(), 1);
357    }
358
359    #[test]
360    fn test_add_propagation() {
361        let mut graph = ImplicationGraph::new();
362
363        graph.add_decision(lit(1), 1);
364        graph.add_propagation(lit(2), 1, vec![lit(1)]);
365
366        assert_eq!(graph.num_nodes(), 2);
367        assert_eq!(graph.get_antecedents(lit(2)), Some(&[lit(1)][..]));
368    }
369
370    #[test]
371    fn test_get_level() {
372        let mut graph = ImplicationGraph::new();
373
374        graph.add_decision(lit(1), 1);
375        graph.add_decision(lit(2), 2);
376
377        assert_eq!(graph.get_level(lit(1)), Some(1));
378        assert_eq!(graph.get_level(lit(2)), Some(2));
379        assert_eq!(graph.get_level(lit(3)), None);
380    }
381
382    #[test]
383    fn test_backjump_level() {
384        let mut graph = ImplicationGraph::new();
385
386        graph.add_decision(lit(1), 1);
387        graph.add_decision(lit(2), 2);
388        graph.add_decision(lit(3), 3);
389
390        let clause = vec![lit(1), lit(2), lit(3)];
391        let level = graph.compute_backjump_level(&clause);
392
393        assert_eq!(level, 2); // Second-highest level
394    }
395
396    #[test]
397    fn test_backtrack() {
398        let mut graph = ImplicationGraph::new();
399
400        graph.add_decision(lit(1), 1);
401        graph.add_decision(lit(2), 2);
402        graph.add_decision(lit(3), 3);
403
404        graph.backtrack(1);
405
406        assert_eq!(graph.num_nodes(), 1);
407        assert_eq!(graph.get_level(lit(1)), Some(1));
408        assert_eq!(graph.get_level(lit(2)), None);
409        assert_eq!(graph.get_level(lit(3)), None);
410    }
411
412    #[test]
413    fn test_clear() {
414        let mut graph = ImplicationGraph::new();
415
416        graph.add_decision(lit(1), 1);
417        graph.add_decision(lit(2), 2);
418
419        graph.clear();
420
421        assert_eq!(graph.num_nodes(), 0);
422        assert_eq!(graph.current_level(), 0);
423    }
424
425    #[test]
426    fn test_find_first_uip() {
427        let mut graph = ImplicationGraph::new();
428
429        graph.add_decision(lit(1), 1);
430        graph.add_propagation(lit(2), 1, vec![lit(1)]);
431        graph.add_propagation(lit(3), 1, vec![lit(2)]);
432
433        let conflict = vec![lit(3)];
434        let uip = graph.find_first_uip(&conflict, 1);
435
436        assert!(uip.is_some());
437    }
438
439    #[test]
440    fn test_compaction() {
441        let config = ImplicationGraphConfig {
442            enable_compaction: true,
443            max_size: 2,
444        };
445
446        let mut graph = ImplicationGraph::with_config(config);
447
448        graph.add_decision(lit(1), 1);
449        graph.add_decision(lit(2), 2);
450        graph.add_decision(lit(3), 3); // Should trigger compaction
451
452        assert!(graph.stats().compactions > 0);
453    }
454}