def recursion_tseitins_transformation(subformula: SatFormula): if subformula is None or subformula.is_leaf: return [] if subformula.left.is_leaf: l_var = subformula.left.value else: if subformula.left.idx not in new_variables: new_variables[subformula.left.idx] = Literal.from_name( "tse{}".format(subformula.left.idx), negated=False) l_var = new_variables[subformula.left.idx] if subformula.operator is Operator.NEGATION or subformula.right is None: r_var = None else: if subformula.right.is_leaf: r_var = subformula.right.value else: if subformula.right.idx not in new_variables: new_variables[subformula.right.idx] = Literal.from_name( "tse{}".format(subformula.right.idx), negated=False) r_var = new_variables[subformula.right.idx] if subformula.idx not in new_variables: new_variables[subformula.idx] = Literal.from_name("tse{}".format( subformula.idx), negated=False) return convert_iff_cnf(new_variables[subformula.idx], rhs1=l_var, rhs2=r_var, operation=subformula.operator) + \ recursion_tseitins_transformation(subformula.right) + recursion_tseitins_transformation(subformula.left)
def get_complicated_graph() -> (ImplicationGraph, int, List[Variable]): variables = [Variable('TEMP') ] + [Variable('x{}'.format(i + 1)) for i in range(10)] pos_l = [Literal(v, negated=False) for v in variables] neg_l = [Literal(v, negated=True) for v in variables] c0 = [pos_l[10] ] # unused clause to keep the numbering aligned to the slides c1 = [pos_l[2], pos_l[3]] c2 = [pos_l[9] ] # unused clause to keep the numbering aligned to the slides c3 = [neg_l[3], neg_l[4]] c4 = [neg_l[4], neg_l[2], neg_l[1]] c5 = [neg_l[6], neg_l[5], pos_l[4]] c6 = [pos_l[7], pos_l[5]] c7 = [neg_l[8], pos_l[7], pos_l[6]] conflict = [c0, c1, c2, c3, c4, c5, c6, c7] clauses = conflict cnf = CnfFormula(clauses) cnf = preprocess(cnf) g = ImplicationGraph(cnf) g.add_decide_node(1, pos_l[1]) g.add_decide_node(2, pos_l[8]) g.add_decide_node(3, pos_l[7]) g.add_node(3, pos_l[6], None, 7) g.add_node(3, pos_l[5], None, 6) g.add_node(3, pos_l[4], None, 5) g.add_node(3, neg_l[3], None, 3) g.add_node(3, neg_l[2], None, 4) return g, 1, variables
def test_find_all_paths(): vars = [Variable('x{}'.format(i + 1)) for i in range(8)] pos_l = [None] + [Literal(v, negated=False) for v in vars] neg_l = [None] + [Literal(v, negated=True) for v in vars] clauses = [[pos_l[1]], [neg_l[1], pos_l[5]], [neg_l[1], pos_l[3]], [neg_l[2], neg_l[3], pos_l[4]], [neg_l[5], pos_l[2]]] cnf = CnfFormula(clauses) cnf = preprocess(cnf) g = ImplicationGraph(cnf) g.add_decide_node(1, pos_l[1]) g.add_node(1, pos_l[3], None, 2) g.add_node(1, pos_l[5], None, 1) g.add_node(1, pos_l[2], None, 4) # g.add_node(1, pos_l[2], None, 3) g.add_node(1, pos_l[4], None, 3) source_node = g._nodes[pos_l[1]] target_node = g._nodes[pos_l[4]] actual_paths = g._find_all_paths(source_node, target_node) expected_paths = [[ pos_l[1].variable, pos_l[5].variable, pos_l[2].variable, pos_l[4].variable ], [pos_l[1].variable, pos_l[3].variable, pos_l[4].variable]] assert {(frozenset(item)) for item in actual_paths} == {(frozenset(item)) for item in expected_paths}
def test_learn_conflict_complicated(): g, conflict_idx, variables = get_complicated_graph() conflict_clause = g.learn_conflict(Literal(variables[3], True), conflict_idx) expected_conflict_clause = [ Literal(variables[1], True), Literal(variables[4], True) ] assert sorted(conflict_clause) == sorted(expected_conflict_clause)
def test_boolean_resolution(): vars = [Variable('x{}'.format(i + 1)) for i in range(10)] pos_l = [None] + [Literal(v, negated=False) for v in vars] neg_l = [None] + [Literal(v, negated=True) for v in vars] c0 = [pos_l[2], pos_l[3]] c1 = [neg_l[4], neg_l[3]] c_tag = ImplicationGraph.boolean_resolution(c0, c1, pos_l[3].variable) expected_c_tag = [pos_l[2], neg_l[4]] assert sorted(c_tag) == sorted(expected_c_tag) c0 = [pos_l[2], neg_l[3], neg_l[5]] c1 = [neg_l[4], pos_l[3], pos_l[1]] c_tag = ImplicationGraph.boolean_resolution(c0, c1, pos_l[3].variable) expected_c_tag = [pos_l[2], neg_l[4], neg_l[5], pos_l[1]] assert sorted(c_tag) == sorted(expected_c_tag)
def test_backjump_simple(): g, conflict_idx, variables = get_simple_graph() conflict_clause = g.learn_conflict(Literal(variables[3], False), conflict_idx) level = g.get_backjump_level(conflict_clause) expected_level = 0 assert level == expected_level
def test_backjump_complicated(): g, conflict_idx, variables = get_complicated_graph() conflict_clause = g.learn_conflict(Literal(variables[3], True), conflict_idx) level = g.get_backjump_level(conflict_clause) expected_level = 1 assert level == expected_level
def create_random_query(num_variables=5, num_clauses=4, clause_length=3): assert clause_length <= num_variables variables = [Variable('x{}'.format(i + 1)) for i in range(num_variables)] pos_l = [Literal(v, negated=False) for v in variables] neg_l = [Literal(v, negated=True) for v in variables] clauses = [] for _ in range(num_clauses): clause = [] vars = sample(range(0, num_variables), clause_length) for i in range(clause_length): if random() > 0.5: clause.append(neg_l[vars[i]]) else: clause.append(pos_l[vars[i]]) clauses.append(clause) return clauses
def test_search_complex_unsat(): variables = [Variable('TEMP') ] + [Variable('x{}'.format(i + 1)) for i in range(10)] pos_l = [Literal(v, negated=False) for v in variables] neg_l = [Literal(v, negated=True) for v in variables] # x1 = T, x2 = F, x3 = # x3 != x2 /\ x1 != x2 /\ (!x1 \/ x2 \/ !x3) /\ (x1 \/ !x2 \/ x3) clauses = [[pos_l[1], pos_l[2]], [neg_l[1], neg_l[2]], [pos_l[3], pos_l[2]], [neg_l[3], neg_l[2]], [neg_l[1], pos_l[2], neg_l[3]], [pos_l[3], neg_l[2], pos_l[1]]] cnf = CnfFormula(clauses) cnf = preprocess(cnf) dpll = DPLL(cnf) search_result = dpll.search() # print(dpll.get_assignment()) assert not search_result
def test_search_simple_unsat(): # (x1|~x2) | (~x1 | x2) x1_var = Variable('x1') x2_var = Variable('x2') # x3_var = Variable('x3') x1 = Literal(x1_var, negated=False) not_x1 = Literal(x1_var, negated=True) x2 = Literal(x2_var, negated=False) not_x2 = Literal(x2_var, negated=True) clauses = [[x1], [x2], [not_x1, not_x2]] # literal_to_clauses = {x1: {0}, x2: {0}, x3: {0}} #not_x1: {2}, not_x2: {0} cnf = CnfFormula(clauses) cnf = preprocess(cnf) dpll = DPLL(cnf) search_result = dpll.search() assert not search_result
def get_simple_graph() -> (ImplicationGraph, int, List[Variable]): variables = [Variable('TEMP') ] + [Variable('x{}'.format(i + 1)) for i in range(8)] pos_l = [Literal(v, negated=False) for v in variables] neg_l = [Literal(v, negated=True) for v in variables] # x1 =True --> x3=True, x5=True --> x2 = True, clauses = [[pos_l[1]], [neg_l[1], pos_l[5]], [neg_l[1], pos_l[3]], [neg_l[2], neg_l[3]], [neg_l[5], pos_l[2]]] cnf = CnfFormula(clauses) cnf = preprocess(cnf) g = ImplicationGraph(cnf) g.add_decide_node(1, pos_l[1]) g.add_node(1, pos_l[3], None, 2) g.add_node(1, pos_l[5], None, 1) g.add_node(1, neg_l[2], None, 4) # g.add_node(1, pos_l[2], None, 3) # g.add_node(1, pos_l[4], None, 3) return g, 3, variables
def __init__(self, cnf_formula: CnfFormula): self._nodes = {} # type: Dict[Variable, Node] self._edges = {} # type: Dict[Node, List[Edge]] self._nodes_order = {} # type: Dict[Variable, int] self._level_to_nodes = defaultdict( set) # type: Dict[int, Set[Variable]] self._incoming_edges = defaultdict( list) # type: Dict[Node, List[Edge]] self._conflict_var = Literal(Variable('Conflict'), False) self._conflict_node = Node(self._conflict_var, -1) self._last_decide_node = None self._formula = cnf_formula
def test_up_unsat(): # (x1|~x2|x3)&x2&(~x1|x3)&(x2|~x3) --> UNSAT x1_var = Variable('x1') x2_var = Variable('x2') x3_var = Variable('x3') x1 = Literal(x1_var, negated=False) not_x1 = Literal(x1_var, negated=True) x2 = Literal(x2_var, negated=False) not_x2 = Literal(x2_var, negated=True) x3 = Literal(x3_var, negated=False) not_x3 = Literal(x3_var, negated=True) clauses = [[x1, not_x2, x3], [x2], [not_x1, x3], [not_x2, not_x3]] # literal_to_clauses = {x1: {0}, not_x1: {2}, not_x2: {0, 3}, x2: {1}, x3: {0, 2}, not_x3: {3}} cnf = CnfFormula(clauses) cnf = preprocess(cnf) dpll = DPLL(cnf) actual_cnf = dpll.unit_propagation() assert actual_cnf is None assert not dpll.get_full_assignment()[x3_var] assert dpll.get_full_assignment()[x2_var]
def test_multi_level_conflict_sat(): vars = [Variable('x{}'.format(i + 1)) for i in range(8)] pos_l = [None] + [Literal(v, negated=False) for v in vars] neg_l = [None] + [Literal(v, negated=True) for v in vars] c1 = [pos_l[2], pos_l[3]] # c2 = [pos_l[1], pos_l[4], neg_l[8]] c3 = [neg_l[3], neg_l[4]] c4 = [neg_l[4], neg_l[2], neg_l[1]] c5 = [neg_l[6], neg_l[5], pos_l[4]] c6 = [pos_l[7], pos_l[5]] c7 = [neg_l[8], pos_l[7], pos_l[6]] conflict = [c3, c4, c5, c6, c7, c1] # c2, # If we implement pure_literal will need to change this # this is just to make sure the order decisions will be: x1=True, x2=True, x3=True, the conflict is because x1 n_temps = 4 temp_literals = [ Literal(Variable('x1_temp{}'.format(idx)), negated=False) for idx in range(n_temps) ] x1_clauses = [[pos_l[1], l] for l in temp_literals] temp_literals = [ Literal(Variable('x8_temp{}'.format(idx)), negated=False) for idx in range(n_temps) ] x8_clauses = [[pos_l[8], l] for l in temp_literals[:-1]] temp_literals = [ Literal(Variable('x7_temp{}'.format(idx)), negated=False) for idx in range(n_temps) ] x7_clauses = [[neg_l[7], l] for l in temp_literals[:-2]] clauses = x1_clauses + x8_clauses + x7_clauses + conflict cnf = CnfFormula(clauses) cnf = preprocess(cnf) dpll = DPLL(cnf) search_result = dpll.search() assert search_result
def test_up_simple(): # (x1|~x2|x3)&x2&(~x1|x3) --> (x1|x3) & (~x1|x3) x1_var = Variable('x1') x2_var = Variable('x2') x3_var = Variable('x3') x1 = Literal(x1_var, negated=False) not_x1 = Literal(x1_var, negated=True) x2 = Literal(x2_var, negated=False) not_x2 = Literal(x2_var, negated=True) x3 = Literal(x3_var, negated=False) clauses = [[x1, not_x2, x3], [x2], [not_x1, x3]] cnf = CnfFormula(clauses) cnf = preprocess(cnf) dpll = DPLL(cnf) actual_cnf = dpll.unit_propagation() expected_cnf = [[x1, not_x2, x3], [not_x1, x3]] assert dpll.get_full_assignment()[x2_var] actual_cnf_real = [cl for cl in actual_cnf.clauses if cl != []] assert actual_cnf_real == expected_cnf, dpll.get_full_assignment()
def conflict(self, partial_assignment: Dict[Variable, bool]) -> List[Literal]: ''' Check if there is a conflict, is so return the reason clause (as a list of literals) which is a negation of the current assignment otherwise, empty list :param partial_assignment: :return: ''' conflict = [] # No conflicts if self.satisfied(partial_assignment): return conflict for v, value in partial_assignment.items(): if v not in self.var_equation_mapping: continue equation = self.equations[self.var_equation_mapping[v]] negated_value = value literal = Literal(equation.fake_variable, negated_value) conflict.append(literal) return conflict
def test_search_simple(): # (x1|x2|~x3) | (x3 | ~x2) x1_var = Variable('x1') x2_var = Variable('x2') x3_var = Variable('x3') x1 = Literal(x1_var, negated=False) not_x1 = Literal(x1_var, negated=True) x2 = Literal(x2_var, negated=False) not_x2 = Literal(x2_var, negated=True) x3 = Literal(x3_var, negated=False) not_x3 = Literal(x3_var, negated=True) clauses = [[x1, x2, not_x3], [x3, not_x2]] cnf = CnfFormula(clauses) cnf = preprocess(cnf) dpll = DPLL(cnf) search_result = dpll.search() assert search_result
def test_multi_level_deduction_sat(): x1_var = Variable('x1') x2_var = Variable('x2') x3_var = Variable('x3') x1 = Literal(x1_var, negated=False) not_x1 = Literal(x1_var, negated=True) x2 = Literal(x2_var, negated=False) not_x2 = Literal(x2_var, negated=True) x3 = Literal(x3_var, negated=False) not_x3 = Literal(x3_var, negated=True) clauses = [[x1], [x3, x2], [not_x2, not_x3, not_x1]] cnf = CnfFormula(clauses) cnf = preprocess(cnf) dpll = DPLL(cnf) search_result = dpll.search() assert search_result
def _negate_assignment(assignment: Dict[Variable, bool]) -> List[Literal]: res = [] for k, v in assignment.items(): res.append(Literal(k, negated=not v)) return res
def search(self) -> bool: ''' :return: True if Sat else False ''' self.rec_num += 1 if self.formula is None: # Formula is unsat return False real_formula = [f for f in self.formula.clauses if f != []] if not real_formula: if not self.check_theory_conflict(): # The current assignment satisfies all clauses but theory finds a conflict return False # Found valid assignment self.unsat = False return True # it and try again, the second literal might not be very helpful self.check_theory_conflict() if self.propagate_helper: smt_res = self.propagate_helper(self._assignment[-1]) # smt_res will be None if partial_assignment is empty if smt_res: self._assignment[-1].update(smt_res) for variable, val in smt_res.items(): if val: lit = Literal(variable, False) else: lit = Literal(variable, True) if not self.remove_clauses_by_assignment(lit): self.formula.clauses = [[]] return self.formula if not smt_res: # Conflict return False next_decision_lit = self.decision_heuristic() if next_decision_lit is None: return False assert isinstance(next_decision_lit, Literal) next_decision = next_decision_lit.variable assert isinstance(next_decision, Variable) assignment_order = [False, True ] if next_decision_lit.negated else [True, False] for d in assignment_order: if self.backjump and self.backjump > len(self._assignment) - 1: # Skip this level self.implication_graph.remove_level(len(self._assignment) - 1) return False elif self.backjump == len(self._assignment) - 1: # Finished to do backjump self.backjump = None # current_assignment = copy(self._assignment) cur_assignment = self._assignment cur_assignment.append(copy(self._assignment[-1])) dpll = DPLL(deepcopy(self.formula), watch_literals=self.watch_literals, partial_assignment=cur_assignment, implication_graph=self.implication_graph, is_first_run=False, conflict_helper=self.conflict_helper, propagate_helper=self.propagate_helper, rec_num=self.rec_num) dpll.assign_true_to_literal(Literal(next_decision, not d), reason=None) sat = dpll.search() if dpll.learned_sat_conflicts: self.formula.add_clause(dpll.learned_sat_conflicts) self.create_watch_literals(len(self.formula.clauses) - 1) self.backjump = dpll.backjump if sat: if not self.check_theory_conflict(): # The current assignment satisfies all clauses but theory finds a conflict return False self.unsat = False return sat # clear the last decision decision_level = len(self._assignment) - 1 self.implication_graph.remove_level(decision_level) self._assignment.pop() return False
import pytest from sat_solver.cnf_formula import CnfFormula from sat_solver.preprocessor import remove_redundant_literals, delete_trivial_clauses from sat_solver.sat_formula import Literal, Variable v1 = Variable(name='v1') v2 = Variable(name='v2') l1 = Literal(v1, negated=False) l2 = Literal(v1, negated=True) l3 = Literal(v2, negated=False) l4 = Literal(v2, negated=True) def test_literal_to_clauses(): formula = CnfFormula([[l1, l2], [l1, l1, l3]]) expected_literal_to_clause = {l1: {0, 1}, l2: {0}, l3: {1}} actual_formula = remove_redundant_literals(formula) assert str(expected_literal_to_clause) == str( dict(actual_formula.literal_to_clauses)) def test_remove_redundant_literals(): formula = CnfFormula([[l1, l2], [l1, l1, l3]]) expected_formula = CnfFormula([[l1, l2], [l1, l3]]) actual_formula = remove_redundant_literals(formula)
def test_search_complex(): # (~x|z) & (~x|~z|~y) & (~z|w) & (~w|~y) (lec3, slide 18) # (~x1 | x2) & (~x1 | ~x2 | ~x3) & (~x2 | x4) & (~x4| ~x3) x1_var = Variable('x1') x2_var = Variable('x2') x3_var = Variable('x3') x4_var = Variable('x4') x5_var = Variable('x5') x6_var = Variable('x6') x1 = Literal(x1_var, negated=False) not_x1 = Literal(x1_var, negated=True) x2 = Literal(x2_var, negated=False) not_x2 = Literal(x2_var, negated=True) x3 = Literal(x3_var, negated=False) not_x3 = Literal(x3_var, negated=True) x4 = Literal(x4_var, negated=False) not_x4 = Literal(x4_var, negated=True) x5 = Literal(x5_var, negated=False) not_x5 = Literal(x5_var, negated=True) x6 = Literal(x6_var, negated=False) not_x6 = Literal(x6_var, negated=True) # (~x1 | x2) & (~x1 | ~x2 | ~x3) & (~x2 | x4) & (~x4| ~x3) clauses = [[not_x1, x2], [not_x1, not_x2, not_x3], [not_x2, x4], [not_x4, not_x3], [x5, x6], [not_x5, not_x6]] cnf = CnfFormula(clauses) cnf = preprocess(cnf) dpll = DPLL(cnf) search_result = dpll.search() assert search_result
def add_literal(self, negated: bool): self.fake_literals[negated] = Literal(self.fake_variable, negated)