Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
sagemath
GitHub Repository: sagemath/sagesmc
Path: blob/master/src/sage/sat/solvers/dimacs.py
8820 views
1
"""
2
SAT-Solvers via DIMACS Files
3
4
Sage supports calling SAT solvers using the popular DIMACS format. This module implements
5
infrastructure to make it easy to add new such interfaces and some example interfaces.
6
7
Currently, interfaces to **RSat** [RS]_ and **Glucose** [GL]_ are included by default.
8
9
.. note::
10
11
Our SAT solver interfaces are 1-based, i.e., literals start at 1. This is consistent with the
12
popular DIMACS format for SAT solving but not with Pythion's 0-based convention. However, this
13
also allows to construct clauses using simple integers.
14
15
AUTHORS:
16
17
- Martin Albrecht (2012): first version
18
19
Classes and Methods
20
-------------------
21
"""
22
##############################################################################
23
# Copyright (C) 2012 Martin Albrecht <[email protected]>
24
# Distributed under the terms of the GNU General Public License (GPL)
25
# The full text of the GPL is available at:
26
# http://www.gnu.org/licenses/
27
##############################################################################
28
29
import os, sys, subprocess, shlex
30
31
from sage.sat.solvers.satsolver import SatSolver
32
from sage.misc.misc import tmp_filename, get_verbose
33
from time import sleep
34
35
class DIMACS(SatSolver):
36
"""
37
Generic DIMACS Solver.
38
39
.. note::
40
41
Usually, users won't have to use this class directly but some
42
class which inherits from this class.
43
44
.. automethod:: __init__
45
.. automethod:: __call__
46
"""
47
48
command = ""
49
50
def __init__(self, command=None, filename=None, verbosity=0, **kwds):
51
"""
52
Construct a new generic DIMACS solver.
53
54
INPUT:
55
56
- ``command`` - a named format string with the command to
57
run. The string must contain {input} and may contain
58
{output} if the solvers writes the solution to an output
59
file. For example "sat-solver {input}" is a valid
60
command. If ``None`` then the class variable ``command`` is
61
used. (default: ``None``)
62
63
- ``filename`` - a filename to write clauses to in DIMACS
64
format, must be writable. If ``None`` a temporary filename
65
is chosen automatically. (default: ``None``)
66
67
- ``verbosity`` - a verbosity level, where zero means silent
68
and anything else means verbose output. (default: ``0``)
69
70
- ``**kwds`` - accepted for compatibility with other solves,
71
ignored.
72
73
TESTS::
74
75
sage: from sage.sat.solvers.dimacs import DIMACS
76
sage: DIMACS()
77
DIMACS Solver: ''
78
"""
79
if filename is None:
80
filename = tmp_filename()
81
82
self._headname = filename
83
self._verbosity = verbosity
84
85
if command is not None:
86
self._command = command
87
else:
88
self._command = self.__class__.command
89
90
self._tail = open(tmp_filename(),'w')
91
self._var = 0
92
self._lit = 0
93
94
def __repr__(self):
95
"""
96
TESTS::
97
98
sage: from sage.sat.solvers.dimacs import DIMACS
99
sage: DIMACS(command="iliketurtles {input}")
100
DIMACS Solver: 'iliketurtles {input}'
101
"""
102
return "DIMACS Solver: '%s'"%(self._command)
103
104
def __del__(self):
105
"""
106
TESTS::
107
108
sage: from sage.sat.solvers.dimacs import DIMACS
109
sage: d = DIMACS(command="iliketurtles {input}")
110
sage: del d
111
"""
112
if not self._tail.closed:
113
self._tail.close()
114
if os.path.exists(self._tail.name):
115
os.unlink(self._tail.name)
116
117
def var(self, decision=None):
118
"""
119
Return a *new* variable.
120
121
INPUT:
122
123
- ``decision`` - accepted for compatibility with other solvers, ignored.
124
125
EXAMPLE::
126
127
sage: from sage.sat.solvers.dimacs import DIMACS
128
sage: solver = DIMACS()
129
sage: solver.var()
130
1
131
"""
132
self._var+= 1
133
return self._var
134
135
def nvars(self):
136
"""
137
Return the number of variables.
138
139
EXAMPLE::
140
141
sage: from sage.sat.solvers.dimacs import DIMACS
142
sage: solver = DIMACS()
143
sage: solver.var()
144
1
145
sage: solver.var(decision=True)
146
2
147
sage: solver.nvars()
148
2
149
"""
150
return self._var
151
152
def add_clause(self, lits):
153
"""
154
Add a new clause to set of clauses.
155
156
INPUT:
157
158
- ``lits`` - a tuple of integers != 0
159
160
.. note::
161
162
If any element ``e`` in ``lits`` has ``abs(e)`` greater
163
than the number of variables generated so far, then new
164
variables are created automatically.
165
166
EXAMPLE::
167
168
sage: from sage.sat.solvers.dimacs import DIMACS
169
sage: solver = DIMACS()
170
sage: solver.var()
171
1
172
sage: solver.var(decision=True)
173
2
174
sage: solver.add_clause( (1, -2 , 3) )
175
sage: solver
176
DIMACS Solver: ''
177
"""
178
l = []
179
for lit in lits:
180
lit = int(lit)
181
while abs(lit) > self.nvars():
182
self.var()
183
l.append(str(lit))
184
l.append("0\n")
185
self._tail.write(" ".join(l) )
186
self._lit += 1
187
188
def write(self, filename=None):
189
"""
190
Write DIMACS file.
191
192
INPUT:
193
194
- ``filename`` - if ``None`` default filename specified at initialization is used for
195
writing to (default: ``None``)
196
197
EXAMPLE::
198
199
sage: from sage.sat.solvers.dimacs import DIMACS
200
sage: fn = tmp_filename()
201
sage: solver = DIMACS(filename=fn)
202
sage: solver.add_clause( (1, -2 , 3) )
203
sage: _ = solver.write()
204
sage: for line in open(fn).readlines():
205
... print line,
206
p cnf 3 1
207
1 -2 3 0
208
209
sage: from sage.sat.solvers.dimacs import DIMACS
210
sage: fn = tmp_filename()
211
sage: solver = DIMACS()
212
sage: solver.add_clause( (1, -2 , 3) )
213
sage: _ = solver.write(fn)
214
sage: for line in open(fn).readlines():
215
... print line,
216
p cnf 3 1
217
1 -2 3 0
218
"""
219
headname = self._headname if filename is None else filename
220
head = open(headname, "w")
221
head.truncate(0)
222
head.write("p cnf %d %d\n"%(self._var,self._lit))
223
head.close()
224
225
tail = self._tail
226
tail.close()
227
228
head = open(headname,"a")
229
tail = open(self._tail.name,"r")
230
head.write(tail.read())
231
tail.close()
232
head.close()
233
234
self._tail = open(self._tail.name,"a")
235
return headname
236
237
def clauses(self, filename=None):
238
"""
239
Return original clauses.
240
241
INPUT:
242
243
- ``filename`` - if not ``None`` clauses are written to ``filename`` in
244
DIMACS format (default: ``None``)
245
246
OUTPUT:
247
248
If ``filename`` is ``None`` then a list of ``lits, is_xor, rhs``
249
tuples is returned, where ``lits`` is a tuple of literals,
250
``is_xor`` is always ``False`` and ``rhs`` is always ``None``.
251
252
If ``filename`` points to a writable file, then the list of original
253
clauses is written to that file in DIMACS format.
254
255
EXAMPLE::
256
257
sage: from sage.sat.solvers.dimacs import DIMACS
258
sage: fn = tmp_filename()
259
sage: solver = DIMACS()
260
sage: solver.add_clause( (1, 2, 3) )
261
sage: solver.clauses()
262
[((1, 2, 3), False, None)]
263
264
sage: solver.add_clause( (1, 2, -3) )
265
sage: solver.clauses(fn)
266
sage: print open(fn).read()
267
p cnf 3 2
268
1 2 3 0
269
1 2 -3 0
270
<BLANKLINE>
271
"""
272
if filename is not None:
273
self.write(filename)
274
else:
275
tail = self._tail
276
tail.close()
277
tail = open(self._tail.name,"r")
278
279
clauses = []
280
for line in tail.readlines():
281
if line.startswith("p") or line.startswith("c"):
282
continue
283
clause = []
284
for lit in line.split(" "):
285
lit = int(lit)
286
if lit == 0:
287
break
288
clause.append(lit)
289
clauses.append( ( tuple(clause), False, None ) )
290
tail.close()
291
self._tail = open(self._tail.name, "a")
292
return clauses
293
294
@staticmethod
295
def render_dimacs(clauses, filename, nlits):
296
"""
297
Produce DIMACS file ``filename`` from ``clauses``.
298
299
INPUT:
300
301
- ``clauses`` - a list of clauses, either in simple format as a list of
302
literals or in extended format for CryptoMiniSat: a tuple of literals,
303
``is_xor`` and ``rhs``.
304
305
- ``filename`` - the file to write to
306
307
- ``nlits -- the number of literals appearing in ``clauses``
308
309
EXAMPLE::
310
311
sage: from sage.sat.solvers.dimacs import DIMACS
312
sage: fn = tmp_filename()
313
sage: solver = DIMACS()
314
sage: solver.add_clause( (1, 2, -3) )
315
sage: DIMACS.render_dimacs(solver.clauses(), fn, solver.nvars())
316
sage: print open(fn).read()
317
p cnf 3 1
318
1 2 -3 0
319
<BLANKLINE>
320
321
This is equivalent to::
322
323
sage: solver.clauses(fn)
324
sage: print open(fn).read()
325
p cnf 3 1
326
1 2 -3 0
327
<BLANKLINE>
328
329
This function also accepts a "simple" format::
330
331
sage: DIMACS.render_dimacs([ (1,2), (1,2,-3) ], fn, 3)
332
sage: print open(fn).read()
333
p cnf 3 2
334
1 2 0
335
1 2 -3 0
336
<BLANKLINE>
337
"""
338
fh = open(filename, "w")
339
fh.write("p cnf %d %d\n"%(nlits,len(clauses)))
340
for clause in clauses:
341
if len(clause) == 3 and clause[1] in (True, False) and clause[2] in (True,False,None):
342
lits, is_xor, rhs = clause
343
else:
344
lits, is_xor, rhs = clause, False, None
345
346
if is_xor:
347
closing = lits[-1] if rhs else -lits[-1]
348
fh.write("x" + " ".join(map(str, lits[:-1])) + " %d 0\n"%closing)
349
else:
350
fh.write(" ".join(map(str, lits)) + " 0\n")
351
fh.close()
352
353
def __call__(self, assumptions=None):
354
"""
355
Run 'command' and collect output.
356
357
INPUT:
358
359
- ``assumptions`` - ignored, accepted for compatibility with
360
other solvers (default: ``None``)
361
362
TESTS:
363
364
This class is not meant to be called directly::
365
366
sage: from sage.sat.solvers.dimacs import DIMACS
367
sage: fn = tmp_filename()
368
sage: solver = DIMACS(filename=fn)
369
sage: solver.add_clause( (1, -2 , 3) )
370
sage: solver()
371
Traceback (most recent call last):
372
...
373
ValueError: No SAT solver command selected.
374
"""
375
if assumptions is not None:
376
raise NotImplementedError("Assumptions are not supported for DIMACS based solvers.")
377
378
self.write()
379
380
output_filename = None
381
self._output = []
382
383
command = self._command.strip()
384
385
if not command:
386
raise ValueError("No SAT solver command selected.")
387
388
389
if "{output}" in command:
390
output_filename = tmp_filename()
391
command = command.format(input=self._headname, output=output_filename)
392
393
args = shlex.split(command)
394
395
try:
396
process = subprocess.Popen(args, stdout=subprocess.PIPE)
397
except OSError:
398
raise OSError("Could run '%s', perhaps you need to add your SAT solver to $PATH?"%(" ".join(args)))
399
400
try:
401
while process.poll() is None:
402
for line in iter(process.stdout.readline,''):
403
if get_verbose() or self._verbosity:
404
print line,
405
sys.stdout.flush()
406
self._output.append(line)
407
sleep(0.1)
408
if output_filename:
409
self._output.extend(open(output_filename).readlines())
410
except BaseException:
411
process.kill()
412
raise
413
414
class RSat(DIMACS):
415
"""
416
An instance of the RSat solver.
417
418
For information on RSat see: http://reasoning.cs.ucla.edu/rsat/
419
"""
420
421
command = "rsat {input} -v -s"
422
423
def __call__(self, assumptions=None):
424
"""
425
Solve this instance.
426
427
INPUT:
428
429
- ``assumptions`` - ignored, accepted for compatibility with
430
other solvers (default: ``None``)
431
432
OUTPUT:
433
434
- If this instance is SAT: A tuple of length ``nvars()+1``
435
where the ``i``-th entry holds an assignment for the
436
``i``-th variables (the ``0``-th entry is always ``None``).
437
438
- If this instance is UNSAT: ``False``
439
440
EXAMPLE::
441
442
sage: from sage.sat.boolean_polynomials import solve as solve_sat
443
sage: F,s = mq.SR(1,1,1,4,gf2=True,polybori=True).polynomial_system()
444
sage: solve_sat(F, solver=sage.sat.solvers.RSat) # not tested, requires RSat in PATH
445
"""
446
DIMACS.__call__(self)
447
448
s = [None] + [False for _ in range(self.nvars())]
449
for line in self._output:
450
if line.startswith("c"):
451
continue
452
if line.startswith("s"):
453
if "UNSAT" in line:
454
return False
455
if line.startswith("v"):
456
lits = map(int, line[2:-2].strip().split(" "))
457
for e in lits:
458
s[abs(e)] = e>0
459
return tuple(s)
460
461
class Glucose(DIMACS):
462
"""
463
An instance of the Glucose solver.
464
465
For information on Glucose see: http://www.lri.fr/~simon/?page=glucose
466
"""
467
468
command = "glucose_static -verb=2 {input} {output}"
469
470
def __call__(self, **kwds):
471
"""
472
Solve this instance.
473
474
INPUT:
475
476
- ``assumptions`` - ignored, accepted for compatibility with
477
other solvers (default: ``None``)
478
479
OUTPUT:
480
481
- If this instance is SAT: A tuple of length ``nvars()+1``
482
where the ``i``-th entry holds an assignment for the
483
``i``-th variables (the ``0``-th entry is always ``None``).
484
485
- If this instance is UNSAT: ``False``
486
487
EXAMPLE::
488
489
sage: from sage.sat.boolean_polynomials import solve as solve_sat
490
sage: F,s = mq.SR(1,1,1,4,gf2=True,polybori=True).polynomial_system()
491
sage: solve_sat(F, solver=sage.sat.solvers.Glucose) # not tested, requires Glucose in PATH
492
"""
493
DIMACS.__call__(self)
494
495
for line in self._output:
496
if line.startswith("c"):
497
continue
498
if line.startswith("s"):
499
if "UNSAT" in line:
500
return False
501
try:
502
s = map(int, line[:-2].strip().split(" "))
503
s = (None,) + tuple(e>0 for e in s)
504
return s
505
except ValueError:
506
pass
507
return False
508
509