Skip to main content

oxiz_sat/
resolution_graph.rs

1//! Resolution Graph Analysis
2//!
3//! Analyzes the structure of resolution proofs to improve clause learning
4//! and branching decisions. This module builds and analyzes resolution DAGs
5//! (Directed Acyclic Graphs) to identify patterns that indicate good vs bad
6//! decisions during search.
7//!
8//! Key features:
9//! - Resolution DAG construction from conflict analysis
10//! - Graph-based clause quality metrics
11//! - Variable importance scoring based on resolution structure
12//! - Resolution pattern detection for better learning
13
14use crate::literal::{Lit, Var};
15#[allow(unused_imports)]
16use crate::prelude::*;
17
18/// Node in the resolution graph
19#[derive(Debug, Clone)]
20pub struct ResolutionNode {
21    /// Unique ID for this node
22    id: usize,
23    /// The clause at this node (None for decision nodes)
24    clause: Option<Vec<Lit>>,
25    /// IDs of parent nodes (clauses that were resolved to produce this)
26    parents: Vec<usize>,
27    /// The variable that was resolved on (if this is a resolution node)
28    resolved_var: Option<Var>,
29    /// Decision level where this clause was derived
30    decision_level: usize,
31    /// Whether this is a decision node
32    is_decision: bool,
33}
34
35impl ResolutionNode {
36    /// Create a new resolution node
37    pub fn new(id: usize, clause: Vec<Lit>, decision_level: usize) -> Self {
38        Self {
39            id,
40            clause: Some(clause),
41            parents: Vec::new(),
42            resolved_var: None,
43            decision_level,
44            is_decision: false,
45        }
46    }
47
48    /// Create a decision node
49    pub fn decision(id: usize, literal: Lit, decision_level: usize) -> Self {
50        Self {
51            id,
52            clause: Some(vec![literal]),
53            parents: Vec::new(),
54            resolved_var: None,
55            decision_level,
56            is_decision: true,
57        }
58    }
59
60    /// Mark this node as a resolution of two parent clauses
61    pub fn add_resolution(&mut self, parent1: usize, parent2: usize, resolved_var: Var) {
62        self.parents.push(parent1);
63        self.parents.push(parent2);
64        self.resolved_var = Some(resolved_var);
65    }
66
67    /// Get the clause at this node
68    pub fn clause(&self) -> Option<&[Lit]> {
69        self.clause.as_deref()
70    }
71
72    /// Get the parent node IDs
73    pub fn parents(&self) -> &[usize] {
74        &self.parents
75    }
76
77    /// Get the variable this node resolved on
78    pub fn resolved_var(&self) -> Option<Var> {
79        self.resolved_var
80    }
81
82    /// Check if this is a decision node
83    pub fn is_decision(&self) -> bool {
84        self.is_decision
85    }
86
87    /// Get the decision level
88    pub fn decision_level(&self) -> usize {
89        self.decision_level
90    }
91}
92
93/// Resolution Graph for analyzing proof structure
94#[derive(Debug)]
95pub struct ResolutionGraph {
96    /// All nodes in the graph
97    nodes: Vec<ResolutionNode>,
98    /// Map from clause hash to node ID for deduplication
99    clause_map: HashMap<u64, usize>,
100    /// Statistics
101    stats: GraphStats,
102}
103
104/// Statistics about the resolution graph
105#[derive(Debug, Default, Clone)]
106pub struct GraphStats {
107    /// Total number of resolution steps
108    pub resolutions: usize,
109    /// Total number of decision nodes
110    pub decisions: usize,
111    /// Maximum graph depth (longest path from leaf to root)
112    pub max_depth: usize,
113    /// Average number of parents per node
114    pub avg_parents: f64,
115    /// Variables that participate in many resolutions
116    pub frequent_vars: HashMap<Var, usize>,
117}
118
119impl ResolutionGraph {
120    /// Create a new resolution graph
121    pub fn new() -> Self {
122        Self {
123            nodes: Vec::new(),
124            clause_map: HashMap::new(),
125            stats: GraphStats::default(),
126        }
127    }
128
129    /// Add a clause node to the graph
130    pub fn add_clause(&mut self, clause: Vec<Lit>, decision_level: usize) -> usize {
131        let hash = Self::hash_clause(&clause);
132
133        // Check if we already have this clause
134        if let Some(&node_id) = self.clause_map.get(&hash) {
135            return node_id;
136        }
137
138        let node_id = self.nodes.len();
139        let node = ResolutionNode::new(node_id, clause, decision_level);
140
141        self.nodes.push(node);
142        self.clause_map.insert(hash, node_id);
143
144        node_id
145    }
146
147    /// Add a decision node to the graph
148    pub fn add_decision(&mut self, literal: Lit, decision_level: usize) -> usize {
149        let node_id = self.nodes.len();
150        let node = ResolutionNode::decision(node_id, literal, decision_level);
151
152        self.nodes.push(node);
153        self.stats.decisions += 1;
154
155        node_id
156    }
157
158    /// Record a resolution between two clauses
159    pub fn add_resolution(
160        &mut self,
161        parent1_id: usize,
162        parent2_id: usize,
163        resolved_var: Var,
164        result_clause: Vec<Lit>,
165        decision_level: usize,
166    ) -> usize {
167        let result_id = self.add_clause(result_clause, decision_level);
168
169        // Update the result node to record the resolution
170        if let Some(node) = self.nodes.get_mut(result_id)
171            && node.parents.is_empty()
172        {
173            // Only add parents if not already set (for deduplication)
174            node.add_resolution(parent1_id, parent2_id, resolved_var);
175            self.stats.resolutions += 1;
176
177            // Track variable frequency
178            *self.stats.frequent_vars.entry(resolved_var).or_insert(0) += 1;
179        }
180
181        result_id
182    }
183
184    /// Compute graph depth starting from a given node
185    pub fn compute_depth(&self, node_id: usize) -> usize {
186        let mut visited = HashSet::new();
187        self.compute_depth_recursive(node_id, &mut visited)
188    }
189
190    /// Recursive depth computation with cycle detection
191    fn compute_depth_recursive(&self, node_id: usize, visited: &mut HashSet<usize>) -> usize {
192        if visited.contains(&node_id) {
193            return 0; // Cycle detected (shouldn't happen in DAG)
194        }
195
196        visited.insert(node_id);
197
198        let node = &self.nodes[node_id];
199        if node.parents.is_empty() {
200            return 1; // Leaf node
201        }
202
203        let max_parent_depth = node
204            .parents
205            .iter()
206            .map(|&parent_id| self.compute_depth_recursive(parent_id, visited))
207            .max()
208            .unwrap_or(0);
209
210        max_parent_depth + 1
211    }
212
213    /// Analyze the graph and update statistics
214    pub fn analyze(&mut self) {
215        if self.nodes.is_empty() {
216            return;
217        }
218
219        // Compute maximum depth
220        self.stats.max_depth = (0..self.nodes.len())
221            .map(|id| self.compute_depth(id))
222            .max()
223            .unwrap_or(0);
224
225        // Compute average number of parents
226        let total_parents: usize = self.nodes.iter().map(|n| n.parents.len()).sum();
227        self.stats.avg_parents = total_parents as f64 / self.nodes.len() as f64;
228    }
229
230    /// Get the top-k most frequently resolved variables
231    pub fn get_frequent_vars(&self, k: usize) -> Vec<(Var, usize)> {
232        let mut vars: Vec<_> = self
233            .stats
234            .frequent_vars
235            .iter()
236            .map(|(&var, &count)| (var, count))
237            .collect();
238
239        vars.sort_by_key(|item| std::cmp::Reverse(item.1));
240        vars.truncate(k);
241        vars
242    }
243
244    /// Compute clause quality based on resolution graph structure
245    ///
246    /// Lower scores indicate better quality clauses:
247    /// - Shorter resolution paths are better (fewer resolution steps)
248    /// - Clauses involving frequently-resolved variables are more important
249    /// - Clauses at lower decision levels are more general
250    pub fn clause_quality(&self, node_id: usize) -> f64 {
251        if node_id >= self.nodes.len() {
252            return f64::MAX;
253        }
254
255        let node = &self.nodes[node_id];
256        let depth = self.compute_depth(node_id) as f64;
257        let decision_level = node.decision_level as f64;
258
259        // Count how many literals involve frequently-resolved variables
260        let freq_score = if let Some(clause) = node.clause() {
261            clause
262                .iter()
263                .filter_map(|lit| {
264                    self.stats
265                        .frequent_vars
266                        .get(&lit.var())
267                        .map(|&count| count as f64)
268                })
269                .sum::<f64>()
270        } else {
271            0.0
272        };
273
274        // Quality = depth + decision_level / (1 + freq_score)
275        // Lower is better
276        depth + decision_level / (1.0 + freq_score)
277    }
278
279    /// Find redundant resolutions in the graph
280    ///
281    /// Returns node IDs of resolutions that could be eliminated
282    pub fn find_redundant_resolutions(&self) -> Vec<usize> {
283        let mut redundant = Vec::new();
284
285        for (i, node) in self.nodes.iter().enumerate() {
286            if node.parents.len() < 2 {
287                continue; // Not a resolution node
288            }
289
290            // Check if this resolution could be bypassed
291            // A resolution is redundant if we can reach the same clause
292            // through a shorter path
293            if self.has_shorter_path(i) {
294                redundant.push(i);
295            }
296        }
297
298        redundant
299    }
300
301    /// Check if there's a shorter path to derive the same clause
302    fn has_shorter_path(&self, node_id: usize) -> bool {
303        let node = &self.nodes[node_id];
304        let Some(target_clause) = node.clause() else {
305            return false;
306        };
307
308        let target_hash = Self::hash_clause(target_clause);
309
310        // BFS from all leaf nodes to see if we can reach this clause faster
311        let mut queue = VecDeque::new();
312        let mut visited = HashSet::new();
313        let mut depths = HashMap::new();
314
315        // Start from leaf nodes (nodes with no parents)
316        for (id, n) in self.nodes.iter().enumerate() {
317            if n.parents.is_empty() && id != node_id {
318                queue.push_back(id);
319                depths.insert(id, 0);
320            }
321        }
322
323        while let Some(current_id) = queue.pop_front() {
324            if visited.contains(&current_id) {
325                continue;
326            }
327            visited.insert(current_id);
328
329            let current_depth = depths[&current_id];
330
331            // Check if this node has the same clause
332            if let Some(clause) = self.nodes[current_id].clause()
333                && Self::hash_clause(clause) == target_hash
334                && current_depth < self.compute_depth(node_id)
335            {
336                return true; // Found a shorter path
337            }
338
339            // Explore children (nodes that use this as a parent)
340            for (child_id, child) in self.nodes.iter().enumerate() {
341                if child.parents.contains(&current_id) && !visited.contains(&child_id) {
342                    queue.push_back(child_id);
343                    depths.insert(child_id, current_depth + 1);
344                }
345            }
346        }
347
348        false
349    }
350
351    /// Hash a clause for deduplication
352    fn hash_clause(clause: &[Lit]) -> u64 {
353        use core::hash::BuildHasher;
354
355        let mut sorted = clause.to_vec();
356        sorted.sort_unstable_by_key(|lit| lit.code());
357
358        let build = core::hash::BuildHasherDefault::<rustc_hash::FxHasher>::default();
359        build.hash_one(&sorted)
360    }
361
362    /// Get statistics about the graph
363    pub fn stats(&self) -> &GraphStats {
364        &self.stats
365    }
366
367    /// Clear the graph
368    pub fn clear(&mut self) {
369        self.nodes.clear();
370        self.clause_map.clear();
371        self.stats = GraphStats::default();
372    }
373
374    /// Get the number of nodes in the graph
375    pub fn num_nodes(&self) -> usize {
376        self.nodes.len()
377    }
378
379    /// Get a node by ID
380    pub fn get_node(&self, node_id: usize) -> Option<&ResolutionNode> {
381        self.nodes.get(node_id)
382    }
383}
384
385impl Default for ResolutionGraph {
386    fn default() -> Self {
387        Self::new()
388    }
389}
390
391/// Resolution Graph Analyzer
392///
393/// Provides high-level analysis of resolution graphs to guide solver decisions
394#[derive(Debug)]
395pub struct ResolutionAnalyzer {
396    /// The resolution graph being analyzed
397    graph: ResolutionGraph,
398    /// Variable scores based on resolution frequency
399    var_scores: HashMap<Var, f64>,
400    /// Whether analysis is enabled
401    enabled: bool,
402}
403
404impl ResolutionAnalyzer {
405    /// Create a new resolution analyzer
406    pub fn new() -> Self {
407        Self {
408            graph: ResolutionGraph::new(),
409            var_scores: HashMap::new(),
410            enabled: true,
411        }
412    }
413
414    /// Enable or disable analysis
415    pub fn set_enabled(&mut self, enabled: bool) {
416        self.enabled = enabled;
417    }
418
419    /// Check if analysis is enabled
420    pub fn is_enabled(&self) -> bool {
421        self.enabled
422    }
423
424    /// Get the resolution graph
425    pub fn graph(&self) -> &ResolutionGraph {
426        &self.graph
427    }
428
429    /// Get mutable access to the resolution graph
430    pub fn graph_mut(&mut self) -> &mut ResolutionGraph {
431        &mut self.graph
432    }
433
434    /// Analyze the current graph and update variable scores
435    pub fn analyze(&mut self) {
436        if !self.enabled {
437            return;
438        }
439
440        self.graph.analyze();
441
442        // Update variable scores based on resolution frequency and graph structure
443        self.var_scores.clear();
444
445        for (&var, &count) in &self.graph.stats.frequent_vars {
446            // Variables that appear in many resolutions are more important
447            let frequency_score = count as f64;
448
449            // Also consider the quality of clauses they appear in
450            let quality_score: f64 = self
451                .graph
452                .nodes
453                .iter()
454                .filter(|node| {
455                    node.clause()
456                        .map(|c| c.iter().any(|lit| lit.var() == var))
457                        .unwrap_or(false)
458                })
459                .map(|node| 1.0 / (1.0 + self.graph.clause_quality(node.id)))
460                .sum();
461
462            self.var_scores.insert(var, frequency_score + quality_score);
463        }
464    }
465
466    /// Get the importance score for a variable
467    ///
468    /// Higher scores indicate more important variables for branching
469    pub fn variable_importance(&self, var: Var) -> f64 {
470        self.var_scores.get(&var).copied().unwrap_or(0.0)
471    }
472
473    /// Get the top-k most important variables
474    pub fn get_important_vars(&self, k: usize) -> Vec<(Var, f64)> {
475        let mut vars: Vec<_> = self
476            .var_scores
477            .iter()
478            .map(|(&var, &score)| (var, score))
479            .collect();
480
481        vars.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(core::cmp::Ordering::Equal));
482        vars.truncate(k);
483        vars
484    }
485
486    /// Clear the analyzer state
487    pub fn clear(&mut self) {
488        self.graph.clear();
489        self.var_scores.clear();
490    }
491
492    /// Get statistics
493    pub fn stats(&self) -> &GraphStats {
494        self.graph.stats()
495    }
496}
497
498impl Default for ResolutionAnalyzer {
499    fn default() -> Self {
500        Self::new()
501    }
502}
503
504#[cfg(test)]
505mod tests {
506    use super::*;
507
508    #[test]
509    fn test_resolution_graph_creation() {
510        let graph = ResolutionGraph::new();
511        assert_eq!(graph.num_nodes(), 0);
512    }
513
514    #[test]
515    fn test_add_clause() {
516        let mut graph = ResolutionGraph::new();
517        let v0 = Var(0);
518        let v1 = Var(1);
519
520        let clause1 = vec![Lit::pos(v0), Lit::pos(v1)];
521        let id1 = graph.add_clause(clause1.clone(), 0);
522
523        assert_eq!(id1, 0);
524        assert_eq!(graph.num_nodes(), 1);
525
526        // Adding same clause should return same ID
527        let id2 = graph.add_clause(clause1, 0);
528        assert_eq!(id1, id2);
529        assert_eq!(graph.num_nodes(), 1);
530    }
531
532    #[test]
533    fn test_add_decision() {
534        let mut graph = ResolutionGraph::new();
535        let v0 = Var(0);
536
537        let id = graph.add_decision(Lit::pos(v0), 1);
538        assert_eq!(id, 0);
539        assert_eq!(graph.num_nodes(), 1);
540
541        let node = graph
542            .get_node(id)
543            .expect("Decision node must exist in graph");
544        assert!(node.is_decision());
545        assert_eq!(node.decision_level(), 1);
546    }
547
548    #[test]
549    fn test_resolution() {
550        let mut graph = ResolutionGraph::new();
551        let v0 = Var(0);
552        let v1 = Var(1);
553
554        // Clause 1: x0 ∨ x1
555        let clause1 = vec![Lit::pos(v0), Lit::pos(v1)];
556        let id1 = graph.add_clause(clause1, 0);
557
558        // Clause 2: ~x0 ∨ x1
559        let clause2 = vec![Lit::neg(v0), Lit::pos(v1)];
560        let id2 = graph.add_clause(clause2, 0);
561
562        // Resolution on x0 produces: x1
563        let result = vec![Lit::pos(v1)];
564        let id3 = graph.add_resolution(id1, id2, v0, result, 1);
565
566        assert_eq!(graph.num_nodes(), 3);
567
568        let node = graph
569            .get_node(id3)
570            .expect("Resolution node must exist in graph");
571        assert_eq!(node.parents().len(), 2);
572        assert_eq!(node.resolved_var(), Some(v0));
573        assert_eq!(graph.stats().resolutions, 1);
574    }
575
576    #[test]
577    fn test_compute_depth() {
578        let mut graph = ResolutionGraph::new();
579        let v0 = Var(0);
580        let v1 = Var(1);
581
582        // Build a simple resolution chain
583        let id1 = graph.add_clause(vec![Lit::pos(v0)], 0);
584        let id2 = graph.add_clause(vec![Lit::neg(v0), Lit::pos(v1)], 0);
585        let id3 = graph.add_resolution(id1, id2, v0, vec![Lit::pos(v1)], 1);
586
587        assert_eq!(graph.compute_depth(id1), 1); // Leaf
588        assert_eq!(graph.compute_depth(id2), 1); // Leaf
589        assert_eq!(graph.compute_depth(id3), 2); // One level above leaves
590    }
591
592    #[test]
593    fn test_analyze() {
594        let mut graph = ResolutionGraph::new();
595        let v0 = Var(0);
596        let v1 = Var(1);
597
598        let id1 = graph.add_clause(vec![Lit::pos(v0)], 0);
599        let id2 = graph.add_clause(vec![Lit::neg(v0), Lit::pos(v1)], 0);
600        graph.add_resolution(id1, id2, v0, vec![Lit::pos(v1)], 1);
601
602        graph.analyze();
603
604        assert_eq!(graph.stats().resolutions, 1);
605        assert!(graph.stats().max_depth > 0);
606    }
607
608    #[test]
609    fn test_frequent_vars() {
610        let mut graph = ResolutionGraph::new();
611        let v0 = Var(0);
612        let v1 = Var(1);
613        let v2 = Var(2);
614
615        // Multiple resolutions on v0
616        let id1 = graph.add_clause(vec![Lit::pos(v0)], 0);
617        let id2 = graph.add_clause(vec![Lit::neg(v0), Lit::pos(v1)], 0);
618        graph.add_resolution(id1, id2, v0, vec![Lit::pos(v1)], 1);
619
620        let id3 = graph.add_clause(vec![Lit::pos(v0), Lit::pos(v2)], 0);
621        let id4 = graph.add_clause(vec![Lit::neg(v0)], 0);
622        graph.add_resolution(id3, id4, v0, vec![Lit::pos(v2)], 1);
623
624        let freq = graph.get_frequent_vars(10);
625        assert!(!freq.is_empty());
626        assert_eq!(freq[0].0, v0); // v0 should be most frequent
627        assert_eq!(freq[0].1, 2); // Resolved twice
628    }
629
630    #[test]
631    fn test_resolution_analyzer() {
632        let mut analyzer = ResolutionAnalyzer::new();
633        assert!(analyzer.is_enabled());
634
635        let v0 = Var(0);
636        let v1 = Var(1);
637
638        let id1 = analyzer.graph_mut().add_clause(vec![Lit::pos(v0)], 0);
639        let id2 = analyzer
640            .graph_mut()
641            .add_clause(vec![Lit::neg(v0), Lit::pos(v1)], 0);
642        analyzer
643            .graph_mut()
644            .add_resolution(id1, id2, v0, vec![Lit::pos(v1)], 1);
645
646        analyzer.analyze();
647
648        // v0 should have some importance since it was resolved on
649        assert!(analyzer.variable_importance(v0) > 0.0);
650    }
651
652    #[test]
653    fn test_important_vars() {
654        let mut analyzer = ResolutionAnalyzer::new();
655        let v0 = Var(0);
656        let v1 = Var(1);
657        let v2 = Var(2);
658
659        // Create multiple resolutions
660        let id1 = analyzer.graph_mut().add_clause(vec![Lit::pos(v0)], 0);
661        let id2 = analyzer
662            .graph_mut()
663            .add_clause(vec![Lit::neg(v0), Lit::pos(v1)], 0);
664        analyzer
665            .graph_mut()
666            .add_resolution(id1, id2, v0, vec![Lit::pos(v1)], 1);
667
668        let id3 = analyzer
669            .graph_mut()
670            .add_clause(vec![Lit::pos(v0), Lit::pos(v2)], 0);
671        let id4 = analyzer.graph_mut().add_clause(vec![Lit::neg(v0)], 0);
672        analyzer
673            .graph_mut()
674            .add_resolution(id3, id4, v0, vec![Lit::pos(v2)], 1);
675
676        analyzer.analyze();
677
678        let important = analyzer.get_important_vars(2);
679        assert!(!important.is_empty());
680        assert_eq!(important[0].0, v0); // v0 should be most important
681    }
682
683    #[test]
684    fn test_clear() {
685        let mut analyzer = ResolutionAnalyzer::new();
686        let v0 = Var(0);
687
688        analyzer.graph_mut().add_clause(vec![Lit::pos(v0)], 0);
689        assert_eq!(analyzer.graph().num_nodes(), 1);
690
691        analyzer.clear();
692        assert_eq!(analyzer.graph().num_nodes(), 0);
693    }
694
695    #[test]
696    fn test_clause_quality() {
697        let mut graph = ResolutionGraph::new();
698        let v0 = Var(0);
699        let v1 = Var(1);
700
701        let id1 = graph.add_clause(vec![Lit::pos(v0)], 0);
702        let id2 = graph.add_clause(vec![Lit::neg(v0), Lit::pos(v1)], 0);
703        let id3 = graph.add_resolution(id1, id2, v0, vec![Lit::pos(v1)], 1);
704
705        graph.analyze();
706
707        // Leaf clauses should have better quality (lower score) than derived clauses
708        let quality1 = graph.clause_quality(id1);
709        let quality3 = graph.clause_quality(id3);
710
711        assert!(quality1 <= quality3);
712    }
713
714    #[test]
715    fn test_disabled_analyzer() {
716        let mut analyzer = ResolutionAnalyzer::new();
717        analyzer.set_enabled(false);
718        assert!(!analyzer.is_enabled());
719
720        let v0 = Var(0);
721        analyzer.graph_mut().add_clause(vec![Lit::pos(v0)], 0);
722
723        // Analyze should do nothing when disabled
724        analyzer.analyze();
725        assert!(analyzer.var_scores.is_empty());
726    }
727}