Skip to content

Commit 9d34688

Browse files
authored
Merge pull request #6 from QuMuLab/tseitin
Tseitin Encoding
2 parents e296aa4 + 4f69428 commit 9d34688

File tree

5 files changed

+173
-13
lines changed

5 files changed

+173
-13
lines changed

Diff for: CONTRIBUTING.md

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
# Contributing to python-nnf
2+
3+
More information coming soon.
4+
5+
## Notes for now
6+
7+
- `tox -e py3`
8+
- Set notation for Internal nodes (rather than lists)
9+
- Use of `@memoize`
10+
- Use of Typing / mypy

Diff for: nnf/__init__.py

+51-5
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,9 @@
1818
import functools
1919
import itertools
2020
import operator
21+
import os
2122
import typing as t
23+
import uuid
2224

2325
from collections import Counter
2426

@@ -28,8 +30,9 @@
2830
Name = t.Hashable
2931
Model = t.Dict[Name, bool]
3032

31-
__all__ = ('NNF', 'Internal', 'And', 'Or', 'Var', 'Builder', 'all_models',
32-
'decision', 'true', 'false', 'dsharp', 'dimacs', 'amc', 'operators')
33+
__all__ = ('NNF', 'Internal', 'And', 'Or', 'Var', 'Aux', 'Builder',
34+
'all_models', 'complete_models', 'decision', 'true', 'false',
35+
'dsharp', 'dimacs', 'amc', 'tseitin', 'operators')
3336

3437

3538
def all_models(names: 't.Iterable[Name]') -> t.Iterator[Model]:
@@ -587,6 +590,11 @@ def neg(node: NNF) -> NNF:
587590

588591
return neg(self)
589592

593+
def to_CNF(self) -> 'And[Or[Var]]':
594+
"""Compile theory to a semantically equivalent CNF formula."""
595+
from nnf import tseitin
596+
return tseitin.to_CNF(self)
597+
590598
def _cnf_satisfiable(self) -> bool:
591599
"""A naive DPLL SAT solver."""
592600
def DPLL(clauses: t.FrozenSet[t.FrozenSet[Var]]) -> bool:
@@ -1067,7 +1075,13 @@ def name(node: NNF) -> int:
10671075
if node not in names:
10681076
number = next(counter)
10691077
if isinstance(node, Var):
1070-
label = str(node.name).replace('"', r'\"')
1078+
if isinstance(node.name, Aux):
1079+
# This matches the repr, but in this context it could
1080+
# be reasonable to number them instead
1081+
label = "<{}>".format(node.name.hex[:4])
1082+
else:
1083+
label = str(node.name)
1084+
label = label.replace('"', r'\"')
10711085
color = colors['var']
10721086
if not node.true:
10731087
label = '¬' + label
@@ -1265,6 +1279,15 @@ def __deepcopy__(self: T_NNF, memodict: t.Dict[t.Any, t.Any]) -> T_NNF:
12651279
return self
12661280

12671281

1282+
class Aux(uuid.UUID):
1283+
"""Unique UUID labels for auxiliary variables.
1284+
1285+
Don't instantiate directly, call :meth:`Var.aux` instead.
1286+
"""
1287+
1288+
__slots__ = ()
1289+
1290+
12681291
class Var(NNF):
12691292
"""A variable, or its negation.
12701293
@@ -1320,10 +1343,12 @@ def __delattr__(self, name: str) -> None:
13201343

13211344
def __repr__(self) -> str:
13221345
if isinstance(self.name, str):
1323-
return str(self.name) if self.true else "~{}".format(self.name)
1346+
base = str(self.name)
1347+
elif isinstance(self.name, Aux):
1348+
base = "<{}>".format(self.name.hex[:4])
13241349
else:
13251350
base = "{}({!r})".format(self.__class__.__name__, self.name)
1326-
return base if self.true else '~' + base
1351+
return base if self.true else '~' + base
13271352

13281353
def __invert__(self) -> 'Var':
13291354
return Var(self.name, not self.true)
@@ -1350,6 +1375,12 @@ def __setstate__(self, state: t.Tuple[Name, bool]) -> None:
13501375
object.__setattr__(self, 'name', state[0])
13511376
object.__setattr__(self, 'true', state[1])
13521377

1378+
@staticmethod
1379+
def aux() -> 'Var':
1380+
"""Create an auxiliary variable with a unique label."""
1381+
# See implementation of uuid.uuid4()
1382+
return Var(Aux(bytes=os.urandom(16), version=4))
1383+
13531384

13541385
class Internal(NNF, t.Generic[T_NNF_co]):
13551386
"""Base class for internal nodes, i.e. And and Or nodes."""
@@ -1539,6 +1570,21 @@ def map(self, func: t.Callable[[T_NNF_co], U_NNF]) -> 'Or[U_NNF]':
15391570
...
15401571

15411572

1573+
def complete_models(
1574+
models: t.Iterable[Model],
1575+
names: t.Iterable[Name]
1576+
) -> t.Iterator[Model]:
1577+
names = frozenset(names)
1578+
diff = None
1579+
for model in models:
1580+
if diff is None:
1581+
diff = names - model.keys()
1582+
for supplement in all_models(diff):
1583+
new = model.copy()
1584+
new.update(supplement)
1585+
yield new
1586+
1587+
15421588
def decision(
15431589
var: Var,
15441590
if_true: T_NNF,

Diff for: nnf/tseitin.py

+54
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
"""Transformations using the well-known Tseitin encoding
2+
<https://en.wikipedia.org/wiki/Tseytin_transformation>
3+
4+
The Tseitin transformation converts any arbitrary circuit to one in CNF in
5+
polynomial time/space. It does so at the cost of introducing new variables
6+
(one for each logical connective in the formula).
7+
"""
8+
9+
from nnf import NNF, Var, And, Or, memoize, Internal
10+
11+
12+
def to_CNF(theory: NNF) -> And[Or[Var]]:
13+
"""Convert an NNF into CNF using the Tseitin Encoding.
14+
15+
:param theory: Theory to convert.
16+
"""
17+
18+
clauses = []
19+
20+
@memoize
21+
def process_node(node: NNF) -> Var:
22+
23+
if isinstance(node, Var):
24+
return node
25+
26+
assert isinstance(node, Internal)
27+
28+
aux = Var.aux()
29+
children = {process_node(c) for c in node.children}
30+
31+
if any(~var in children for var in children):
32+
if isinstance(node, And):
33+
clauses.append(Or({~aux}))
34+
else:
35+
clauses.append(Or({aux}))
36+
37+
elif isinstance(node, And):
38+
clauses.append(Or({~c for c in children} | {aux}))
39+
for c in children:
40+
clauses.append(Or({~aux, c}))
41+
42+
elif isinstance(node, Or):
43+
clauses.append(Or(children | {~aux}))
44+
for c in children:
45+
clauses.append(Or({~c, aux}))
46+
47+
else:
48+
raise TypeError(node)
49+
50+
return aux
51+
52+
root = process_node(theory)
53+
clauses.append(Or({root}))
54+
return And(clauses)

Diff for: test_nnf.py

+55-5
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@
1212

1313
import nnf
1414

15-
from nnf import Var, And, Or, amc, dimacs, dsharp, operators
15+
from nnf import (Var, And, Or, amc, dimacs, dsharp, operators,
16+
tseitin, complete_models)
1617

1718
settings.register_profile('patient', deadline=2000,
1819
suppress_health_check=(HealthCheck.too_slow,))
@@ -120,13 +121,19 @@ def CNF(draw):
120121

121122
@st.composite
122123
def models(draw):
123-
return And(Var(name, draw(st.booleans()))
124-
for name in range(1, 9))
124+
return And(
125+
Var(name, draw(st.booleans()))
126+
for name in range(1, draw(st.integers(min_value=1, max_value=9)))
127+
)
125128

126129

127130
@st.composite
128131
def MODS(draw):
129-
return Or(draw(st.frozensets(models())))
132+
num = draw(st.integers(min_value=1, max_value=9))
133+
amount = draw(st.integers(min_value=0, max_value=10))
134+
return Or(And(Var(name, draw(st.booleans()))
135+
for name in range(1, num))
136+
for _ in range(amount))
130137

131138

132139
@st.composite
@@ -702,7 +709,7 @@ def test_nor(a: nnf.NNF, b: nnf.NNF):
702709

703710

704711
@given(NNF(), NNF())
705-
def test_implies(a: nnf.NNF, b: nnf.NNF):
712+
def test_implies2(a: nnf.NNF, b: nnf.NNF):
706713
c = operators.implies(a, b)
707714
for model in nnf.all_models(c.vars()):
708715
assert ((a.satisfied_by(model) and not b.satisfied_by(model)) !=
@@ -771,3 +778,46 @@ def test_dsharp_compile_converting_names(sentence: And[Or[Var]]):
771778
assert all(isinstance(name, str) for name in compiled.vars())
772779
if sentence.satisfiable():
773780
assert sentence.equivalent(compiled)
781+
782+
783+
@given(NNF())
784+
def test_tseitin(sentence: nnf.NNF):
785+
786+
# Assumption to reduce the time in testing
787+
assume(sentence.size() <= 10)
788+
789+
T = tseitin.to_CNF(sentence)
790+
assert T.is_CNF()
791+
792+
# TODO: Once forgetting/projection is implemented,
793+
# do this more complete check
794+
# aux = filter(lambda x: 'aux' in str(x.name), T.vars())
795+
# assert T.forget(aux).equivalent(sentence)
796+
797+
models = list(complete_models(T.models(), sentence.vars() | T.vars()))
798+
799+
for mt in models:
800+
assert sentence.satisfied_by(mt)
801+
802+
assert len(models) == sentence.model_count()
803+
804+
@given(models())
805+
def test_complete_models(model: nnf.And[nnf.Var]):
806+
m = {v.name: v.true for v in model}
807+
neg = {v.name: not v.true for v in model}
808+
809+
zero = list(complete_models([m], model.vars()))
810+
assert len(zero) == 1
811+
812+
one = list(complete_models([m], model.vars() | {"test1"}))
813+
assert len(one) == 2
814+
815+
two = list(complete_models([m], model.vars() | {"test1", "test2"}))
816+
assert len(two) == 4
817+
assert all(x.keys() == m.keys() | {"test1", "test2"} for x in two)
818+
819+
if m:
820+
multi = list(complete_models([m, neg], model.vars() | {"test1", "test2"}))
821+
assert len(multi) == 8
822+
assert len({frozenset(x.items()) for x in multi}) == 8 # all unique
823+
assert all(x.keys() == m.keys() | {"test1", "test2"} for x in multi)

Diff for: tox.ini

+3-3
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,11 @@ toxworkdir = {env:TOX_WORK_DIR:.tox}
66
deps =
77
pytest
88
flake8
9-
py{35,36,37,38}: mypy
9+
py{3,35,36,37,38}: mypy
1010
hypothesis
1111
commands =
1212
flake8 nnf
13-
py{35,36,37,38}: python -m mypy --strict nnf
13+
py{3,35,36,37,38}: python -m mypy --strict nnf
1414
pytest
15-
py{36,37,38,py3}: python -m doctest nnf/__init__.py -o ELLIPSIS
15+
py{3,36,37,38,py3}: python -m doctest nnf/__init__.py -o ELLIPSIS
1616
python examples/socialchoice.py

0 commit comments

Comments
 (0)