def test_tseitin_required_detection(): assert a.to_CNF() == And({Or({a})}) assert And().to_CNF() == And() assert Or().to_CNF() == And({Or()}) assert (a | b).to_CNF() == And({a | b}) assert And({a | b, b | c}).to_CNF() == And({a | b, b | c}) assert And({And({Or({And({~a})})})}).to_CNF() == And({Or({~a})})
def process_required(node: NNF) -> None: """For nodes that have to be satisfied. This lets us perform some optimizations. """ if isinstance(node, Var): clauses.append(Or({node})) return assert isinstance(node, Internal) if len(node.children) == 1: [child] = node.children process_required(child) elif isinstance(node, Or): children = {process_node(c) for c in node.children} if any(~var in children for var in children): return clauses.append(Or(children)) elif isinstance(node, And): for child in node.children: process_required(child) else: raise TypeError(node)
def _parse_cnf(tokens: t.Iterable[str]) -> And[Or[Var]]: clauses = set() # type: t.Set[Or[Var]] clause = set() # type: t.Set[Var] for token in tokens: if token == '0': clauses.add(Or(clause)) clause = set() elif token == '%': # Some example files end with: # 0 # % # 0 # I don't know why. break elif token.startswith('-'): clause.add(Var(_parse_int(token[1:]), False)) else: clause.add(Var(_parse_int(token))) if clause: # A file may or may not end with a 0 # Adding an empty clause is not desirable clauses.add(Or(clause)) sentence = And(clauses) NNF._is_CNF_loose.set(sentence, True) return sentence
def test_dimacs_cnf_serialize(): sample_input = """c Example CNF format file c p cnf 4 3 1 3 -4 0 4 0 2 -3 """ assert dimacs.loads(sample_input) == And( {Or({Var(1), Var(3), ~Var(4)}), Or({Var(4)}), Or({Var(2), ~Var(3)})})
def test_dimacs_sat_serialize(): # http://www.domagoj-babic.com/uploads/ResearchProjects/Spear/dimacs-cnf.pdf sample_input = """c Sample SAT format c p sat 4 (*(+(1 3 -4) +(4) +(2 3))) """ assert dimacs.loads(sample_input) == And( {Or({Var(1), Var(3), ~Var(4)}), Or({Var(4)}), Or({Var(2), Var(3)})})
def load(fp: t.TextIO, var_labels: t.Optional[t.Dict[int, Name]] = None) -> NNF: """Load a sentence from an open file. An optional ``var_labels`` dictionary can map integers to other names. """ def decode_name(num: int) -> Name: if var_labels is not None: return var_labels[num] return num fmt, nodecount, edges, varcount = fp.readline().split() node_specs = dict(enumerate(line.split() for line in fp)) assert fmt == 'nnf' nodes = {} # type: t.Dict[int, NNF] for num, spec in node_specs.items(): if spec[0] == 'L': if spec[1].startswith('-'): nodes[num] = Var(decode_name(int(spec[1][1:])), False) else: nodes[num] = Var(decode_name(int(spec[1]))) elif spec[0] == 'A': nodes[num] = And(nodes[int(n)] for n in spec[2:]) elif spec[0] == 'O': nodes[num] = Or(nodes[int(n)] for n in spec[3:]) else: raise ValueError("Can't parse line {}: {}".format(num, spec)) if int(nodecount) == 0: raise ValueError("The sentence doesn't have any nodes.") return nodes[int(nodecount) - 1]
def _parse_sat(tokens: 't.Deque[str]') -> NNF: cur = tokens.popleft() if cur == '(': content = _parse_sat(tokens) close = tokens.popleft() if close != ')': raise DecodeError( "Expected closing paren, found {!r}".format(close)) return content elif cur == '-': content = _parse_sat(tokens) if not isinstance(content, Var): raise DecodeError( "Only variables can be negated, not {!r}".format(content)) return ~content elif cur == '*(': children = [] while tokens[0] != ')': children.append(_parse_sat(tokens)) tokens.popleft() if children: return And(children) else: return true elif cur == '+(': children = [] while tokens[0] != ')': children.append(_parse_sat(tokens)) tokens.popleft() if children: return Or(children) else: return false else: return Var(_parse_int(cur))
def test_is_DNF_examples(): assert Or().is_DNF() assert Or().is_DNF(strict=True) assert Or({And()}).is_DNF() assert Or({And()}).is_DNF(strict=True) assert Or({And({a, ~b})}).is_DNF() assert Or({And({a, ~b})}).is_DNF(strict=True) assert Or({And({a, ~b}), And({c, ~c})}).is_DNF() assert not Or({And({a, ~b}), And({c, ~c})}).is_DNF(strict=True)
def test_is_CNF_examples(): assert And().is_CNF() assert And().is_CNF(strict=True) assert And({Or()}).is_CNF() assert And({Or()}).is_CNF(strict=True) assert And({Or({a, ~b})}).is_CNF() assert And({Or({a, ~b})}).is_CNF(strict=True) assert And({Or({a, ~b}), Or({c, ~c})}).is_CNF() assert not And({Or({a, ~b}), Or({c, ~c})}).is_CNF(strict=True)
def test_dsharp_compile_converting_names(sentence: And[Or[Var]]): sentence = And( Or(Var(str(var.name), var.true) for var in clause) for clause in sentence) compiled = dsharp.compile(sentence) assert all(isinstance(name, str) for name in compiled.vars()) if sentence.satisfiable(): assert sentence.equivalent(compiled)
def test_implicates_implicants_negation_rule_example(): """These failed an old version of the previous test. See issue #3.""" sentence = Or({And({~Var(1), Var(2)}), And({~Var(3), Var(1)})}) assert (sentence.negate().implicants().negate().children >= sentence.implicates().children) assert (sentence.negate().implicates().negate().children <= sentence.implicants().children)
def process_node(node: NNF) -> Var: if isinstance(node, Var): return node assert isinstance(node, Internal) children = {process_node(c) for c in node.children} if len(children) == 1: [child] = children return child aux = Var.aux() if any(~var in children for var in children): if isinstance(node, And): clauses.append(Or({~aux})) else: clauses.append(Or({aux})) elif isinstance(node, And): clauses.append(Or({~c for c in children} | {aux})) for c in children: clauses.append(Or({~aux, c})) elif isinstance(node, Or): clauses.append(Or(children | {~aux})) for c in children: clauses.append(Or({~c, aux})) else: raise TypeError(node) return aux
def check_correct(self): var = [] ret_string = "" chars = list(self.characters.keys()) sentence_vals = {x: False for x in chars} chars.remove('weapon') chars.remove('location') for char in chars: var.append(Var(char)) weapon = self.characters['weapon'].goals['murderweapon'].points sus_weapon = self.characters['weapon'].goals['suspectedweapon'].points location = self.characters['location'].goals['murderlocation'].points sus_location = self.characters['location'].goals['suspectedlocation'].points for char in chars: if self.characters[char].goals['issuspect'].points == 1: sus_killer = char if self.characters[char].goals['iskiller'].points == 1: killer = char w = Var('weapon') l = Var('location') sentence = And({w, l, Or(tuple(var))}) if killer == sus_killer: sentence_vals[killer] = True ret_string += "You got the killer!\n" else: ret_string += "The killer got away!\n" if weapon == sus_weapon: sentence_vals['weapon'] = True ret_string += "You correctly identified the weapon!\n" else: ret_string += "You didn't identify the murder weapon correctly.\n" if location == sus_location: sentence_vals['location'] = True ret_string += "You deduced the correct location!\n\n" else: ret_string += "The murder took place in another location.\n\n" if sentence.satisfied_by(sentence_vals): ret_string += "Congratulations, you solved the mystery!\n" else: ret_string += "Unfortunately, you didn't solve the mystery completely.\n" return ret_string
def implies_all(self, inputs: dict, left: list, right: list) -> NNF: """All left variables imply all right variables. Arguments --------- inputs: dict left : list[nnf.Var] right: list[nnf.Var] Returns ------- nnf.NNF: And(Or(~left_i, right_j)) """ clauses = [] # constraint created by function if not inputs: if left and right: left_vars = list(map(lambda var: ~var, left)) clauses = list( map(lambda clause: Or(clause), product(left_vars, right))) return And(clauses) assert isinstance(inputs, dict) # constraint from decorator for key, value in inputs.items(): left_vars = left + [key] right_vars = right + value negated_left = list(map(lambda var: ~var, left_vars)) res = list( map(lambda clause: Or(clause), product(negated_left, right_vars))) clauses.extend(res) self.add_to_instance_constraints(tuple(left_vars), res) return And(clauses)
def reduce_(node: NNF) -> NNF: if isinstance(node, Or): best = add_neut candidates = [] # type: t.List[NNF] for child in node.children: value = eval_(child) if value > best: # type: ignore best = value candidates = [child] elif value == best: candidates.append(child) return Or(reduce_(candidate) for candidate in candidates) elif isinstance(node, And): return And(reduce_(child) for child in node.children) else: return node
def none_of(self, inputs: list) -> NNF: """None of the inputs are true. Arguments --------- inputs : list[nnf.Var] Returns ------- theory : nnf.NNF And(~a, ~b) for all a,b in input """ if not inputs: raise ValueError(f"Inputs are empty for {self}") return Or(inputs).negate()
def at_least_one(self, inputs: list) -> NNF: """At least one of the inputs are true. This is equivalent to a disjunction across all variables Arguments --------- inputs : list[nnf.Var] Returns ------- nnf.NNF: Or(inputs) Disjunction across all variables. """ if not inputs: raise ValueError(f"Inputs are empty for {self}") return Or(inputs)
def at_most_k(self, inputs: list, k: int) -> NNF: """ At most k variables can be true. Arguments --------- inputs : list[nnf.Var] k : int Returns ------- nnf.NNF """ if not 1 <= k <= len(inputs): raise ValueError(f"The provided k={k} is greater" " than the number of propositional" f" variables (i.e. {len(inputs)} variables)" f" for {self}.") elif k == 1: return _ConstraintBuilder.at_most_one(inputs) if k >= len(inputs): warnings.warn(f"The provided k={k} for building the at most K" " constraint is greater than or equal to" f" the number of variables, which is {len(inputs)}." f" We're setting k = {len(inputs) - 1} as a result.") k = len(inputs) - 1 clauses = set() # avoid adding duplicate clauses inputs = list(map(lambda var: ~var, inputs)) # combinations from choosing k from n inputs for 1 <= k <n chosen = list(combinations(inputs, k)) for combo in chosen: combo = list(combo) excludes_combo = [x for x in inputs if x not in combo] for x in excludes_combo: clause = Or(combo + [x]) clauses.add(clause) self.add_to_instance_constraints(tuple(combo), clause) return And(clauses)
def at_most_one(self, inputs: list) -> NNF: """At most one of the inputs are true. Arguments --------- inputs : list[nnf.Var] Returns ------- theory : nnf.NNF And(Or(~a, ~b)) for all a,b in input """ if not inputs: raise ValueError(f"Inputs are empty for {self}") clauses = [] for var in inputs: # negate variables that aren't the current var excludes_var = [~x for x in inputs if x != var] clause = list(map(lambda c: Or(c), product([~var], excludes_var))) clauses.extend(clause) self.add_to_instance_constraints(str(var), clause) return And(clauses)
sample_input = """c Sample SAT format c p sat 4 (*(+(1 3 -4) +(4) +(2 3))) """ assert dimacs.loads(sample_input) == And( {Or({Var(1), Var(3), ~Var(4)}), Or({Var(4)}), Or({Var(2), Var(3)})}) @pytest.mark.parametrize( 'serialized, sentence', [('p sat 2\n(+((1)+((2))))', Or({Var(1), Or({Var(2)})}))]) def test_dimacs_sat_weird_input(serialized: str, sentence: nnf.NNF): assert dimacs.loads(serialized) == sentence def test_dimacs_cnf_serialize(): sample_input = """c Example CNF format file c p cnf 4 3 1 3 -4 0 4 0 2 -3 """ assert dimacs.loads(sample_input) == And( {Or({Var(1), Var(3), ~Var(4)}), Or({Var(4)}),
def encode_circuit_search(original_theory, num_gates, models=[]): ######## # Vars # ######## # Inputs to the circuit are the variables of the input theory inputs = [Var(v) for v in original_theory.vars()] if models: input_clones = clone_varset(models, inputs, inputs) else: input_clones = {} # Gates are either & | or ~ gates = [] gate_modalities = {} for i in range(num_gates): gates.append(Var(Gate(i))) gate_modalities[gates[-1]] = {} for m in ['and', 'or', 'not']: gate_modalities[gates[-1]][m] = Var(GateType(gates[-1], m)) if models: gate_clones = clone_varset(models, inputs, gates) else: gate_clones = {} # Single (arbitrary) gate is the circuit output output = gates[0] # Connections between inputs/gates to gates, and transitivity connections = {} unconnections = {} for src in inputs + gates[1:]: connections[src] = {} for dst in gates: if src != dst: connections[src][dst] = Var(Connection(src, dst)) if dst not in unconnections: unconnections[dst] = {} unconnections[dst][src] = connections[src][dst] C = connections # convenience orders = {} for src in gates: orders[src] = {} for dst in gates: orders[src][dst] = Var(Order(src,dst)) ############### # Constraints # ############### ''' add decorators for simplifying adding constraints (i.e. a conjunct)''' conjuncts = [] # Orderings (to forbid cycles in the circuit) for g1 in gates: # Connection implies orders for src in connections: for dst in connections[src]: if isinstance(src.name, Gate) and isinstance(dst.name, Gate): conjuncts.append(~connections[src][dst] | orders[src][dst]) # Can't order before yourself conjuncts.append(~orders[g1][g1]) # Transitive closure for g2 in gates: for g3 in gates: conjuncts.append(~orders[g1][g2] | ~orders[g2][g3] | orders[g1][g3]) if FORCE_TREE: # At max one outgoing connection on a gate for src in gates[1:]: for dst1 in connections[src]: for dst2 in connections[src]: if dst1 != dst2: conjuncts.append(~connections[src][dst1] | ~connections[src][dst2]) # Every gate has at least one input for dst in unconnections: conjuncts.append(Or(unconnections[dst].values())) # Every gate has at most two inputs and negation gates have at most one for j in gates: for i1 in inputs+gates[1:]: for i2 in inputs+gates[1:]: if not unique([j,i1,i2]): continue conjuncts.append(~gate_modalities[j]['not'] | ~C[i1][j] | ~C[i2][j]) for i3 in inputs+gates[1:]: if not unique([j,i1,i2,i3]): continue conjuncts.append(~C[i1][j] | ~C[i2][j] | ~C[i3][j]) # Every gate has exactly one modality for g in gates: # At least one conjuncts.append(Or(gate_modalities[g].values())) # At most one for m1 in ['and', 'or', 'not']: remaining = set(['and', 'or', 'not']) - set([m1]) conjuncts.append(Or([~gate_modalities[g][m2] for m2 in remaining])) # Re-usable theories notneg_cache = {} def notneg(src, dst, cloned_src=None): if not cloned_src: cloned_src = src if (cloned_src,dst) not in notneg_cache: notneg_cache[(cloned_src,dst)] = ~C[src][dst] | cloned_src return notneg_cache[(cloned_src,dst)] notpos_cache = {} def notpos(src, dst, cloned_src=None): if not cloned_src: cloned_src = src if (cloned_src,dst) not in notpos_cache: notpos_cache[(cloned_src,dst)] = ~C[src][dst] | ~cloned_src return notpos_cache[(cloned_src,dst)] # Implement the gates for g in gates: ins = [i for i in inputs+gates[1:] if i != g] conjuncts.append(~gate_modalities[g]['and'] | iff(g, And([notneg(src,g) for src in ins]))) t = Or([notpos(src,g).negate() for src in ins]) conjuncts.append(~gate_modalities[g]['or'] | iff(g, t)) conjuncts.append(~gate_modalities[g]['not'] | iff(g, t.negate())) for m in models: bitvec = model_to_bitvec(m, inputs) orig_mapping = {**{input_clones[bitvec][i]: i for i in inputs if i != g}, **{gate_clones[bitvec][i]: i for i in gates[1:] if i != g}} ins = orig_mapping.keys() conjuncts.append(~gate_modalities[g]['and'] | iff(gate_clones[bitvec][g], And([notneg(orig_mapping[src],g,src) for src in ins]))) t = Or([notpos(orig_mapping[src],g,src).negate() for src in ins]) conjuncts.append(~gate_modalities[g]['or'] | iff(gate_clones[bitvec][g], t)) conjuncts.append(~gate_modalities[g]['not'] | iff(gate_clones[bitvec][g], t.negate())) # Finally, lock in the models if models: for m in models: bitvec = model_to_bitvec(m, inputs) for var in m: if m[var]: conjuncts.append(input_clones[bitvec][Var(var)]) else: conjuncts.append(~input_clones[bitvec][Var(var)]) if original_theory.satisfied_by(m): conjuncts.append(gate_clones[bitvec][output]) else: conjuncts.append(~gate_clones[bitvec][output]) else: for model in all_models(original_theory.vars()): t = false # negating the conjunction because of the implication: flips the signs for var,val in model.items(): if val: t |= ~Var(var) else: t |= Var(var) if original_theory.satisfied_by(model): t |= output else: t |= ~output conjuncts.append(t) versions = {} example = {} for c in conjuncts: cn = c.simplify().to_CNF() stats = "(%d / %d / %d) > (%d / %d / %d)" % (c.simplify().size(), c.simplify().height(), len(c.simplify().vars()), cn.size(), cn.height(), len(cn.vars())) example[stats] = str(c.simplify()) versions[stats] = versions.get(stats, 0) + 1 print("Conjunct stats:") for k in versions: print("\n - (%d) %s: %s" % (versions[k], k, example[k])) T = And(conjuncts) return T.simplify(), inputs, gates, output, connections, unconnections, gate_modalities
def DNF(draw): return Or(draw(st.frozensets(terms())))
def clauses(draw): return Or(Var(name, draw(st.booleans())) for name in draw(st.sets(names)))
def C(S): if S == candidates: return true return Or(C_child(i, S) for i in candidates - S)
def MODS(draw): num = draw(st.integers(min_value=1, max_value=9)) amount = draw(st.integers(min_value=0, max_value=10)) return Or( And(Var(name, draw(st.booleans())) for name in range(1, num)) for _ in range(amount))
def test_hyp(sentence: nnf.Or): assume(len(sentence.children) != 0) assume(sentence.decomposable()) assert sentence.satisfiable() assert sentence.vars() <= set(range(1, 9))
def test_MODS_satisfiable(sentence: nnf.Or): if len(sentence.children) > 0: assert sentence.satisfiable() else: assert not sentence.satisfiable()
def test_MODS(sentence: nnf.Or): assert sentence.smooth() assert sentence.flat() assert sentence.decomposable() assert sentence.simply_conjunct()
def distribute(node: NNF): if isinstance(node, Var): return node elif isinstance(node, And): # And(Var(A), Var(B)) -> And(Var(A), Var(B)) if all([isinstance(child, Var) for child in node.children]): return node else: left, right = node.children return And({distribute(left), distribute(right)}) elif isinstance(node, Or): # Or(Var(A), Var(B)) -> Or(Var(A), Var(B)) if all([isinstance(child, Var) for child in node.children]): return node else: left, right = node.children # Or(And(A,B), And(C, D)) -> And(Or(A,C), Or(A,D), Or(B,C), Or(B,D)) if all([isinstance(child, And) for child in node.children]): res = [] for c in left.children: for d in right.children: res.append(Or({c, d})) return And(res) # Either Or(And(A,B), C) or Or(C, And(A,B)) # where C can be Var or Or elif any([isinstance(child, And) for child in node.children]): if isinstance(left, And): clause1, clause2 = left.children distributed_left = Or( {distribute(clause1), distribute(right)}) distributed_right = Or( {distribute(clause2), distribute(right)}) return And({ distribute(distributed_left), distribute(distributed_right) }) else: clause1, clause2 = right.children distributed_left = Or( {distribute(clause1), distribute(left)}) distributed_right = Or( {distribute(clause2), distribute(left)}) return And({ distribute(distributed_left), distribute(distributed_right) }) # Or(Or(A, B), Or(D, C)) else: left, right = node.children return Or({distribute(left), distribute(right)})
def C(S): if len(S) < k: return Or(C_child(i, S) for i in candidates - S) return And(~Var((i, j)) for i in candidates - S for j in candidates - S if i != j)