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
pysat/examples/optux.py
ADDED
|
@@ -0,0 +1,710 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
#-*- coding:utf-8 -*-
|
|
3
|
+
##
|
|
4
|
+
## optux.py
|
|
5
|
+
##
|
|
6
|
+
## Created on: Mar 22, 2021
|
|
7
|
+
## Author: Alexey Ignatiev
|
|
8
|
+
## E-mail: alexey.ignatiev@monash.edu
|
|
9
|
+
##
|
|
10
|
+
|
|
11
|
+
"""
|
|
12
|
+
===============
|
|
13
|
+
List of classes
|
|
14
|
+
===============
|
|
15
|
+
|
|
16
|
+
.. autosummary::
|
|
17
|
+
:nosignatures:
|
|
18
|
+
|
|
19
|
+
OptUx
|
|
20
|
+
|
|
21
|
+
==================
|
|
22
|
+
Module description
|
|
23
|
+
==================
|
|
24
|
+
|
|
25
|
+
An implementation of an extractor of a smallest size minimal unsatisfiable
|
|
26
|
+
subset (smallest MUS, or SMUS) [1]_ [2]_ [3]_ [4]_ and enumerator of
|
|
27
|
+
SMUSes based on *implicit hitting set enumeration* [1]_. This
|
|
28
|
+
implementation tries to replicate the well-known SMUS extractor Forqes
|
|
29
|
+
[1]_. In contrast to Forqes, this implementation supports not only plain
|
|
30
|
+
DIMACS :class:`.CNF` formulas but also weighted :class:`.WCNF` formulas.
|
|
31
|
+
As a result, the tool is able to compute and enumerate *optimal* MUSes in
|
|
32
|
+
case of weighted formulas. On the other hand, this prototype lacks a
|
|
33
|
+
number of command-line options used in Forqes and so it may be less
|
|
34
|
+
efficient compared to Forqes but the performance difference should not be
|
|
35
|
+
significant.
|
|
36
|
+
|
|
37
|
+
.. [1] Alexey Ignatiev, Alessandro Previti, Mark H. Liffiton, Joao
|
|
38
|
+
Marques-Silva. *Smallest MUS Extraction with Minimal Hitting Set
|
|
39
|
+
Dualization*. CP 2015. pp. 173-182
|
|
40
|
+
|
|
41
|
+
.. [2] Mark H. Liffiton, Maher N. Mneimneh, Ines Lynce, Zaher S. Andraus,
|
|
42
|
+
Joao Marques-Silva, Karem A. Sakallah. *A branch and bound algorithm
|
|
43
|
+
for extracting smallest minimal unsatisfiable subformulas*.
|
|
44
|
+
Constraints An Int. J. 14(4). 2009. pp. 415-442
|
|
45
|
+
|
|
46
|
+
.. [3] Alexey Ignatiev, Mikolas Janota, Joao Marques-Silva. *Quantified
|
|
47
|
+
Maximum Satisfiability: A Core-Guided Approach*. SAT 2013.
|
|
48
|
+
pp. 250-266
|
|
49
|
+
|
|
50
|
+
.. [4] Alexey Ignatiev, Mikolas Janota, Joao Marques-Silva. *Quantified
|
|
51
|
+
maximum satisfiability*. Constraints An Int. J. 21(2). 2016.
|
|
52
|
+
pp. 277-302
|
|
53
|
+
|
|
54
|
+
The file provides a class :class:`OptUx`, which is the basic
|
|
55
|
+
implementation of the algorithm. It can be applied to any formula in the
|
|
56
|
+
:class:`.CNF` or :class:`.WCNF` format.
|
|
57
|
+
|
|
58
|
+
The implementation can be used as an executable (the list of available
|
|
59
|
+
command-line options can be shown using ``optux.py -h``) in the following
|
|
60
|
+
way:
|
|
61
|
+
|
|
62
|
+
::
|
|
63
|
+
|
|
64
|
+
$ xzcat formula.wcnf.xz
|
|
65
|
+
p wcnf 3 6 4
|
|
66
|
+
1 1 0
|
|
67
|
+
1 2 0
|
|
68
|
+
1 3 0
|
|
69
|
+
4 -1 -2 0
|
|
70
|
+
4 -1 -3 0
|
|
71
|
+
4 -2 -3 0
|
|
72
|
+
|
|
73
|
+
$ optux.py -vvv formula.wcnf.xz
|
|
74
|
+
c mcs: 1 2 0
|
|
75
|
+
c mcses: 0 unit, 1 disj
|
|
76
|
+
c mus: 1 2 0
|
|
77
|
+
c cost: 2
|
|
78
|
+
c oracle time: 0.0001
|
|
79
|
+
|
|
80
|
+
Alternatively, the algorithm can be accessed and invoked through the
|
|
81
|
+
standard ``import`` interface of Python, e.g.
|
|
82
|
+
|
|
83
|
+
.. code-block:: python
|
|
84
|
+
|
|
85
|
+
>>> from pysat.examples.optux import OptUx
|
|
86
|
+
>>> from pysat.formula import WCNF
|
|
87
|
+
>>>
|
|
88
|
+
>>> wcnf = WCNF(from_file='formula.wcnf.xz')
|
|
89
|
+
>>>
|
|
90
|
+
>>> with OptUx(wcnf) as optux:
|
|
91
|
+
... for mus in optux.enumerate():
|
|
92
|
+
... print('mus {0} has cost {1}'.format(mus, optux.cost))
|
|
93
|
+
mus [1, 2] has cost 2
|
|
94
|
+
mus [1, 3] has cost 2
|
|
95
|
+
mus [2, 3] has cost 2
|
|
96
|
+
|
|
97
|
+
As can be seen in the example above, the solver can be instructed either
|
|
98
|
+
to compute one optimal MUS of an input formula, or to enumerate a given
|
|
99
|
+
number (or *all*) of its top optimal MUSes.
|
|
100
|
+
|
|
101
|
+
==============
|
|
102
|
+
Module details
|
|
103
|
+
==============
|
|
104
|
+
"""
|
|
105
|
+
|
|
106
|
+
#
|
|
107
|
+
#==============================================================================
|
|
108
|
+
from __future__ import print_function
|
|
109
|
+
import getopt
|
|
110
|
+
import os
|
|
111
|
+
from pysat.examples.hitman import Atom, Hitman
|
|
112
|
+
from pysat.examples.rc2 import RC2
|
|
113
|
+
from pysat.formula import CNFPlus, WCNFPlus
|
|
114
|
+
from pysat.process import Processor
|
|
115
|
+
from pysat.solvers import Solver, SolverNames
|
|
116
|
+
import re
|
|
117
|
+
import sys
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
#
|
|
121
|
+
#==============================================================================
|
|
122
|
+
class OptUx(object):
|
|
123
|
+
"""
|
|
124
|
+
A simple Python version of the implicit hitting set based optimal MUS
|
|
125
|
+
extractor and enumerator. Given a (weighted) (partial) CNF formula,
|
|
126
|
+
i.e. formula in the :class:`.WCNF` format, this class can be used to
|
|
127
|
+
compute a given number of optimal MUS (starting from the *best* one)
|
|
128
|
+
of the input formula. :class:`OptUx` roughly follows the
|
|
129
|
+
implementation of Forqes [1]_ but lacks a few additional heuristics,
|
|
130
|
+
which however aren't applied in Forqes by default.
|
|
131
|
+
|
|
132
|
+
As a result, OptUx applies exhaustive *disjoint* minimal correction
|
|
133
|
+
subset (MCS) enumeration [1]_, [2]_, [3]_, [4]_ with the incremental
|
|
134
|
+
use of RC2 [5]_ as an underlying MaxSAT solver. Disjoint MCS
|
|
135
|
+
enumeration is run only if the corresponding input parameter
|
|
136
|
+
``nodisj`` is set to ``False``. Once disjoint MCSes are enumerated,
|
|
137
|
+
they are used to bootstrap a hitting set solver. This implementation
|
|
138
|
+
uses :class:`.Hitman` as a hitting set solver, which is again based on
|
|
139
|
+
RC2.
|
|
140
|
+
|
|
141
|
+
Note that in the main implicit hitting enumeration loop of the
|
|
142
|
+
algorithm, OptUx follows Forqes in that it does not reduce correction
|
|
143
|
+
subsets detected to minimal correction subsets. As a result,
|
|
144
|
+
correction subsets computed in the main loop are added to
|
|
145
|
+
:class:`Hitman` *unreduced*.
|
|
146
|
+
|
|
147
|
+
:class:`OptUx` can use any SAT solver available in PySAT. The default
|
|
148
|
+
SAT solver to use is ``g3``, which stands for Glucose 3 [6]_ (see
|
|
149
|
+
:class:`.SolverNames`). Boolean parameters ``adapt``, ``exhaust``, and
|
|
150
|
+
``minz`` control whether or not the underlying :class:`.RC2` oracles
|
|
151
|
+
should apply detection and adaptation of intrinsic AtMost1
|
|
152
|
+
constraints, core exhaustion, and core reduction. Also, unsatisfiable
|
|
153
|
+
cores can be trimmed if the ``trim`` parameter is set to a non-zero
|
|
154
|
+
integer. Finally, verbosity level can be set using the ``verbose``
|
|
155
|
+
parameter.
|
|
156
|
+
|
|
157
|
+
Two additional optional parameters ``unsorted`` and ``dcalls`` can be
|
|
158
|
+
used to instruct the tool to enumerate MUSes in the unsorted fashion,
|
|
159
|
+
i.e. optimal MUSes are not guaranteed to go first. For this,
|
|
160
|
+
:class:`OptUx` applies LBX-like MCS enumeration (it uses :class:`.LBX`
|
|
161
|
+
directly). Parameter ``dcalls`` can be applied to instruct the
|
|
162
|
+
underlying MCS enumerator to apply clause D oracle calls.
|
|
163
|
+
|
|
164
|
+
Another optional paramater ``puresat`` can be used to instruct OptUx
|
|
165
|
+
to run a purely SAT-based minimal hitting set enumerator, following
|
|
166
|
+
the ideas of [7]_. The value of ``puresat`` can be either ``False``,
|
|
167
|
+
meaning that no pure SAT enumeration is to be done or be equal to
|
|
168
|
+
``'mgh'``, ``'cd15'``, or ``'lgl'`` - these are the solvers that
|
|
169
|
+
support *hard* phase setting, i.e. user preferences will not be
|
|
170
|
+
overwritten by the *phase saving* heuristic [8]_.
|
|
171
|
+
|
|
172
|
+
Additionally, the input formula can be preprocessed before running MUS
|
|
173
|
+
enumeration. This is controlled by the input parameter ``process``
|
|
174
|
+
whose integer value signifies the number of processing rounds to be
|
|
175
|
+
applied. The number of rounds is set to 0 by default.
|
|
176
|
+
|
|
177
|
+
Finally, one more optional input parameter ``cover`` is to be used
|
|
178
|
+
when exhaustive enumeration of MUSes is not necessary and the tool can
|
|
179
|
+
stop as soon as a given formula is covered by the set of currently
|
|
180
|
+
computed MUSes. This can be made to work if the soft clauses of
|
|
181
|
+
``formula`` are of size 1.
|
|
182
|
+
|
|
183
|
+
.. [5] Alexey Ignatiev, Antonio Morgado, Joao Marques-Silva. *RC2: an
|
|
184
|
+
Efficient MaxSAT Solver*. J. Satisf. Boolean Model. Comput. 11(1).
|
|
185
|
+
2019. pp. 53-64
|
|
186
|
+
|
|
187
|
+
.. [6] Gilles Audemard, Jean-Marie Lagniez, Laurent Simon.
|
|
188
|
+
*Improving Glucose for Incremental SAT Solving with
|
|
189
|
+
Assumptions: Application to MUS Extraction*. SAT 2013.
|
|
190
|
+
pp. 309-317
|
|
191
|
+
|
|
192
|
+
.. [7] Enrico Giunchiglia, Marco Maratea. *Solving Optimization
|
|
193
|
+
Problems with DLL*. ECAI 2006. pp. 377-381
|
|
194
|
+
|
|
195
|
+
.. [8] Knot Pipatsrisawat, Adnan Darwiche. *A Lightweight Component
|
|
196
|
+
Caching Scheme for Satisfiability Solvers*. SAT 2007. pp. 294-299
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
:param formula: (weighted) (partial) CNFPlus formula
|
|
200
|
+
:param solver: SAT oracle name
|
|
201
|
+
:param adapt: detect and adapt intrinsic AtMost1 constraints
|
|
202
|
+
:param cover: CNFPlus formula to cover when doing MUS enumeration
|
|
203
|
+
:param dcalls: apply clause D oracle calls (for unsorted enumeration only)
|
|
204
|
+
:param exhaust: do core exhaustion
|
|
205
|
+
:param minz: do heuristic core reduction
|
|
206
|
+
:param nodisj: do not enumerate disjoint MCSes
|
|
207
|
+
:param process: apply formula preprocessing this many times
|
|
208
|
+
:param puresat: use pure SAT-based hitting set enumeration
|
|
209
|
+
:param unsorted: apply unsorted MUS enumeration
|
|
210
|
+
:param trim: do core trimming at most this number of times
|
|
211
|
+
:param verbose: verbosity level
|
|
212
|
+
|
|
213
|
+
:type formula: :class:`.WCNFPlus`
|
|
214
|
+
:type solver: str
|
|
215
|
+
:type adapt: bool
|
|
216
|
+
:type cover: :class:`.CNFPlus`
|
|
217
|
+
:type dcalls: bool
|
|
218
|
+
:type exhaust: bool
|
|
219
|
+
:type minz: bool
|
|
220
|
+
:type nodisj: bool
|
|
221
|
+
:type process: int
|
|
222
|
+
:type puresat: str
|
|
223
|
+
:type unsorted: bool
|
|
224
|
+
:type trim: int
|
|
225
|
+
:type verbose: int
|
|
226
|
+
"""
|
|
227
|
+
|
|
228
|
+
def __init__(self, formula, solver='g3', adapt=False, cover=None,
|
|
229
|
+
dcalls=False, exhaust=False, minz=False, nodisj=False, process=0,
|
|
230
|
+
puresat=False, unsorted=False, trim=False, verbose=0):
|
|
231
|
+
"""
|
|
232
|
+
Constructor.
|
|
233
|
+
"""
|
|
234
|
+
|
|
235
|
+
assert not puresat or unsorted, '\'unsorted\' needs to be True for pure SAT mode'
|
|
236
|
+
|
|
237
|
+
# verbosity level
|
|
238
|
+
self.verbose = verbose
|
|
239
|
+
|
|
240
|
+
# constructing a local copy of the formula
|
|
241
|
+
self.formula = WCNFPlus()
|
|
242
|
+
self.formula.hard = formula.hard[:]
|
|
243
|
+
self.formula.wght = formula.wght[:]
|
|
244
|
+
self.formula.topw = formula.topw
|
|
245
|
+
self.formula.nv = formula.nv
|
|
246
|
+
|
|
247
|
+
# copying atmost constraints, if any
|
|
248
|
+
if isinstance(formula, WCNFPlus) and formula.atms:
|
|
249
|
+
self.formula.atms = formula.atms[:]
|
|
250
|
+
|
|
251
|
+
# top variable identifier
|
|
252
|
+
self.topv = formula.nv
|
|
253
|
+
|
|
254
|
+
# processing soft clauses
|
|
255
|
+
self._process_soft(formula)
|
|
256
|
+
self.formula.nv = self.topv
|
|
257
|
+
|
|
258
|
+
# applying formula processing (if any)
|
|
259
|
+
if process:
|
|
260
|
+
# the processor is immediately destroyed,
|
|
261
|
+
# as we do not need to restore the models
|
|
262
|
+
with Processor(bootstrap_with=self.formula.hard) as processor:
|
|
263
|
+
proc = processor.process(rounds=process, freeze=self.sels)
|
|
264
|
+
self.formula.hard = proc.clauses
|
|
265
|
+
self.formula.nv = max(self.formula.nv, proc.nv)
|
|
266
|
+
|
|
267
|
+
# creating an unweighted copy
|
|
268
|
+
unweighted = self.formula.copy()
|
|
269
|
+
unweighted.wght = [1 for w in unweighted.wght]
|
|
270
|
+
|
|
271
|
+
if not nodisj:
|
|
272
|
+
# enumerating disjoint MCSes (including unit-size MCSes)
|
|
273
|
+
to_hit, self.units = self._disjoint(unweighted, solver, adapt, exhaust,
|
|
274
|
+
minz, trim)
|
|
275
|
+
else:
|
|
276
|
+
to_hit, self.units, self.disj_time = [], [], 0.
|
|
277
|
+
|
|
278
|
+
if self.verbose > 2:
|
|
279
|
+
print('c mcses: {0} unit, {1} disj'.format(len(self.units),
|
|
280
|
+
len(to_hit) + len(self.units)))
|
|
281
|
+
|
|
282
|
+
if not unsorted:
|
|
283
|
+
# MaxSAT-based hitting set enumerator
|
|
284
|
+
self.hitman = Hitman(bootstrap_with=to_hit, weights=self.weights,
|
|
285
|
+
solver=solver, htype='sorted', mxs_adapt=adapt,
|
|
286
|
+
mxs_exhaust=exhaust, mxs_minz=minz, mxs_trim=trim)
|
|
287
|
+
elif not puresat:
|
|
288
|
+
# MCS-based hitting set enumerator
|
|
289
|
+
self.hitman = Hitman(bootstrap_with=to_hit, weights=self.weights,
|
|
290
|
+
solver=solver, htype='lbx', mcs_usecld=dcalls)
|
|
291
|
+
else:
|
|
292
|
+
# pure SAT-based hitting set enumerator with preferred phases
|
|
293
|
+
self.hitman = Hitman(bootstrap_with=to_hit, weights=self.weights,
|
|
294
|
+
solver=puresat, htype='sat')
|
|
295
|
+
|
|
296
|
+
# adding the formula to cover to the hitting set enumerator
|
|
297
|
+
self.cover = cover is not None
|
|
298
|
+
if cover:
|
|
299
|
+
# mapping literals to Hitman's atoms
|
|
300
|
+
m = lambda l: Atom(l, sign=True) if -l not in self.weights else Atom(-l, sign=False)
|
|
301
|
+
|
|
302
|
+
for cl in cover:
|
|
303
|
+
if len(cl) != 2 or not type(cl[0]) in (list, tuple, set):
|
|
304
|
+
cl = [m(l) for l in cl]
|
|
305
|
+
else:
|
|
306
|
+
cl = [[m(l) for l in cl[0]], cl[1]]
|
|
307
|
+
|
|
308
|
+
self.hitman.add_hard(cl, weights=self.weights)
|
|
309
|
+
|
|
310
|
+
# SAT oracle bootstrapped with the hard clauses; note that
|
|
311
|
+
# clauses of the unit-size MCSes are enforced to be enabled
|
|
312
|
+
self.oracle = Solver(name=solver, bootstrap_with=unweighted.hard +
|
|
313
|
+
[[mcs] for mcs in self.units], use_timer=True)
|
|
314
|
+
|
|
315
|
+
if unweighted.atms:
|
|
316
|
+
if solver in SolverNames.cadical195:
|
|
317
|
+
# we are using CaDiCaL195 and it can use external linear engine
|
|
318
|
+
self.oracle.activate_atmost()
|
|
319
|
+
|
|
320
|
+
assert self.oracle.supports_atmost(), \
|
|
321
|
+
'{0} does not support native cardinality constraints. Make sure you use the right type of formula.'.format(self.solver)
|
|
322
|
+
|
|
323
|
+
for atm in unweighted.atms:
|
|
324
|
+
self.oracle.add_atmost(*atm)
|
|
325
|
+
|
|
326
|
+
def __del__(self):
|
|
327
|
+
"""
|
|
328
|
+
Destructor.
|
|
329
|
+
"""
|
|
330
|
+
|
|
331
|
+
self.delete()
|
|
332
|
+
|
|
333
|
+
def __enter__(self):
|
|
334
|
+
"""
|
|
335
|
+
'with' constructor.
|
|
336
|
+
"""
|
|
337
|
+
|
|
338
|
+
return self
|
|
339
|
+
|
|
340
|
+
def __exit__(self, exc_type, exc_value, traceback):
|
|
341
|
+
"""
|
|
342
|
+
'with' destructor.
|
|
343
|
+
"""
|
|
344
|
+
|
|
345
|
+
self.delete()
|
|
346
|
+
|
|
347
|
+
def delete(self):
|
|
348
|
+
"""
|
|
349
|
+
Explicit destructor of the internal hitting set and SAT oracles.
|
|
350
|
+
"""
|
|
351
|
+
|
|
352
|
+
if self.hitman:
|
|
353
|
+
self.hitman.delete()
|
|
354
|
+
self.hitman = None
|
|
355
|
+
|
|
356
|
+
if self.oracle:
|
|
357
|
+
self.oracle.delete()
|
|
358
|
+
self.oracle = None
|
|
359
|
+
|
|
360
|
+
def _process_soft(self, formula):
|
|
361
|
+
"""
|
|
362
|
+
The method is for processing the soft clauses of the input
|
|
363
|
+
formula. Concretely, it checks which soft clauses must be relaxed
|
|
364
|
+
by a unique selector literal and applies the relaxation.
|
|
365
|
+
|
|
366
|
+
:param formula: input formula
|
|
367
|
+
:type formula: :class:`.WCNF`
|
|
368
|
+
"""
|
|
369
|
+
|
|
370
|
+
# list of selectors
|
|
371
|
+
self.sels = []
|
|
372
|
+
|
|
373
|
+
# mapping from selectors to clause ids
|
|
374
|
+
self.smap = {}
|
|
375
|
+
|
|
376
|
+
# duplicate unit clauses
|
|
377
|
+
processed_dups = set()
|
|
378
|
+
|
|
379
|
+
# processing the soft clauses
|
|
380
|
+
for cl in formula.soft:
|
|
381
|
+
# if the clause is unit-size, its sole literal acts a selector
|
|
382
|
+
selv = cl[0]
|
|
383
|
+
|
|
384
|
+
# if clause is not unit, we relax it
|
|
385
|
+
if len(cl) > 1:
|
|
386
|
+
self.topv += 1
|
|
387
|
+
selv = self.topv
|
|
388
|
+
self.formula.hard.append(cl + [-selv])
|
|
389
|
+
elif selv in self.smap:
|
|
390
|
+
# the clause is unit but a there is a previously seen
|
|
391
|
+
# duplicate of this clause; this means we have to
|
|
392
|
+
# reprocess the previous clause again and relax it
|
|
393
|
+
if selv not in processed_dups:
|
|
394
|
+
self.topv += 1
|
|
395
|
+
nsel = self.topv
|
|
396
|
+
self.sels[self.smap[selv] - 1] = nsel
|
|
397
|
+
self.formula.hard.append(self.formula.soft[self.smap[selv] - 1] + [-nsel])
|
|
398
|
+
self.formula.soft[self.smap[selv] - 1] = [nsel]
|
|
399
|
+
self.smap[nsel] = self.smap[selv]
|
|
400
|
+
processed_dups.add(selv)
|
|
401
|
+
|
|
402
|
+
# processing the current clause
|
|
403
|
+
self.topv += 1
|
|
404
|
+
selv = self.topv
|
|
405
|
+
self.formula.hard.append(cl + [-selv])
|
|
406
|
+
|
|
407
|
+
self.sels.append(selv)
|
|
408
|
+
self.formula.soft.append([selv])
|
|
409
|
+
self.smap[selv] = len(self.sels)
|
|
410
|
+
|
|
411
|
+
# garbage-collecting the duplicates
|
|
412
|
+
for selv in processed_dups:
|
|
413
|
+
del self.smap[selv]
|
|
414
|
+
|
|
415
|
+
# these numbers should be equal after the processing
|
|
416
|
+
assert len(self.sels) == len(self.smap) == len(self.formula.wght)
|
|
417
|
+
|
|
418
|
+
# creating a dictionary of weights
|
|
419
|
+
self.weights = {l: w for l, w in zip(self.sels, self.formula.wght)}
|
|
420
|
+
|
|
421
|
+
def _disjoint(self, formula, solver, adapt, exhaust, minz, trim):
|
|
422
|
+
"""
|
|
423
|
+
This method constitutes the preliminary step of the implicit
|
|
424
|
+
hitting set paradigm of Forqes. Namely, it enumerates all the
|
|
425
|
+
disjoint *minimal correction subsets* (MCSes) of the formula,
|
|
426
|
+
which will be later used to bootstrap the hitting set solver.
|
|
427
|
+
|
|
428
|
+
Note that the MaxSAT solver in use is :class:`.RC2`. As a result,
|
|
429
|
+
all the input parameters of the method, namely, ``formula``,
|
|
430
|
+
``solver``, ``adapt``, `exhaust``, ``minz``, and ``trim`` -
|
|
431
|
+
represent the input and the options for the RC2 solver.
|
|
432
|
+
|
|
433
|
+
:param formula: input formula
|
|
434
|
+
:param solver: SAT solver name
|
|
435
|
+
:param adapt: detect and adapt AtMost1 constraints
|
|
436
|
+
:param exhaust: exhaust unsatisfiable cores
|
|
437
|
+
:param minz: apply heuristic core minimization
|
|
438
|
+
:param trim: trim unsatisfiable cores at most this number of times
|
|
439
|
+
|
|
440
|
+
:type formula: :class:`.WCNF`
|
|
441
|
+
:type solver: str
|
|
442
|
+
:type adapt: bool
|
|
443
|
+
:type exhaust: bool
|
|
444
|
+
:type minz: bool
|
|
445
|
+
:type trim: int
|
|
446
|
+
"""
|
|
447
|
+
|
|
448
|
+
# these will store disjoint MCSes
|
|
449
|
+
# (unit-size MCSes are stored separately)
|
|
450
|
+
to_hit, units = [], []
|
|
451
|
+
|
|
452
|
+
with RC2(formula, solver=solver, adapt=adapt, exhaust=exhaust,
|
|
453
|
+
minz=minz, trim=trim, verbose=0) as oracle:
|
|
454
|
+
|
|
455
|
+
# iterating over MaxSAT solutions
|
|
456
|
+
while True:
|
|
457
|
+
# a new MaxSAT model
|
|
458
|
+
model = oracle.compute()
|
|
459
|
+
|
|
460
|
+
if model is None:
|
|
461
|
+
# no model => no more disjoint MCSes
|
|
462
|
+
break
|
|
463
|
+
|
|
464
|
+
# extracting the MCS corresponding to the model
|
|
465
|
+
falsified = [l for l in self.sels if model[abs(l) - 1] == -l]
|
|
466
|
+
|
|
467
|
+
# unit size or not?
|
|
468
|
+
if len(falsified) > 1:
|
|
469
|
+
to_hit.append(falsified)
|
|
470
|
+
else:
|
|
471
|
+
units.append(falsified[0])
|
|
472
|
+
|
|
473
|
+
# blocking the MCS;
|
|
474
|
+
# next time, all these clauses will be satisfied
|
|
475
|
+
for l in falsified:
|
|
476
|
+
oracle.add_clause([l])
|
|
477
|
+
|
|
478
|
+
# reporting the MCS
|
|
479
|
+
if self.verbose > 3:
|
|
480
|
+
print('c mcs: {0} 0'.format(' '.join([str(self.smap[s]) for s in falsified])))
|
|
481
|
+
|
|
482
|
+
# RC2 will be destroyed next; let's keep the oracle time
|
|
483
|
+
self.disj_time = oracle.oracle_time()
|
|
484
|
+
|
|
485
|
+
return to_hit, units
|
|
486
|
+
|
|
487
|
+
def compute(self):
|
|
488
|
+
"""
|
|
489
|
+
This method implements the main look of the implicit hitting set
|
|
490
|
+
paradigm of Forqes to compute a best-cost MUS. The result MUS is
|
|
491
|
+
returned as a list of integers, each representing a soft clause
|
|
492
|
+
index.
|
|
493
|
+
|
|
494
|
+
:rtype: list(int)
|
|
495
|
+
"""
|
|
496
|
+
|
|
497
|
+
# correctly computed cost of the unit-mcs component
|
|
498
|
+
units_cost = sum(map(lambda l: self.weights[l], self.units))
|
|
499
|
+
|
|
500
|
+
while True:
|
|
501
|
+
# computing a new optimal hitting set
|
|
502
|
+
hs = self.hitman.get()
|
|
503
|
+
|
|
504
|
+
if hs is None:
|
|
505
|
+
# no more hitting sets exist
|
|
506
|
+
break
|
|
507
|
+
|
|
508
|
+
# setting all the selector polarities to true
|
|
509
|
+
self.oracle.set_phases(self.sels)
|
|
510
|
+
|
|
511
|
+
# testing satisfiability of the {self.units + hs} subset
|
|
512
|
+
res = self.oracle.solve(assumptions=hs)
|
|
513
|
+
|
|
514
|
+
if res == False:
|
|
515
|
+
# the candidate subset of clauses is unsatisfiable,
|
|
516
|
+
# i.e. it is an optimal MUS we are searching for;
|
|
517
|
+
# therefore, blocking it and returning
|
|
518
|
+
self.hitman.block(hs)
|
|
519
|
+
self.cost = sum(map(lambda l: self.weights[l], hs)) + units_cost
|
|
520
|
+
|
|
521
|
+
if self.units and self.cover:
|
|
522
|
+
# due to additional constraints to cover and unit mcses,
|
|
523
|
+
# there may be duplicate clauses in self.units + hs
|
|
524
|
+
return sorted(map(lambda s: self.smap[s], sorted(set(self.units + hs))))
|
|
525
|
+
else:
|
|
526
|
+
return sorted(map(lambda s: self.smap[s], self.units + hs))
|
|
527
|
+
else:
|
|
528
|
+
# the candidate subset is satisfiable,
|
|
529
|
+
# thus extracting a correction subset
|
|
530
|
+
model = self.oracle.get_model()
|
|
531
|
+
cs = [l for l in self.sels if model[abs(l) - 1] == -l]
|
|
532
|
+
|
|
533
|
+
# hitting the new correction subset
|
|
534
|
+
self.hitman.hit(cs, weights=self.weights)
|
|
535
|
+
|
|
536
|
+
def enumerate(self):
|
|
537
|
+
"""
|
|
538
|
+
This is generator method iterating through MUSes and enumerating
|
|
539
|
+
them until the formula has no more MUSes, or a user decides to
|
|
540
|
+
stop the process.
|
|
541
|
+
|
|
542
|
+
:rtype: list(int)
|
|
543
|
+
"""
|
|
544
|
+
|
|
545
|
+
done = False
|
|
546
|
+
|
|
547
|
+
while not done:
|
|
548
|
+
mus = self.compute()
|
|
549
|
+
|
|
550
|
+
if mus != None:
|
|
551
|
+
yield mus
|
|
552
|
+
else:
|
|
553
|
+
done = True
|
|
554
|
+
|
|
555
|
+
def oracle_time(self):
|
|
556
|
+
"""
|
|
557
|
+
This method computes and returns the total SAT solving time
|
|
558
|
+
involved.
|
|
559
|
+
|
|
560
|
+
:rtype: float
|
|
561
|
+
"""
|
|
562
|
+
|
|
563
|
+
return self.disj_time + self.hitman.oracle_time() + self.oracle.time_accum()
|
|
564
|
+
|
|
565
|
+
|
|
566
|
+
#
|
|
567
|
+
#==============================================================================
|
|
568
|
+
def parse_options():
|
|
569
|
+
"""
|
|
570
|
+
Parses command-line options.
|
|
571
|
+
"""
|
|
572
|
+
|
|
573
|
+
try:
|
|
574
|
+
opts, args = getopt.getopt(sys.argv[1:], 'ac:de:hmnp:P:s:t:uvx',
|
|
575
|
+
['adapt', 'cover=', 'dcalls', 'enum=',
|
|
576
|
+
'exhaust', 'help', 'minimize', 'no-disj',
|
|
577
|
+
'solver=', 'puresat=', 'process=',
|
|
578
|
+
'unsorted', 'trim=', 'verbose'])
|
|
579
|
+
except getopt.GetoptError as err:
|
|
580
|
+
sys.stderr.write(str(err).capitalize() + '\n')
|
|
581
|
+
usage()
|
|
582
|
+
sys.exit(1)
|
|
583
|
+
|
|
584
|
+
adapt = False
|
|
585
|
+
cover = None
|
|
586
|
+
dcalls = False
|
|
587
|
+
exhaust = False
|
|
588
|
+
minz = False
|
|
589
|
+
no_disj = False
|
|
590
|
+
to_enum = 1
|
|
591
|
+
solver = 'g3'
|
|
592
|
+
process = 0
|
|
593
|
+
puresat = False
|
|
594
|
+
unsorted = False
|
|
595
|
+
trim = 0
|
|
596
|
+
verbose = 1
|
|
597
|
+
|
|
598
|
+
for opt, arg in opts:
|
|
599
|
+
if opt in ('-a', '--adapt'):
|
|
600
|
+
adapt = True
|
|
601
|
+
elif opt in ('-c', '--cover'):
|
|
602
|
+
cover = str(arg)
|
|
603
|
+
if cover.lower() == 'none':
|
|
604
|
+
cover = None
|
|
605
|
+
elif opt in ('-d', '--dcalls'):
|
|
606
|
+
dcalls = True
|
|
607
|
+
elif opt in ('-e', '--enum'):
|
|
608
|
+
to_enum = str(arg)
|
|
609
|
+
if to_enum != 'all':
|
|
610
|
+
to_enum = int(to_enum)
|
|
611
|
+
elif opt in ('-h', '--help'):
|
|
612
|
+
usage()
|
|
613
|
+
sys.exit(0)
|
|
614
|
+
elif opt in ('-m', '--minimize'):
|
|
615
|
+
minz = True
|
|
616
|
+
elif opt in ('-n', '--no-disj'):
|
|
617
|
+
no_disj = True
|
|
618
|
+
elif opt in ('-p', '--puresat'):
|
|
619
|
+
puresat = str(arg)
|
|
620
|
+
elif opt in ('-P', '--process'):
|
|
621
|
+
process = int(arg)
|
|
622
|
+
elif opt in ('-s', '--solver'):
|
|
623
|
+
solver = str(arg)
|
|
624
|
+
elif opt in ('-u', '--unsorted'):
|
|
625
|
+
unsorted = True
|
|
626
|
+
elif opt in ('-t', '--trim'):
|
|
627
|
+
trim = int(arg)
|
|
628
|
+
elif opt in ('-v', '--verbose'):
|
|
629
|
+
verbose += 1
|
|
630
|
+
elif opt in ('-x', '--exhaust'):
|
|
631
|
+
exhaust = True
|
|
632
|
+
else:
|
|
633
|
+
assert False, 'Unhandled option: {0} {1}'.format(opt, arg)
|
|
634
|
+
|
|
635
|
+
return adapt, cover, dcalls, exhaust, minz, no_disj, trim, to_enum, \
|
|
636
|
+
solver, process, puresat, unsorted, verbose, args
|
|
637
|
+
|
|
638
|
+
|
|
639
|
+
#
|
|
640
|
+
#==============================================================================
|
|
641
|
+
def usage():
|
|
642
|
+
"""
|
|
643
|
+
Prints help message.
|
|
644
|
+
"""
|
|
645
|
+
|
|
646
|
+
print('Usage:', os.path.basename(sys.argv[0]), '[options] file')
|
|
647
|
+
print('Options:')
|
|
648
|
+
print(' -a, --adapt Try to adapt (simplify) input formula')
|
|
649
|
+
print(' -c, --cover=<string> Stop MUS enumeration as soon as this formula is covered')
|
|
650
|
+
print(' Available values: any valid file path, none (default = none)')
|
|
651
|
+
print(' -d, --dcalls Apply clause D calls (in unsorted enumeration only)')
|
|
652
|
+
print(' -e, --enum=<string> Enumerate top-k solutions')
|
|
653
|
+
print(' Available values: [1 .. INT_MAX], all (default: 1)')
|
|
654
|
+
print(' -h, --help Show this message')
|
|
655
|
+
print(' -m, --minimize Use a heuristic unsatisfiable core minimizer')
|
|
656
|
+
print(' -n, --no-disj Do not enumerate disjoint MCSes')
|
|
657
|
+
print(' -p, --puresat=<string> Use a pure SAT-based hitting set enumerator')
|
|
658
|
+
print(' Available values: cd15, cd19, lgl, mgh (default = mgh)')
|
|
659
|
+
print(' Requires: unsorted mode, i.e. \'-u\'')
|
|
660
|
+
print(' -P, --process=<int> Number of processing rounds')
|
|
661
|
+
print(' Available values: [0 .. INT_MAX] (default = 0)')
|
|
662
|
+
print(' -s, --solver SAT solver to use')
|
|
663
|
+
print(' Available values: cd15, cd19, g3, g4, lgl, mcb, mcm, mpl, m22, mc, mgh (default = g3)')
|
|
664
|
+
print(' -t, --trim=<int> How many times to trim unsatisfiable cores')
|
|
665
|
+
print(' Available values: [0 .. INT_MAX] (default = 0)')
|
|
666
|
+
print(' -u, --unsorted Enumerate MUSes in an unsorted way')
|
|
667
|
+
print(' -v, --verbose Be verbose')
|
|
668
|
+
print(' -x, --exhaust Exhaust new unsatisfiable cores')
|
|
669
|
+
|
|
670
|
+
|
|
671
|
+
#
|
|
672
|
+
#==============================================================================
|
|
673
|
+
if __name__ == '__main__':
|
|
674
|
+
adapt, cover, dcalls, exhaust, minz, no_disj, trim, to_enum, solver, \
|
|
675
|
+
process, puresat, unsorted, verbose, files = parse_options()
|
|
676
|
+
|
|
677
|
+
if files:
|
|
678
|
+
# reading standard CNF, WCNF, or (W)CNF+
|
|
679
|
+
assert re.search(r'cnf[p|+]?(\.(gz|bz2|lzma|xz|zst))?$', files[0]), 'Unknown input file extension'
|
|
680
|
+
if re.search(r'\.wcnf[p|+]?(\.(gz|bz2|lzma|xz|zst))?$', files[0]):
|
|
681
|
+
formula = WCNFPlus(from_file=files[0])
|
|
682
|
+
else: # expecting '*.cnf[,p,+].*'
|
|
683
|
+
formula = CNFPlus(from_file=files[0]).weighted()
|
|
684
|
+
|
|
685
|
+
if cover: # expecting '*.cnf[,p,+].*' only!
|
|
686
|
+
assert re.search(r'cnf[p|+]?(\.(gz|bz2|lzma|xz|zst))?$', cover), 'Wrong file for formula to cover'
|
|
687
|
+
cover = CNFPlus(from_file=cover)
|
|
688
|
+
|
|
689
|
+
# creating an object of OptUx
|
|
690
|
+
with OptUx(formula, solver=solver, adapt=adapt, cover=cover,
|
|
691
|
+
dcalls=dcalls, exhaust=exhaust, minz=minz, nodisj=no_disj,
|
|
692
|
+
process=process, puresat=puresat, unsorted=unsorted,
|
|
693
|
+
trim=trim, verbose=verbose) as optux:
|
|
694
|
+
|
|
695
|
+
# iterating over the necessary number of optimal MUSes
|
|
696
|
+
for i, mus in enumerate(optux.enumerate()):
|
|
697
|
+
# reporting the current solution
|
|
698
|
+
if verbose:
|
|
699
|
+
print('c mus:', ' '.join([str(cl_id) for cl_id in mus]), '0')
|
|
700
|
+
|
|
701
|
+
if verbose > 1:
|
|
702
|
+
print('c cost:', optux.cost)
|
|
703
|
+
|
|
704
|
+
# checking if we are done
|
|
705
|
+
if to_enum and i + 1 == to_enum:
|
|
706
|
+
break
|
|
707
|
+
|
|
708
|
+
# reporting the total oracle time
|
|
709
|
+
if verbose > 1:
|
|
710
|
+
print('c oracle time: {0:.4f}'.format(optux.oracle_time()))
|