Skip to content

Commit d89337c

Browse files
authored
Merge pull request #9 from QuMuLab/dynamic-solver-loading
Dynamic solver loading
2 parents 290ba99 + 5eaabf1 commit d89337c

File tree

9 files changed

+172
-6
lines changed

9 files changed

+172
-6
lines changed

MANIFEST.in

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
recursive-include nnf/bin *

docs/nnf.rst

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,3 +51,11 @@ nnf.tseitin module
5151
:members:
5252
:undoc-members:
5353
:show-inheritance:
54+
55+
nnf.kissat module
56+
-----------------
57+
58+
.. automodule:: nnf.kissat
59+
:members:
60+
:undoc-members:
61+
:show-inheritance:

nnf/__init__.py

Lines changed: 35 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,8 @@
3232

3333
__all__ = ('NNF', 'Internal', 'And', 'Or', 'Var', 'Aux', 'Builder',
3434
'all_models', 'complete_models', 'decision', 'true', 'false',
35-
'dsharp', 'dimacs', 'amc', 'tseitin', 'operators')
35+
'dsharp', 'dimacs', 'amc', 'kissat', 'using_kissat', 'tseitin',
36+
'operators')
3637

3738

3839
def all_models(names: 't.Iterable[Name]') -> t.Iterator[Model]:
@@ -60,6 +61,26 @@ def all_models(names: 't.Iterable[Name]') -> t.Iterator[Model]:
6061
T_NNF_co = t.TypeVar('T_NNF_co', bound='NNF', covariant=True)
6162
_Tristate = t.Optional[bool]
6263

64+
# Valid values: native and kissat
65+
SAT_BACKEND = 'native'
66+
67+
68+
class using_kissat():
69+
"""Context manager to use the kissat solver in a block of code."""
70+
71+
def __init__(self) -> None:
72+
self.setting = SAT_BACKEND
73+
74+
def __enter__(self) -> 'using_kissat':
75+
global SAT_BACKEND
76+
SAT_BACKEND = 'kissat'
77+
return self
78+
79+
def __exit__(self, *_: t.Any) -> None:
80+
global SAT_BACKEND
81+
SAT_BACKEND = self.setting
82+
83+
6384
if t.TYPE_CHECKING:
6485
def memoize(func: T) -> T:
6586
...
@@ -264,10 +285,9 @@ def satisfiable(
264285

265286
if cnf:
266287
return self._cnf_satisfiable()
267-
268-
# todo: use a better fallback
269-
return any(self.satisfied_by(model)
270-
for model in all_models(self.vars()))
288+
else:
289+
from nnf import tseitin
290+
return tseitin.to_CNF(self)._cnf_satisfiable()
271291

272292
def _satisfiable_decomposable(self) -> bool:
273293
"""Checks satisfiability of decomposable sentences.
@@ -596,6 +616,16 @@ def to_CNF(self) -> 'And[Or[Var]]':
596616
return tseitin.to_CNF(self)
597617

598618
def _cnf_satisfiable(self) -> bool:
619+
"""Call a SAT solver on the presumed CNF theory."""
620+
if SAT_BACKEND == 'native':
621+
return self._cnf_satisfiable_native()
622+
elif SAT_BACKEND == 'kissat':
623+
from nnf import kissat
624+
return kissat.solve(t.cast(And[Or[Var]], self)) is not None
625+
else:
626+
raise NotImplementedError('Unrecognized SAT backend: '+SAT_BACKEND)
627+
628+
def _cnf_satisfiable_native(self) -> bool:
599629
"""A naive DPLL SAT solver."""
600630
def DPLL(clauses: t.FrozenSet[t.FrozenSet[Var]]) -> bool:
601631
if not clauses:

nnf/bin/LICENSE.kissat

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
Copyright (c) 2019-2020 Armin Biere, Johannes Kepler University Linz, Austria
2+
3+
Permission is hereby granted, free of charge, to any person obtaining a copy
4+
of this software and associated documentation files (the "Software"), to deal
5+
in the Software without restriction, including without limitation the rights
6+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7+
copies of the Software, and to permit persons to whom the Software is
8+
furnished to do so, subject to the following conditions:
9+
10+
The above copyright notice and this permission notice shall be included in all
11+
copies or substantial portions of the Software.
12+
13+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19+
SOFTWARE.

nnf/bin/README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# Binary files included with python-nnf
2+
3+
## kissat
4+
5+
The `kissat` SAT solver is released under MIT license, and can be found [here](https://github.com/arminbiere/kissat/). The binary included in this directory corresponds to the statically linked version at [this commit](https://github.com/arminbiere/kissat/commit/baef4609163f542dc08f43aef02ce8da0581a2b5). No modifications were made to the `kissat` solver.

nnf/bin/kissat

1.28 MB
Binary file not shown.

nnf/kissat.py

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
"""Interoperability with `kissat <http://fmv.jku.at/kissat/>`_.
2+
3+
``solve`` invokes the SAT solver directly on the given theory.
4+
"""
5+
6+
import os
7+
import shutil
8+
import subprocess
9+
import typing as t
10+
11+
from nnf import And, Or, Var, dimacs, Model
12+
13+
__all__ = ('solve',)
14+
15+
16+
def solve(
17+
sentence: And[Or[Var]],
18+
extra_args: t.Sequence[str] = ()
19+
) -> t.Optional[Model]:
20+
"""Run kissat to compute a satisfying assignment.
21+
22+
:param sentence: The CNF sentence to solve.
23+
:param extra_args: Extra arguments to pass to kissat.
24+
"""
25+
26+
if not sentence.is_CNF():
27+
raise ValueError("Sentence must be in CNF")
28+
29+
if shutil.which('kissat') is not None:
30+
SOLVER = 'kissat'
31+
else:
32+
SOLVER = os.path.join(
33+
os.path.dirname(os.path.abspath(__file__)), 'bin', 'kissat'
34+
)
35+
assert os.path.isfile(SOLVER), "Cannot seem to find kissat solver."
36+
37+
args = [SOLVER]
38+
args.extend(extra_args)
39+
40+
var_labels = dict(enumerate(sentence.vars(), start=1))
41+
var_labels_inverse = {v: k for k, v in var_labels.items()}
42+
43+
cnf = dimacs.dumps(sentence, mode='cnf', var_labels=var_labels_inverse)
44+
45+
try:
46+
proc = subprocess.Popen(
47+
args,
48+
stdout=subprocess.PIPE,
49+
stdin=subprocess.PIPE,
50+
universal_newlines=True
51+
)
52+
log, _ = proc.communicate(cnf)
53+
except OSError as err:
54+
if err.errno == 8:
55+
print("Error: Attempting to run the kissat binary on an")
56+
print(" incompatible system. Consider compiling kissat")
57+
print(" natively so it is accessible via the command line.")
58+
raise
59+
60+
# Two known exit codes for the solver
61+
if proc.returncode not in [10, 20]:
62+
raise RuntimeError(
63+
"kissat failed with code {}. Log:\n\n{}".format(
64+
proc.returncode, log
65+
)
66+
)
67+
68+
# Unsatisfiable
69+
if proc.returncode == 20:
70+
return None
71+
72+
assert proc.returncode == 10, "Bad error code. Log:\n\n{}".format(log)
73+
74+
variable_lines = [
75+
line[2:] for line in log.split("\n") if line.startswith("v ")
76+
]
77+
literals = [int(num) for line in variable_lines for num in line.split()]
78+
assert literals[-1] == 0, "Last entry should be 0. Log:\n\n{}".format(log)
79+
literals.pop()
80+
model = {var_labels[abs(lit)]: lit > 0 for lit in literals}
81+
82+
return model

setup.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,4 +46,6 @@
4646
'Documentation': "https://python-nnf.readthedocs.io/",
4747
'Source': "https://github.com/QuMuLab/python-nnf",
4848
},
49+
include_package_data=True,
50+
zip_safe=False,
4951
)

test_nnf.py

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import copy
22
import pickle
3+
import platform
34
import shutil
45
import os
56

@@ -13,7 +14,7 @@
1314
import nnf
1415

1516
from nnf import (Var, And, Or, amc, dimacs, dsharp, operators,
16-
tseitin, complete_models)
17+
tseitin, complete_models, using_kissat)
1718

1819
settings.register_profile('patient', deadline=2000,
1920
suppress_health_check=(HealthCheck.too_slow,))
@@ -821,3 +822,21 @@ def test_complete_models(model: nnf.And[nnf.Var]):
821822
assert len(multi) == 8
822823
assert len({frozenset(x.items()) for x in multi}) == 8 # all unique
823824
assert all(x.keys() == m.keys() | {"test1", "test2"} for x in multi)
825+
826+
if (platform.uname().system, platform.uname().machine) == ('Linux', 'x86_64'):
827+
828+
def test_kissat_uf20():
829+
for sentence in uf20_cnf:
830+
with using_kissat():
831+
assert sentence.satisfiable()
832+
833+
@given(CNF())
834+
def test_kissat_cnf(sentence: And[Or[Var]]):
835+
assume(all(len(clause) > 0 for clause in sentence))
836+
with using_kissat():
837+
assert sentence.satisfiable() == sentence._cnf_satisfiable_native()
838+
839+
@given(NNF())
840+
def test_kissat_nnf(sentence: And[Or[Var]]):
841+
with using_kissat():
842+
assert sentence.satisfiable() == tseitin.to_CNF(sentence)._cnf_satisfiable_native()

0 commit comments

Comments
 (0)