python-sat 1.8.dev25__cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of python-sat might be problematic. Click here for more details.
- pycard.cpython-312-x86_64-linux-gnu.so +0 -0
- pysat/__init__.py +24 -0
- pysat/_fileio.py +209 -0
- pysat/_utils.py +58 -0
- pysat/allies/__init__.py +0 -0
- pysat/allies/approxmc.py +385 -0
- pysat/allies/unigen.py +435 -0
- pysat/card.py +802 -0
- pysat/engines.py +1302 -0
- pysat/examples/__init__.py +0 -0
- pysat/examples/bbscan.py +663 -0
- pysat/examples/bica.py +691 -0
- pysat/examples/fm.py +527 -0
- pysat/examples/genhard.py +516 -0
- pysat/examples/hitman.py +653 -0
- pysat/examples/lbx.py +638 -0
- pysat/examples/lsu.py +496 -0
- pysat/examples/mcsls.py +610 -0
- pysat/examples/models.py +189 -0
- pysat/examples/musx.py +344 -0
- pysat/examples/optux.py +710 -0
- pysat/examples/primer.py +620 -0
- pysat/examples/rc2.py +1858 -0
- pysat/examples/usage.py +63 -0
- pysat/formula.py +5619 -0
- pysat/pb.py +463 -0
- pysat/process.py +363 -0
- pysat/solvers.py +7591 -0
- pysolvers.cpython-312-x86_64-linux-gnu.so +0 -0
- python_sat-1.8.dev25.data/scripts/approxmc.py +385 -0
- python_sat-1.8.dev25.data/scripts/bbscan.py +663 -0
- python_sat-1.8.dev25.data/scripts/bica.py +691 -0
- python_sat-1.8.dev25.data/scripts/fm.py +527 -0
- python_sat-1.8.dev25.data/scripts/genhard.py +516 -0
- python_sat-1.8.dev25.data/scripts/lbx.py +638 -0
- python_sat-1.8.dev25.data/scripts/lsu.py +496 -0
- python_sat-1.8.dev25.data/scripts/mcsls.py +610 -0
- python_sat-1.8.dev25.data/scripts/models.py +189 -0
- python_sat-1.8.dev25.data/scripts/musx.py +344 -0
- python_sat-1.8.dev25.data/scripts/optux.py +710 -0
- python_sat-1.8.dev25.data/scripts/primer.py +620 -0
- python_sat-1.8.dev25.data/scripts/rc2.py +1858 -0
- python_sat-1.8.dev25.data/scripts/unigen.py +435 -0
- python_sat-1.8.dev25.dist-info/METADATA +45 -0
- python_sat-1.8.dev25.dist-info/RECORD +48 -0
- python_sat-1.8.dev25.dist-info/WHEEL +6 -0
- python_sat-1.8.dev25.dist-info/licenses/LICENSE.txt +21 -0
- python_sat-1.8.dev25.dist-info/top_level.txt +3 -0
|
@@ -0,0 +1,638 @@
|
|
|
1
|
+
#!python
|
|
2
|
+
#-*- coding:utf-8 -*-
|
|
3
|
+
##
|
|
4
|
+
## lbx.py
|
|
5
|
+
##
|
|
6
|
+
## Created on: Jan 9, 2017
|
|
7
|
+
## Author: Alexey S. Ignatiev
|
|
8
|
+
## E-mail: aignatiev@ciencias.ulisboa.pt
|
|
9
|
+
##
|
|
10
|
+
|
|
11
|
+
"""
|
|
12
|
+
===============
|
|
13
|
+
List of classes
|
|
14
|
+
===============
|
|
15
|
+
|
|
16
|
+
.. autosummary::
|
|
17
|
+
:nosignatures:
|
|
18
|
+
|
|
19
|
+
LBX
|
|
20
|
+
|
|
21
|
+
==================
|
|
22
|
+
Module description
|
|
23
|
+
==================
|
|
24
|
+
|
|
25
|
+
This module implements a prototype of the LBX algorithm for the computation
|
|
26
|
+
of a *minimal correction subset* (MCS) and/or MCS enumeration. The LBX
|
|
27
|
+
abbreviation stands for *literal-based MCS extraction* algorithm, which was
|
|
28
|
+
proposed in [1]_. Note that this prototype does not follow the original
|
|
29
|
+
low-level implementation of the corresponding MCS extractor available
|
|
30
|
+
`online <https://reason.di.fc.ul.pt/wiki/doku.php?id=lbx>`_ (compared to
|
|
31
|
+
our prototype, the low-level implementation has a number of additional
|
|
32
|
+
heuristics used). However, it implements the LBX algorithm for partial
|
|
33
|
+
MaxSAT formulas, as described in [1]_.
|
|
34
|
+
|
|
35
|
+
.. [1] Carlos Mencia, Alessandro Previti, Joao Marques-Silva.
|
|
36
|
+
*Literal-Based MCS Extraction*. IJCAI 2015. pp. 1973-1979
|
|
37
|
+
|
|
38
|
+
The implementation can be used as an executable (the list of available
|
|
39
|
+
command-line options can be shown using ``lbx.py -h``) in the following
|
|
40
|
+
way:
|
|
41
|
+
|
|
42
|
+
::
|
|
43
|
+
|
|
44
|
+
$ xzcat formula.wcnf.xz
|
|
45
|
+
p wcnf 3 6 4
|
|
46
|
+
1 1 0
|
|
47
|
+
1 2 0
|
|
48
|
+
1 3 0
|
|
49
|
+
4 -1 -2 0
|
|
50
|
+
4 -1 -3 0
|
|
51
|
+
4 -2 -3 0
|
|
52
|
+
|
|
53
|
+
$ lbx.py -d -e all -s glucose3 -vv formula.wcnf.xz
|
|
54
|
+
c MCS: 1 3 0
|
|
55
|
+
c cost: 2
|
|
56
|
+
c MCS: 2 3 0
|
|
57
|
+
c cost: 2
|
|
58
|
+
c MCS: 1 2 0
|
|
59
|
+
c cost: 2
|
|
60
|
+
c oracle time: 0.0002
|
|
61
|
+
|
|
62
|
+
Alternatively, the algorithm can be accessed and invoked through the
|
|
63
|
+
standard ``import`` interface of Python, e.g.
|
|
64
|
+
|
|
65
|
+
.. code-block:: python
|
|
66
|
+
|
|
67
|
+
>>> from pysat.examples.lbx import LBX
|
|
68
|
+
>>> from pysat.formula import WCNF
|
|
69
|
+
>>>
|
|
70
|
+
>>> wcnf = WCNF(from_file='formula.wcnf.xz')
|
|
71
|
+
>>>
|
|
72
|
+
>>> lbx = LBX(wcnf, use_cld=True, solver_name='g3')
|
|
73
|
+
>>> for mcs in lbx.enumerate():
|
|
74
|
+
... lbx.block(mcs)
|
|
75
|
+
... print(mcs)
|
|
76
|
+
[1, 3]
|
|
77
|
+
[2, 3]
|
|
78
|
+
[1, 2]
|
|
79
|
+
|
|
80
|
+
==============
|
|
81
|
+
Module details
|
|
82
|
+
==============
|
|
83
|
+
"""
|
|
84
|
+
|
|
85
|
+
#
|
|
86
|
+
#==============================================================================
|
|
87
|
+
from __future__ import print_function
|
|
88
|
+
import collections
|
|
89
|
+
import getopt
|
|
90
|
+
from math import copysign
|
|
91
|
+
import os
|
|
92
|
+
from pysat.formula import CNFPlus, WCNFPlus
|
|
93
|
+
from pysat.process import Processor
|
|
94
|
+
from pysat.solvers import Solver, SolverNames
|
|
95
|
+
import re
|
|
96
|
+
from six.moves import range
|
|
97
|
+
import sys
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
#
|
|
101
|
+
#==============================================================================
|
|
102
|
+
class LBX(object):
|
|
103
|
+
"""
|
|
104
|
+
LBX-like algorithm for computing MCSes. Given an unsatisfiable partial
|
|
105
|
+
CNF formula, i.e. formula in the :class:`.WCNF` format, this class can
|
|
106
|
+
be used to compute a given number of MCSes of the formula. The
|
|
107
|
+
implementation follows the LBX algorithm description in [1]_. It can
|
|
108
|
+
use any SAT solver available in PySAT. Additionally, the "clause
|
|
109
|
+
:math:`D`" heuristic can be used when enumerating MCSes.
|
|
110
|
+
|
|
111
|
+
The default SAT solver to use is ``m22`` (see :class:`.SolverNames`).
|
|
112
|
+
The "clause :math:`D`" heuristic is disabled by default, i.e.
|
|
113
|
+
``use_cld`` is set to ``False``. Internal SAT solver's timer is also
|
|
114
|
+
disabled by default, i.e. ``use_timer`` is ``False``.
|
|
115
|
+
|
|
116
|
+
Additionally, the input formula can be preprocessed before running MCS
|
|
117
|
+
enumeration. This is controlled by the input parameter ``process``
|
|
118
|
+
whose integer value signifies the number of processing rounds to be
|
|
119
|
+
applied. The number of rounds is set to 0 by default.
|
|
120
|
+
|
|
121
|
+
:param formula: unsatisfiable partial CNF formula
|
|
122
|
+
:param use_cld: whether or not to use "clause :math:`D`"
|
|
123
|
+
:param solver_name: SAT oracle name
|
|
124
|
+
:param process: apply formula preprocessing this many times
|
|
125
|
+
:param use_timer: whether or not to use SAT solver's timer
|
|
126
|
+
|
|
127
|
+
:type formula: :class:`.WCNF`
|
|
128
|
+
:type use_cld: bool
|
|
129
|
+
:type solver_name: str
|
|
130
|
+
:type process: int
|
|
131
|
+
:type use_timer: bool
|
|
132
|
+
"""
|
|
133
|
+
|
|
134
|
+
def __init__(self, formula, use_cld=False, solver_name='m22', process=0,
|
|
135
|
+
use_timer=False):
|
|
136
|
+
"""
|
|
137
|
+
Constructor.
|
|
138
|
+
"""
|
|
139
|
+
|
|
140
|
+
# dealing with preprocessing, if required
|
|
141
|
+
hard = formula.hard if process > 0 else []
|
|
142
|
+
|
|
143
|
+
# bootstrapping the solver with hard clauses
|
|
144
|
+
self.oracle = Solver(name=solver_name,
|
|
145
|
+
bootstrap_with=formula.hard if process == 0 else [],
|
|
146
|
+
use_timer=use_timer)
|
|
147
|
+
self.solver = solver_name
|
|
148
|
+
|
|
149
|
+
# adding native cardinality constraints (if any) as hard clauses
|
|
150
|
+
# this can be done only if the Minicard solver is in use
|
|
151
|
+
if isinstance(formula, WCNFPlus) and formula.atms:
|
|
152
|
+
# we are using CaDiCaL195 and it can use external linear engine
|
|
153
|
+
if solver_name in SolverNames.cadical195:
|
|
154
|
+
self.oracle.activate_atmost()
|
|
155
|
+
|
|
156
|
+
assert self.oracle.supports_atmost(), \
|
|
157
|
+
'{0} does not support native cardinality constraints. Make sure you use the right type of formula.'.format(solver_name)
|
|
158
|
+
|
|
159
|
+
for atm in formula.atms:
|
|
160
|
+
self.oracle.add_atmost(*atm)
|
|
161
|
+
|
|
162
|
+
self.topv = formula.nv # top variable id
|
|
163
|
+
self.soft = formula.soft
|
|
164
|
+
self.sels = []
|
|
165
|
+
self.ucld = use_cld
|
|
166
|
+
|
|
167
|
+
# mappings between internal and external variables
|
|
168
|
+
VariableMap = collections.namedtuple('VariableMap', ['e2i', 'i2e'])
|
|
169
|
+
self.vmap = VariableMap(e2i={}, i2e={})
|
|
170
|
+
|
|
171
|
+
# at this point internal and external variables are the same
|
|
172
|
+
for v in range(1, formula.nv + 1):
|
|
173
|
+
self.vmap.e2i[v] = v
|
|
174
|
+
self.vmap.i2e[v] = v
|
|
175
|
+
|
|
176
|
+
for cl in self.soft:
|
|
177
|
+
sel = cl[0]
|
|
178
|
+
if len(cl) > 1 or cl[0] < 0:
|
|
179
|
+
self.topv += 1
|
|
180
|
+
sel = self.topv
|
|
181
|
+
|
|
182
|
+
if process == 0:
|
|
183
|
+
self.oracle.add_clause(cl + [-sel])
|
|
184
|
+
else:
|
|
185
|
+
# adding to formula's hard clauses
|
|
186
|
+
# if any preprocessing is required
|
|
187
|
+
hard.append(cl + [-sel])
|
|
188
|
+
|
|
189
|
+
self.sels.append(sel)
|
|
190
|
+
|
|
191
|
+
# finally, applying formula processing, if any
|
|
192
|
+
if process:
|
|
193
|
+
# the processor is immediately destroyed,
|
|
194
|
+
# as we do not need to restore the models
|
|
195
|
+
with Processor(bootstrap_with=hard) as processor:
|
|
196
|
+
proc = processor.process(rounds=process, freeze=self.sels)
|
|
197
|
+
self.oracle.append_formula(proc)
|
|
198
|
+
|
|
199
|
+
# we won't have access to the original soft
|
|
200
|
+
# clauses in the case of formula preprocessing
|
|
201
|
+
self.soft = [[l] for l in self.sels]
|
|
202
|
+
|
|
203
|
+
def __del__(self):
|
|
204
|
+
"""
|
|
205
|
+
Destructor.
|
|
206
|
+
"""
|
|
207
|
+
|
|
208
|
+
self.delete()
|
|
209
|
+
|
|
210
|
+
def __enter__(self):
|
|
211
|
+
"""
|
|
212
|
+
'with' constructor.
|
|
213
|
+
"""
|
|
214
|
+
|
|
215
|
+
return self
|
|
216
|
+
|
|
217
|
+
def __exit__(self, exc_type, exc_value, traceback):
|
|
218
|
+
"""
|
|
219
|
+
'with' destructor.
|
|
220
|
+
"""
|
|
221
|
+
|
|
222
|
+
self.delete()
|
|
223
|
+
|
|
224
|
+
def delete(self):
|
|
225
|
+
"""
|
|
226
|
+
Explicit destructor of the internal SAT oracle.
|
|
227
|
+
"""
|
|
228
|
+
|
|
229
|
+
if self.oracle:
|
|
230
|
+
self.oracle.delete()
|
|
231
|
+
self.oracle = None
|
|
232
|
+
|
|
233
|
+
def add_clause(self, clause, soft=False):
|
|
234
|
+
"""
|
|
235
|
+
The method for adding a new hard of soft clause to the problem
|
|
236
|
+
formula. Although the input formula is to be specified as an
|
|
237
|
+
argument of the constructor of :class:`LBX`, adding clauses may be
|
|
238
|
+
helpful when *enumerating* MCSes of the formula. This way, the
|
|
239
|
+
clauses are added incrementally, i.e. *on the fly*.
|
|
240
|
+
|
|
241
|
+
The clause to add can be any iterable over integer literals. The
|
|
242
|
+
additional Boolean parameter ``soft`` can be set to ``True``
|
|
243
|
+
meaning the the clause being added is soft (note that parameter
|
|
244
|
+
``soft`` is set to ``False`` by default).
|
|
245
|
+
|
|
246
|
+
Also note that besides pure clauses, the method can also expect
|
|
247
|
+
native cardinality constraints represented as a pair ``(lits,
|
|
248
|
+
bound)``. Only hard cardinality constraints can be added.
|
|
249
|
+
|
|
250
|
+
:param clause: a clause to add
|
|
251
|
+
:param soft: whether or not the clause is soft
|
|
252
|
+
|
|
253
|
+
:type clause: iterable(int)
|
|
254
|
+
:type soft: bool
|
|
255
|
+
"""
|
|
256
|
+
|
|
257
|
+
# first, map external literals to internal literals
|
|
258
|
+
# introduce new variables if necessary
|
|
259
|
+
cl = list(map(lambda l: self._map_extlit(l), clause if not len(clause) == 2 or not type(clause[0]) in (list, tuple, set) else clause[0]))
|
|
260
|
+
|
|
261
|
+
if not soft:
|
|
262
|
+
if not len(clause) == 2 or not type(clause[0]) in (list, tuple, set):
|
|
263
|
+
# the clause is hard, and so we simply add it to the SAT oracle
|
|
264
|
+
self.oracle.add_clause(cl)
|
|
265
|
+
else:
|
|
266
|
+
# this should be a native cardinality constraint,
|
|
267
|
+
# which can be used only together with Minicard
|
|
268
|
+
assert self.oracle.supports_atmost(), \
|
|
269
|
+
'{0} does not support native cardinality constraints. Make sure you use the right type of formula.'.format(self.solver)
|
|
270
|
+
|
|
271
|
+
self.oracle.add_atmost(cl, clause[1])
|
|
272
|
+
else:
|
|
273
|
+
self.soft.append(cl)
|
|
274
|
+
|
|
275
|
+
# soft clauses should be augmented with a selector
|
|
276
|
+
sel = cl[0]
|
|
277
|
+
if len(cl) > 1 or cl[0] < 0:
|
|
278
|
+
self.topv += 1
|
|
279
|
+
sel = self.topv
|
|
280
|
+
|
|
281
|
+
self.oracle.add_clause(cl + [-sel])
|
|
282
|
+
|
|
283
|
+
self.sels.append(sel)
|
|
284
|
+
|
|
285
|
+
def compute(self, enable=[]):
|
|
286
|
+
"""
|
|
287
|
+
Compute and return one solution. This method checks whether the
|
|
288
|
+
hard part of the formula is satisfiable, i.e. an MCS can be
|
|
289
|
+
extracted. If the formula is satisfiable, the model computed by the
|
|
290
|
+
SAT call is used as an *over-approximation* of the MCS in the
|
|
291
|
+
method :func:`_compute` invoked here, which implements the LBX
|
|
292
|
+
algorithm.
|
|
293
|
+
|
|
294
|
+
An MCS is reported as a list of integers, each representing a soft
|
|
295
|
+
clause index (the smallest index is ``1``).
|
|
296
|
+
|
|
297
|
+
An optional input parameter is ``enable``, which represents a
|
|
298
|
+
sequence (normally a list) of soft clause indices that a user
|
|
299
|
+
would prefer to enable/satisfy. Note that this may result in an
|
|
300
|
+
unsatisfiable oracle call, in which case ``None`` will be reported
|
|
301
|
+
as solution. Also, the smallest clause index is assumed to be
|
|
302
|
+
``1``.
|
|
303
|
+
|
|
304
|
+
:param enable: a sequence of clause ids to enable
|
|
305
|
+
:type enable: iterable(int)
|
|
306
|
+
|
|
307
|
+
:rtype: list(int)
|
|
308
|
+
"""
|
|
309
|
+
|
|
310
|
+
self.setd = []
|
|
311
|
+
self.satc = [False for cl in self.soft] # satisfied clauses
|
|
312
|
+
self.solution = None
|
|
313
|
+
self.bb_assumps = [] # backbone assumptions
|
|
314
|
+
self.ss_assumps = [] # satisfied soft clause assumptions
|
|
315
|
+
|
|
316
|
+
if self.oracle.solve(assumptions=[self.sels[cl_id - 1] for cl_id in enable]):
|
|
317
|
+
# hard part is satisfiable => there is a solution
|
|
318
|
+
self._filter_satisfied(update_setd=True)
|
|
319
|
+
self._compute()
|
|
320
|
+
|
|
321
|
+
self.solution = list(map(lambda i: i + 1, filter(lambda i: not self.satc[i], range(len(self.soft)))))
|
|
322
|
+
|
|
323
|
+
return self.solution
|
|
324
|
+
|
|
325
|
+
def enumerate(self):
|
|
326
|
+
"""
|
|
327
|
+
This method iterates through MCSes enumerating them until the
|
|
328
|
+
formula has no more MCSes. The method iteratively invokes
|
|
329
|
+
:func:`compute`. Note that the method does not block the MCSes
|
|
330
|
+
computed - this should be explicitly done by a user.
|
|
331
|
+
"""
|
|
332
|
+
|
|
333
|
+
done = False
|
|
334
|
+
while not done:
|
|
335
|
+
mcs = self.compute()
|
|
336
|
+
|
|
337
|
+
if mcs != None:
|
|
338
|
+
yield mcs
|
|
339
|
+
else:
|
|
340
|
+
done = True
|
|
341
|
+
|
|
342
|
+
def block(self, mcs):
|
|
343
|
+
"""
|
|
344
|
+
Block a (previously computed) MCS. The MCS should be given as an
|
|
345
|
+
iterable of integers. Note that this method is not automatically
|
|
346
|
+
invoked from :func:`enumerate` because a user may want to block
|
|
347
|
+
some of the MCSes conditionally depending on the needs. For
|
|
348
|
+
example, one may want to compute disjoint MCSes only in which case
|
|
349
|
+
this standard blocking is not appropriate.
|
|
350
|
+
|
|
351
|
+
:param mcs: an MCS to block
|
|
352
|
+
:type mcs: iterable(int)
|
|
353
|
+
"""
|
|
354
|
+
|
|
355
|
+
self.oracle.add_clause([self.sels[cl_id - 1] for cl_id in mcs])
|
|
356
|
+
|
|
357
|
+
def _satisfied(self, cl, model):
|
|
358
|
+
"""
|
|
359
|
+
Given a clause (as an iterable of integers) and an assignment (as a
|
|
360
|
+
list of integers), this method checks whether or not the assignment
|
|
361
|
+
satisfies the clause. This is done by a simple clause traversal.
|
|
362
|
+
The method is invoked from :func:`_filter_satisfied`.
|
|
363
|
+
|
|
364
|
+
:param cl: a clause to check
|
|
365
|
+
:param model: an assignment
|
|
366
|
+
|
|
367
|
+
:type cl: iterable(int)
|
|
368
|
+
:type model: list(int)
|
|
369
|
+
|
|
370
|
+
:rtype: bool
|
|
371
|
+
"""
|
|
372
|
+
|
|
373
|
+
for l in cl:
|
|
374
|
+
if len(model) < abs(l) or model[abs(l) - 1] == l:
|
|
375
|
+
# either literal is unassigned or satisfied by the model
|
|
376
|
+
return True
|
|
377
|
+
|
|
378
|
+
return False
|
|
379
|
+
|
|
380
|
+
def _filter_satisfied(self, update_setd=False):
|
|
381
|
+
"""
|
|
382
|
+
This method extracts a model provided by the previous call to a SAT
|
|
383
|
+
oracle and iterates over all soft clauses checking if each of is
|
|
384
|
+
satisfied by the model. Satisfied clauses are marked accordingly
|
|
385
|
+
while the literals of the unsatisfied clauses are kept in a list
|
|
386
|
+
called ``setd``, which is then used to refine the correction set
|
|
387
|
+
(see :func:`_compute`, and :func:`do_cld_check`).
|
|
388
|
+
|
|
389
|
+
Optional Boolean parameter ``update_setd`` enforces the method to
|
|
390
|
+
update variable ``self.setd``. If this parameter is set to
|
|
391
|
+
``False``, the method only updates the list of satisfied clauses,
|
|
392
|
+
which is an under-approximation of a *maximal satisfiable subset*
|
|
393
|
+
(MSS).
|
|
394
|
+
|
|
395
|
+
:param update_setd: whether or not to update setd
|
|
396
|
+
:type update_setd: bool
|
|
397
|
+
"""
|
|
398
|
+
|
|
399
|
+
model = self.oracle.get_model()
|
|
400
|
+
setd = set()
|
|
401
|
+
|
|
402
|
+
for i, cl in enumerate(self.soft):
|
|
403
|
+
if not self.satc[i]:
|
|
404
|
+
if self._satisfied(cl, model):
|
|
405
|
+
self.satc[i] = True
|
|
406
|
+
self.ss_assumps.append(self.sels[i])
|
|
407
|
+
else:
|
|
408
|
+
setd = setd.union(set(cl))
|
|
409
|
+
|
|
410
|
+
if update_setd:
|
|
411
|
+
self.setd = sorted(setd)
|
|
412
|
+
|
|
413
|
+
def _compute(self):
|
|
414
|
+
"""
|
|
415
|
+
The main method of the class, which computes an MCS given its
|
|
416
|
+
over-approximation. The over-approximation is defined by a model
|
|
417
|
+
for the hard part of the formula obtained in :func:`compute`.
|
|
418
|
+
|
|
419
|
+
The method is essentially a simple loop going over all literals
|
|
420
|
+
unsatisfied by the previous model, i.e. the literals of
|
|
421
|
+
``self.setd`` and checking which literals can be satisfied. This
|
|
422
|
+
process can be seen a refinement of the over-approximation of the
|
|
423
|
+
MCS. The algorithm follows the pseudo-code of the LBX algorithm
|
|
424
|
+
presented in [1]_.
|
|
425
|
+
|
|
426
|
+
Additionally, if :class:`LBX` was constructed with the requirement
|
|
427
|
+
to make "clause :math:`D`" calls, the method calls
|
|
428
|
+
:func:`do_cld_check` at every iteration of the loop using the
|
|
429
|
+
literals of ``self.setd`` not yet checked, as the contents of
|
|
430
|
+
"clause :math:`D`".
|
|
431
|
+
"""
|
|
432
|
+
|
|
433
|
+
# unless clause D checks are used, test one literal at a time
|
|
434
|
+
# and add it either to satisfied of backbone assumptions
|
|
435
|
+
i = 0
|
|
436
|
+
while i < len(self.setd):
|
|
437
|
+
if self.ucld:
|
|
438
|
+
self.do_cld_check(self.setd[i:])
|
|
439
|
+
i = 0
|
|
440
|
+
|
|
441
|
+
if self.setd: # if may be empty after the clause D check
|
|
442
|
+
if self.oracle.solve(assumptions=self.ss_assumps + self.bb_assumps + [self.setd[i]]):
|
|
443
|
+
# filtering satisfied clauses
|
|
444
|
+
self._filter_satisfied()
|
|
445
|
+
else:
|
|
446
|
+
# current literal is backbone
|
|
447
|
+
self.bb_assumps.append(-self.setd[i])
|
|
448
|
+
|
|
449
|
+
i += 1
|
|
450
|
+
|
|
451
|
+
def do_cld_check(self, cld):
|
|
452
|
+
"""
|
|
453
|
+
Do the "clause :math:`D`" check. This method receives a list of
|
|
454
|
+
literals, which serves a "clause :math:`D`" [2]_, and checks
|
|
455
|
+
whether the formula conjoined with :math:`D` is satisfiable.
|
|
456
|
+
|
|
457
|
+
.. [2] Joao Marques-Silva, Federico Heras, Mikolas Janota,
|
|
458
|
+
Alessandro Previti, Anton Belov. *On Computing Minimal
|
|
459
|
+
Correction Subsets*. IJCAI 2013. pp. 615-622
|
|
460
|
+
|
|
461
|
+
If clause :math:`D` cannot be satisfied together with the formula,
|
|
462
|
+
then negations of all of its literals are backbones of the formula
|
|
463
|
+
and the LBX algorithm can stop. Otherwise, the literals satisfied
|
|
464
|
+
by the new model refine the MCS further.
|
|
465
|
+
|
|
466
|
+
Every time the method is called, a new fresh selector variable
|
|
467
|
+
:math:`s` is introduced, which augments the current clause
|
|
468
|
+
:math:`D`. The SAT oracle then checks if clause :math:`(D \\vee
|
|
469
|
+
\\neg{s})` can be satisfied together with the internal formula.
|
|
470
|
+
The :math:`D` clause is then disabled by adding a hard clause
|
|
471
|
+
:math:`(\\neg{s})`.
|
|
472
|
+
|
|
473
|
+
:param cld: clause :math:`D` to check
|
|
474
|
+
:type cld: list(int)
|
|
475
|
+
"""
|
|
476
|
+
|
|
477
|
+
# adding a selector literal to clause D
|
|
478
|
+
# selector literals for clauses D currently
|
|
479
|
+
# cannot be reused, but this may change later
|
|
480
|
+
self.topv += 1
|
|
481
|
+
sel = self.topv
|
|
482
|
+
cld.append(-sel)
|
|
483
|
+
|
|
484
|
+
# adding clause D
|
|
485
|
+
self.oracle.add_clause(cld)
|
|
486
|
+
|
|
487
|
+
if self.oracle.solve(assumptions=self.ss_assumps + self.bb_assumps + [sel]):
|
|
488
|
+
# filtering satisfied
|
|
489
|
+
self._filter_satisfied(update_setd=True)
|
|
490
|
+
else:
|
|
491
|
+
# clause D is unsatisfiable => all literals are backbones
|
|
492
|
+
self.bb_assumps.extend([-l for l in cld[:-1]])
|
|
493
|
+
self.setd = []
|
|
494
|
+
|
|
495
|
+
# deactivating clause D
|
|
496
|
+
self.oracle.add_clause([-sel])
|
|
497
|
+
|
|
498
|
+
def _map_extlit(self, l):
|
|
499
|
+
"""
|
|
500
|
+
Map an external variable to an internal one if necessary.
|
|
501
|
+
|
|
502
|
+
This method is used when new clauses are added to the formula
|
|
503
|
+
incrementally, which may result in introducing new variables
|
|
504
|
+
clashing with the previously used *clause selectors*. The method
|
|
505
|
+
makes sure no clash occurs, i.e. it maps the original variables
|
|
506
|
+
used in the new problem clauses to the newly introduced auxiliary
|
|
507
|
+
variables (see :func:`add_clause`).
|
|
508
|
+
|
|
509
|
+
Given an integer literal, a fresh literal is returned. The returned
|
|
510
|
+
integer has the same sign as the input literal.
|
|
511
|
+
|
|
512
|
+
:param l: literal to map
|
|
513
|
+
:type l: int
|
|
514
|
+
|
|
515
|
+
:rtype: int
|
|
516
|
+
"""
|
|
517
|
+
|
|
518
|
+
v = abs(l)
|
|
519
|
+
|
|
520
|
+
if v in self.vmap.e2i:
|
|
521
|
+
return int(copysign(self.vmap.e2i[v], l))
|
|
522
|
+
else:
|
|
523
|
+
self.topv += 1
|
|
524
|
+
|
|
525
|
+
self.vmap.e2i[v] = self.topv
|
|
526
|
+
self.vmap.i2e[self.topv] = v
|
|
527
|
+
|
|
528
|
+
return int(copysign(self.topv, l))
|
|
529
|
+
|
|
530
|
+
def oracle_time(self):
|
|
531
|
+
"""
|
|
532
|
+
Report the total SAT solving time.
|
|
533
|
+
"""
|
|
534
|
+
|
|
535
|
+
return self.oracle.time_accum()
|
|
536
|
+
|
|
537
|
+
|
|
538
|
+
#
|
|
539
|
+
#==============================================================================
|
|
540
|
+
def parse_options():
|
|
541
|
+
"""
|
|
542
|
+
Parses command-line options.
|
|
543
|
+
"""
|
|
544
|
+
|
|
545
|
+
try:
|
|
546
|
+
opts, args = getopt.getopt(sys.argv[1:],
|
|
547
|
+
'de:hp:p:s:v',
|
|
548
|
+
['dcalls',
|
|
549
|
+
'enum=',
|
|
550
|
+
'help',
|
|
551
|
+
'process=',
|
|
552
|
+
'solver=',
|
|
553
|
+
'verbose'])
|
|
554
|
+
except getopt.GetoptError as err:
|
|
555
|
+
sys.stderr.write(str(err).capitalize() + '\n')
|
|
556
|
+
usage()
|
|
557
|
+
sys.exit(1)
|
|
558
|
+
|
|
559
|
+
dcalls = False
|
|
560
|
+
to_enum = 1
|
|
561
|
+
process = 0
|
|
562
|
+
solver = 'm22'
|
|
563
|
+
verbose = 0
|
|
564
|
+
|
|
565
|
+
for opt, arg in opts:
|
|
566
|
+
if opt in ('-d', '--dcalls'):
|
|
567
|
+
dcalls = True
|
|
568
|
+
elif opt in ('-e', '--enum'):
|
|
569
|
+
to_enum = str(arg)
|
|
570
|
+
if to_enum != 'all':
|
|
571
|
+
to_enum = int(to_enum)
|
|
572
|
+
elif opt in ('-h', '--help'):
|
|
573
|
+
usage()
|
|
574
|
+
sys.exit(0)
|
|
575
|
+
elif opt in ('-p', '--process'):
|
|
576
|
+
process = int(arg)
|
|
577
|
+
elif opt in ('-s', '--solver'):
|
|
578
|
+
solver = str(arg)
|
|
579
|
+
elif opt in ('-v', '--verbose'):
|
|
580
|
+
verbose += 1
|
|
581
|
+
else:
|
|
582
|
+
assert False, 'Unhandled option: {0} {1}'.format(opt, arg)
|
|
583
|
+
|
|
584
|
+
return dcalls, to_enum, solver, process, verbose, args
|
|
585
|
+
|
|
586
|
+
|
|
587
|
+
#
|
|
588
|
+
#==============================================================================
|
|
589
|
+
def usage():
|
|
590
|
+
"""
|
|
591
|
+
Prints help message.
|
|
592
|
+
"""
|
|
593
|
+
|
|
594
|
+
print('Usage:', os.path.basename(sys.argv[0]), '[options] file')
|
|
595
|
+
print('Options:')
|
|
596
|
+
print(' -d, --dcalls Apply clause D calls')
|
|
597
|
+
print(' -e, --enum=<string> How many solutions to compute')
|
|
598
|
+
print(' Available values: [1 .. all] (default: 1)')
|
|
599
|
+
print(' -h, --help')
|
|
600
|
+
print(' -p, --process=<int> Number of processing rounds')
|
|
601
|
+
print(' Available values: [0 .. INT_MAX] (default = 0)')
|
|
602
|
+
print(' -s, --solver SAT solver to use')
|
|
603
|
+
print(' Available values: cd15, cd19, g3, g4, lgl, mcb, mcm, mpl, m22, mc, mgh (default = m22)')
|
|
604
|
+
print(' -v, --verbose Be verbose')
|
|
605
|
+
|
|
606
|
+
|
|
607
|
+
#
|
|
608
|
+
#==============================================================================
|
|
609
|
+
if __name__ == '__main__':
|
|
610
|
+
dcalls, to_enum, solver, process, verbose, files = parse_options()
|
|
611
|
+
|
|
612
|
+
if type(to_enum) == str:
|
|
613
|
+
to_enum = 0
|
|
614
|
+
|
|
615
|
+
if files:
|
|
616
|
+
# reading standard CNF, WCNF, or (W)CNF+
|
|
617
|
+
assert re.search(r'cnf[p|+]?(\.(gz|bz2|lzma|xz|zst))?$', files[0]), 'Unknown input file extension'
|
|
618
|
+
if re.search(r'\.wcnf[p|+]?(\.(gz|bz2|lzma|xz|zst))?$', files[0]):
|
|
619
|
+
formula = WCNFPlus(from_file=files[0])
|
|
620
|
+
else: # expecting '*.cnf[,p,+].*'
|
|
621
|
+
formula = CNFPlus(from_file=files[0]).weighted()
|
|
622
|
+
|
|
623
|
+
with LBX(formula, use_cld=dcalls, solver_name=solver, process=process,
|
|
624
|
+
use_timer=True) as mcsls:
|
|
625
|
+
for i, mcs in enumerate(mcsls.enumerate()):
|
|
626
|
+
if verbose:
|
|
627
|
+
print('c MCS:', ' '.join([str(cl_id) for cl_id in mcs]), '0')
|
|
628
|
+
|
|
629
|
+
if verbose > 1:
|
|
630
|
+
cost = sum([formula.wght[cl_id - 1] for cl_id in mcs])
|
|
631
|
+
print('c cost:', cost)
|
|
632
|
+
|
|
633
|
+
if to_enum and i + 1 == to_enum:
|
|
634
|
+
break
|
|
635
|
+
|
|
636
|
+
mcsls.block(mcs)
|
|
637
|
+
|
|
638
|
+
print('c oracle time: {0:.4f}'.format(mcsls.oracle_time()))
|