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