python-sat 1.8.dev25__cp310-cp310-musllinux_1_2_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.
- pycard.cpython-310-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-310-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 +50 -0
- python_sat-1.8.dev25.dist-info/WHEEL +5 -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
- python_sat.libs/libgcc_s-0cd532bd.so.1 +0 -0
- python_sat.libs/libstdc++-5d72f927.so.6.0.33 +0 -0
pysat/examples/rc2.py
ADDED
|
@@ -0,0 +1,1858 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
#-*- coding:utf-8 -*-
|
|
3
|
+
##
|
|
4
|
+
## rc2.py
|
|
5
|
+
##
|
|
6
|
+
## Created on: Dec 2, 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
|
+
RC2
|
|
20
|
+
RC2Stratified
|
|
21
|
+
|
|
22
|
+
==================
|
|
23
|
+
Module description
|
|
24
|
+
==================
|
|
25
|
+
|
|
26
|
+
An implementation of the RC2 algorithm for solving maximum
|
|
27
|
+
satisfiability. RC2 stands for *relaxable cardinality constraints*
|
|
28
|
+
(alternatively, *soft cardinality constraints*) and represents an
|
|
29
|
+
improved version of the OLLITI algorithm, which was described in
|
|
30
|
+
[1]_ and [2]_ and originally implemented in the `MSCG MaxSAT
|
|
31
|
+
solver <https://reason.di.fc.ul.pt/wiki/doku.php?id=mscg>`_.
|
|
32
|
+
|
|
33
|
+
Initially, this solver was supposed to serve as an example of a possible
|
|
34
|
+
PySAT usage illustrating how a state-of-the-art MaxSAT algorithm could be
|
|
35
|
+
implemented in Python and still be efficient. It participated in the
|
|
36
|
+
`MaxSAT Evaluations 2018
|
|
37
|
+
<https://maxsat-evaluations.github.io/2018/rankings.html>`_ and `2019
|
|
38
|
+
<https://maxsat-evaluations.github.io/2019/rankings.html>`_ where,
|
|
39
|
+
surprisingly, it was ranked first in two complete categories: *unweighted*
|
|
40
|
+
and *weighted*. A brief solver description can be found in [3]_. A more
|
|
41
|
+
detailed solver description can be found in [4]_.
|
|
42
|
+
|
|
43
|
+
.. [1] António Morgado, Carmine Dodaro, Joao Marques-Silva.
|
|
44
|
+
*Core-Guided MaxSAT with Soft Cardinality Constraints*. CP
|
|
45
|
+
2014. pp. 564-573
|
|
46
|
+
|
|
47
|
+
.. [2] António Morgado, Alexey Ignatiev, Joao Marques-Silva.
|
|
48
|
+
*MSCG: Robust Core-Guided MaxSAT Solving*. JSAT 9. 2014.
|
|
49
|
+
pp. 129-134
|
|
50
|
+
|
|
51
|
+
.. [3] Alexey Ignatiev, António Morgado, Joao Marques-Silva.
|
|
52
|
+
*RC2: A Python-based MaxSAT Solver*. MaxSAT Evaluation 2018.
|
|
53
|
+
p. 22
|
|
54
|
+
|
|
55
|
+
.. [4] Alexey Ignatiev, António Morgado, Joao Marques-Silva.
|
|
56
|
+
*RC2: An Efficient MaxSAT Solver*. MaxSAT Evaluation 2018.
|
|
57
|
+
JSAT 11. 2019. pp. 53-64
|
|
58
|
+
|
|
59
|
+
The file implements two classes: :class:`RC2` and
|
|
60
|
+
:class:`RC2Stratified`. The former class is the basic
|
|
61
|
+
implementation of the algorithm, which can be applied to a MaxSAT
|
|
62
|
+
formula in the :class:`.WCNFPlus` format. The latter class
|
|
63
|
+
additionally implements Boolean lexicographic optimization (BLO)
|
|
64
|
+
[5]_ and stratification [6]_ on top of :class:`RC2`.
|
|
65
|
+
|
|
66
|
+
.. [5] Joao Marques-Silva, Josep Argelich, Ana Graça, Inês Lynce.
|
|
67
|
+
*Boolean lexicographic optimization: algorithms &
|
|
68
|
+
applications*. Ann. Math. Artif. Intell. 62(3-4). 2011.
|
|
69
|
+
pp. 317-343
|
|
70
|
+
|
|
71
|
+
.. [6] Carlos Ansótegui, Maria Luisa Bonet, Joel Gabàs, Jordi
|
|
72
|
+
Levy. *Improving WPM2 for (Weighted) Partial MaxSAT*. CP
|
|
73
|
+
2013. pp. 117-132
|
|
74
|
+
|
|
75
|
+
The implementation can be used as an executable (the list of
|
|
76
|
+
available command-line options can be shown using ``rc2.py -h``)
|
|
77
|
+
in the following way:
|
|
78
|
+
|
|
79
|
+
::
|
|
80
|
+
|
|
81
|
+
$ xzcat formula.wcnf.xz
|
|
82
|
+
p wcnf 3 6 4
|
|
83
|
+
1 1 0
|
|
84
|
+
1 2 0
|
|
85
|
+
1 3 0
|
|
86
|
+
4 -1 -2 0
|
|
87
|
+
4 -1 -3 0
|
|
88
|
+
4 -2 -3 0
|
|
89
|
+
|
|
90
|
+
$ rc2.py -vv formula.wcnf.xz
|
|
91
|
+
c formula: 3 vars, 3 hard, 3 soft
|
|
92
|
+
c cost: 1; core sz: 2; soft sz: 2
|
|
93
|
+
c cost: 2; core sz: 2; soft sz: 1
|
|
94
|
+
s OPTIMUM FOUND
|
|
95
|
+
o 2
|
|
96
|
+
v -1 -2 3
|
|
97
|
+
c oracle time: 0.0001
|
|
98
|
+
|
|
99
|
+
Alternatively, the algorithm can be accessed and invoked through the
|
|
100
|
+
standard ``import`` interface of Python, e.g.
|
|
101
|
+
|
|
102
|
+
.. code-block:: python
|
|
103
|
+
|
|
104
|
+
>>> from pysat.examples.rc2 import RC2
|
|
105
|
+
>>> from pysat.formula import WCNF
|
|
106
|
+
>>>
|
|
107
|
+
>>> wcnf = WCNF(from_file='formula.wcnf.xz')
|
|
108
|
+
>>>
|
|
109
|
+
>>> with RC2(wcnf) as rc2:
|
|
110
|
+
... for m in rc2.enumerate():
|
|
111
|
+
... print('model {0} has cost {1}'.format(m, rc2.cost))
|
|
112
|
+
model [-1, -2, 3] has cost 2
|
|
113
|
+
model [1, -2, -3] has cost 2
|
|
114
|
+
model [-1, 2, -3] has cost 2
|
|
115
|
+
model [-1, -2, -3] has cost 3
|
|
116
|
+
|
|
117
|
+
As can be seen in the example above, the solver can be instructed
|
|
118
|
+
either to compute one MaxSAT solution of an input formula, or to
|
|
119
|
+
enumerate a given number (or *all*) of its top MaxSAT solutions.
|
|
120
|
+
|
|
121
|
+
Importantly, the value of the cost computed during the solving process is
|
|
122
|
+
*not* the optimal value of the objective function. Instead, it is a
|
|
123
|
+
*complement* to the optimal value, i.e. the smallest price one has to pay
|
|
124
|
+
with the optimal solution.
|
|
125
|
+
|
|
126
|
+
==============
|
|
127
|
+
Module details
|
|
128
|
+
==============
|
|
129
|
+
"""
|
|
130
|
+
|
|
131
|
+
#
|
|
132
|
+
#==============================================================================
|
|
133
|
+
from __future__ import print_function
|
|
134
|
+
import collections
|
|
135
|
+
import getopt
|
|
136
|
+
import itertools
|
|
137
|
+
from math import copysign
|
|
138
|
+
import os
|
|
139
|
+
from pysat.formula import CNFPlus, WCNFPlus, IDPool
|
|
140
|
+
from pysat.card import ITotalizer
|
|
141
|
+
from pysat.process import Processor
|
|
142
|
+
from pysat.solvers import Solver, SolverNames
|
|
143
|
+
import re
|
|
144
|
+
import six
|
|
145
|
+
from six.moves import range
|
|
146
|
+
import sys
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
# names of BLO strategies
|
|
150
|
+
#==============================================================================
|
|
151
|
+
blomap = {'none': 0, 'basic': 1, 'div': 3, 'cluster': 5, 'full': 7}
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
#
|
|
155
|
+
#==============================================================================
|
|
156
|
+
class RC2(object):
|
|
157
|
+
"""
|
|
158
|
+
Implementation of the basic RC2 algorithm. Given a (weighted)
|
|
159
|
+
(partial) CNF formula, i.e. formula in the :class:`.WCNFPlus`
|
|
160
|
+
format, this class can be used to compute a given number of
|
|
161
|
+
MaxSAT solutions for the input formula. :class:`RC2` roughly
|
|
162
|
+
follows the implementation of algorithm OLLITI [1]_ [2]_ of
|
|
163
|
+
MSCG and applies a few heuristics on top of it. These include
|
|
164
|
+
|
|
165
|
+
- *unsatisfiable core exhaustion* (see method :func:`exhaust_core`),
|
|
166
|
+
- *unsatisfiable core reduction* (see method :func:`minimize_core`),
|
|
167
|
+
- *intrinsic AtMost1 constraints* (see method :func:`adapt_am1`).
|
|
168
|
+
|
|
169
|
+
:class:`RC2` can use any SAT solver available in PySAT. The default
|
|
170
|
+
SAT solver to use is ``g3`` (see :class:`.SolverNames`). Additionally,
|
|
171
|
+
if Glucose is chosen, the ``incr`` parameter controls whether to use
|
|
172
|
+
the incremental mode of Glucose [7]_ (turned off by default). Boolean
|
|
173
|
+
parameters ``adapt``, ``exhaust``, and ``minz`` control whether or to
|
|
174
|
+
apply detection and adaptation of intrinsic AtMost1 constraints, core
|
|
175
|
+
exhaustion, and core reduction. Unsatisfiable cores can be trimmed if
|
|
176
|
+
the ``trim`` parameter is set to a non-zero integer. Formula
|
|
177
|
+
preprocessing can be applied a given number of rounds specified as the
|
|
178
|
+
value of parameter ``process``. Finally, verbosity level can be set
|
|
179
|
+
using the ``verbose`` parameter.
|
|
180
|
+
|
|
181
|
+
.. [7] Gilles Audemard, Jean-Marie Lagniez, Laurent Simon.
|
|
182
|
+
*Improving Glucose for Incremental SAT Solving with
|
|
183
|
+
Assumptions: Application to MUS Extraction*. SAT 2013.
|
|
184
|
+
pp. 309-317
|
|
185
|
+
|
|
186
|
+
:param formula: (weighted) (partial) CNFPlus formula
|
|
187
|
+
:param solver: SAT oracle name
|
|
188
|
+
:param adapt: detect and adapt intrinsic AtMost1 constraints
|
|
189
|
+
:param exhaust: do core exhaustion
|
|
190
|
+
:param incr: use incremental mode of Glucose
|
|
191
|
+
:param minz: do heuristic core reduction
|
|
192
|
+
:param process: apply formula preprocessing this many times
|
|
193
|
+
:param trim: do core trimming at most this number of times
|
|
194
|
+
:param verbose: verbosity level
|
|
195
|
+
|
|
196
|
+
:type formula: :class:`.WCNFPlus`
|
|
197
|
+
:type solver: str
|
|
198
|
+
:type adapt: bool
|
|
199
|
+
:type exhaust: bool
|
|
200
|
+
:type incr: bool
|
|
201
|
+
:type minz: bool
|
|
202
|
+
:type process: int
|
|
203
|
+
:type trim: int
|
|
204
|
+
:type verbose: int
|
|
205
|
+
"""
|
|
206
|
+
|
|
207
|
+
def __init__(self, formula, solver='g3', adapt=False, exhaust=False,
|
|
208
|
+
incr=False, minz=False, process=0, trim=0, verbose=0):
|
|
209
|
+
"""
|
|
210
|
+
Constructor.
|
|
211
|
+
"""
|
|
212
|
+
|
|
213
|
+
# saving verbosity level and other options
|
|
214
|
+
self.verbose = verbose
|
|
215
|
+
self.exhaust = exhaust
|
|
216
|
+
self.process = process
|
|
217
|
+
self.solver = solver
|
|
218
|
+
self.adapt = adapt
|
|
219
|
+
self.minz = minz
|
|
220
|
+
self.trim = trim
|
|
221
|
+
|
|
222
|
+
# oracles are initialised to be None
|
|
223
|
+
self.oracle, self.processor = None, None
|
|
224
|
+
|
|
225
|
+
# clause selectors and mapping from selectors to clause ids
|
|
226
|
+
# .sall, .s2cl, and .sneg are required only for model enumeration
|
|
227
|
+
self.sels, self.smap, self.sall, self.s2cl, self.sneg = [], {}, [], {}, set([])
|
|
228
|
+
|
|
229
|
+
# other MaxSAT related stuff
|
|
230
|
+
self.pool = IDPool(start_from=formula.nv + 1)
|
|
231
|
+
self.wght = {} # weights of soft clauses
|
|
232
|
+
self.sums = [] # totalizer sum assumptions
|
|
233
|
+
self.bnds = {} # a mapping from sum assumptions to totalizer bounds
|
|
234
|
+
self.tobj = {} # a mapping from sum assumptions to totalizer objects
|
|
235
|
+
self.swgt = {} # a mapping from sum assumptions to their core weights
|
|
236
|
+
self.cost = 0
|
|
237
|
+
|
|
238
|
+
# mappings between internal and external variables
|
|
239
|
+
VariableMap = collections.namedtuple('VariableMap', ['e2i', 'i2e'])
|
|
240
|
+
self.vmap = VariableMap(e2i={}, i2e={})
|
|
241
|
+
|
|
242
|
+
# initialize SAT oracle with hard clauses only
|
|
243
|
+
self.init(formula, incr=incr)
|
|
244
|
+
|
|
245
|
+
# core minimization is going to be extremely expensive
|
|
246
|
+
# for large plain formulas, and so we turn it off here
|
|
247
|
+
wght = self.wght.values()
|
|
248
|
+
if not formula.hard and len(self.sels) > 100000 and min(wght) == max(wght):
|
|
249
|
+
self.minz = False
|
|
250
|
+
|
|
251
|
+
def __del__(self):
|
|
252
|
+
"""
|
|
253
|
+
Destructor.
|
|
254
|
+
"""
|
|
255
|
+
|
|
256
|
+
self.delete()
|
|
257
|
+
|
|
258
|
+
def __enter__(self):
|
|
259
|
+
"""
|
|
260
|
+
'with' constructor.
|
|
261
|
+
"""
|
|
262
|
+
|
|
263
|
+
return self
|
|
264
|
+
|
|
265
|
+
def __exit__(self, exc_type, exc_value, traceback):
|
|
266
|
+
"""
|
|
267
|
+
'with' destructor.
|
|
268
|
+
"""
|
|
269
|
+
|
|
270
|
+
self.delete()
|
|
271
|
+
|
|
272
|
+
def init(self, formula, incr=False):
|
|
273
|
+
"""
|
|
274
|
+
Initialize the internal SAT oracle. The oracle is used
|
|
275
|
+
incrementally and so it is initialized only once when
|
|
276
|
+
constructing an object of class :class:`RC2`. Given an
|
|
277
|
+
input :class:`.WCNFPlus` formula, the method bootstraps the
|
|
278
|
+
oracle with its hard clauses. It also augments the soft
|
|
279
|
+
clauses with "fresh" selectors and adds them to the oracle
|
|
280
|
+
afterwards.
|
|
281
|
+
|
|
282
|
+
Optional input parameter ``incr`` (``False`` by default)
|
|
283
|
+
regulates whether or not Glucose's incremental mode [7]_
|
|
284
|
+
is turned on.
|
|
285
|
+
|
|
286
|
+
:param formula: input formula
|
|
287
|
+
:param incr: apply incremental mode of Glucose
|
|
288
|
+
|
|
289
|
+
:type formula: :class:`.WCNFPlus`
|
|
290
|
+
:type incr: bool
|
|
291
|
+
"""
|
|
292
|
+
|
|
293
|
+
# creating a solver object
|
|
294
|
+
self.oracle = Solver(name=self.solver,
|
|
295
|
+
bootstrap_with=formula.hard if self.process == 0 else [],
|
|
296
|
+
incr=incr, use_timer=True)
|
|
297
|
+
|
|
298
|
+
# adding native cardinality constraints (if any) as hard clauses
|
|
299
|
+
# this can be done only if the Minicard solver is in use
|
|
300
|
+
# this cannot be done if RC2 is run from the command line
|
|
301
|
+
if isinstance(formula, WCNFPlus) and formula.atms:
|
|
302
|
+
# we are using CaDiCaL195 and it can use external linear engine
|
|
303
|
+
if self.solver in SolverNames.cadical195:
|
|
304
|
+
self.oracle.activate_atmost()
|
|
305
|
+
|
|
306
|
+
assert self.oracle.supports_atmost(), \
|
|
307
|
+
'{0} does not support native cardinality constraints. Make sure you use the right type of formula.'.format(self.solver)
|
|
308
|
+
|
|
309
|
+
for atm in formula.atms:
|
|
310
|
+
self.oracle.add_atmost(*atm)
|
|
311
|
+
|
|
312
|
+
# adding soft clauses to oracle
|
|
313
|
+
for i, cl in enumerate(formula.soft):
|
|
314
|
+
selv = cl[0] # if clause is unit, selector variable is its literal
|
|
315
|
+
|
|
316
|
+
if len(cl) > 1:
|
|
317
|
+
selv = self.pool.id()
|
|
318
|
+
|
|
319
|
+
self.s2cl[selv] = cl[:]
|
|
320
|
+
cl.append(-selv)
|
|
321
|
+
|
|
322
|
+
if self.process == 0:
|
|
323
|
+
self.oracle.add_clause(cl)
|
|
324
|
+
else:
|
|
325
|
+
# adding to formula's hard clauses
|
|
326
|
+
# if any preprocessing is required
|
|
327
|
+
formula.hard.append(cl + [-sel])
|
|
328
|
+
formula.soft[i] = [sel]
|
|
329
|
+
|
|
330
|
+
if selv not in self.wght:
|
|
331
|
+
# record selector and its weight
|
|
332
|
+
self.sels.append(selv)
|
|
333
|
+
self.wght[selv] = formula.wght[i]
|
|
334
|
+
self.smap[selv] = i
|
|
335
|
+
else:
|
|
336
|
+
# selector is not new; increment its weight
|
|
337
|
+
self.wght[selv] += formula.wght[i]
|
|
338
|
+
|
|
339
|
+
# storing the set of selectors
|
|
340
|
+
self.sels_set = set(self.sels)
|
|
341
|
+
self.sall = self.sels[:]
|
|
342
|
+
|
|
343
|
+
# we may end up having zero-weighed soft clauses
|
|
344
|
+
self.garbage = set([l for l in self.sels if self.wght[l] == 0])
|
|
345
|
+
if self.garbage:
|
|
346
|
+
self.filter_assumps()
|
|
347
|
+
|
|
348
|
+
# hard clauses are added last if formula processing has to be done
|
|
349
|
+
if self.process > 0:
|
|
350
|
+
self.processor = Processor(bootstrap_with=formula.hard)
|
|
351
|
+
hard = self.processor.process(rounds=self.process, freeze=self.sels)
|
|
352
|
+
self.oracle.append_formula(hard)
|
|
353
|
+
|
|
354
|
+
# at this point internal and external variables are the same
|
|
355
|
+
for v in range(1, formula.nv + 1):
|
|
356
|
+
self.vmap.e2i[v] = v
|
|
357
|
+
self.vmap.i2e[v] = v
|
|
358
|
+
|
|
359
|
+
if self.verbose > 1:
|
|
360
|
+
nofh = len(hard.clauses) if self.processor else len(formula.hard)
|
|
361
|
+
print('c formula: {0} vars, {1} hard, {2} soft'.format(formula.nv,
|
|
362
|
+
nofh, len(formula.soft)))
|
|
363
|
+
|
|
364
|
+
def add_clause(self, clause, weight=None):
|
|
365
|
+
"""
|
|
366
|
+
The method for adding a new hard of soft clause to the
|
|
367
|
+
problem formula. Although the input formula is to be
|
|
368
|
+
specified as an argument of the constructor of
|
|
369
|
+
:class:`RC2`, adding clauses may be helpful when
|
|
370
|
+
*enumerating* MaxSAT solutions of the formula. This way,
|
|
371
|
+
the clauses are added incrementally, i.e. *on the fly*.
|
|
372
|
+
|
|
373
|
+
The clause to add can be any iterable over integer
|
|
374
|
+
literals. The additional integer parameter ``weight`` can
|
|
375
|
+
be set to meaning the the clause being added is soft
|
|
376
|
+
having the corresponding weight (note that parameter
|
|
377
|
+
``weight`` is set to ``None`` by default meaning that the
|
|
378
|
+
clause is hard).
|
|
379
|
+
|
|
380
|
+
Also note that besides pure clauses, the method can also expect
|
|
381
|
+
native cardinality constraints represented as a pair ``(lits,
|
|
382
|
+
bound)``. Only hard cardinality constraints can be added.
|
|
383
|
+
|
|
384
|
+
:param clause: a clause to add
|
|
385
|
+
:param weight: weight of the clause (if any)
|
|
386
|
+
|
|
387
|
+
:type clause: iterable(int)
|
|
388
|
+
:type weight: int
|
|
389
|
+
|
|
390
|
+
.. code-block:: python
|
|
391
|
+
|
|
392
|
+
>>> from pysat.examples.rc2 import RC2
|
|
393
|
+
>>> from pysat.formula import WCNF
|
|
394
|
+
>>>
|
|
395
|
+
>>> wcnf = WCNF()
|
|
396
|
+
>>> wcnf.append([-1, -2]) # adding hard clauses
|
|
397
|
+
>>> wcnf.append([-1, -3])
|
|
398
|
+
>>>
|
|
399
|
+
>>> wcnf.append([1], weight=1) # adding soft clauses
|
|
400
|
+
>>> wcnf.append([2], weight=1)
|
|
401
|
+
>>> wcnf.append([3], weight=1)
|
|
402
|
+
>>>
|
|
403
|
+
>>> with RC2(wcnf) as rc2:
|
|
404
|
+
... rc2.compute() # solving the MaxSAT problem
|
|
405
|
+
[-1, 2, 3]
|
|
406
|
+
... print(rc2.cost)
|
|
407
|
+
1
|
|
408
|
+
... rc2.add_clause([-2, -3]) # adding one more hard clause
|
|
409
|
+
... rc2.compute() # computing another model
|
|
410
|
+
[-1, -2, 3]
|
|
411
|
+
... print(rc2.cost)
|
|
412
|
+
2
|
|
413
|
+
"""
|
|
414
|
+
|
|
415
|
+
# first, map external literals to internal literals
|
|
416
|
+
# introduce new variables if necessary
|
|
417
|
+
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]))
|
|
418
|
+
|
|
419
|
+
if not weight:
|
|
420
|
+
if not len(clause) == 2 or not type(clause[0]) in (list, tuple, set):
|
|
421
|
+
# the clause is hard, and so we simply add it to the SAT oracle
|
|
422
|
+
self.oracle.add_clause(cl)
|
|
423
|
+
else:
|
|
424
|
+
# this should be a native cardinality constraint,
|
|
425
|
+
# which can be used only together with Minicard
|
|
426
|
+
assert self.oracle.supports_atmost(), \
|
|
427
|
+
'{0} does not support native cardinality constraints. Make sure you use the right type of formula.'.format(self.solver)
|
|
428
|
+
|
|
429
|
+
self.oracle.add_atmost(cl, clause[1], weights=clause[2] if len(clause) == 3 else [])
|
|
430
|
+
else:
|
|
431
|
+
# soft clauses should be augmented with a selector
|
|
432
|
+
selv = cl[0] # for a unit clause, no selector is needed
|
|
433
|
+
|
|
434
|
+
if len(cl) > 1:
|
|
435
|
+
selv = self.pool.id()
|
|
436
|
+
|
|
437
|
+
self.s2cl[selv] = cl[:]
|
|
438
|
+
cl.append(-selv)
|
|
439
|
+
self.oracle.add_clause(cl)
|
|
440
|
+
|
|
441
|
+
if selv not in self.wght:
|
|
442
|
+
# record selector and its weight
|
|
443
|
+
self.sels.append(selv)
|
|
444
|
+
self.wght[selv] = weight
|
|
445
|
+
self.smap[selv] = len(self.sels) - 1
|
|
446
|
+
else:
|
|
447
|
+
# selector is not new; increment its weight
|
|
448
|
+
self.wght[selv] += weight
|
|
449
|
+
|
|
450
|
+
self.sall.append(selv)
|
|
451
|
+
self.sels_set.add(selv)
|
|
452
|
+
|
|
453
|
+
def delete(self):
|
|
454
|
+
"""
|
|
455
|
+
Explicit destructor of the internal SAT oracle and all the
|
|
456
|
+
totalizer objects creating during the solving process. This also
|
|
457
|
+
destroys the processor (if any).
|
|
458
|
+
"""
|
|
459
|
+
|
|
460
|
+
if self.oracle:
|
|
461
|
+
if not self.oracle.supports_atmost(): # for minicard-like, there is nothing to free
|
|
462
|
+
for t in six.itervalues(self.tobj):
|
|
463
|
+
t.delete()
|
|
464
|
+
|
|
465
|
+
self.oracle.delete()
|
|
466
|
+
self.oracle = None
|
|
467
|
+
|
|
468
|
+
if self.processor:
|
|
469
|
+
self.processor.delete()
|
|
470
|
+
self.processor = None
|
|
471
|
+
|
|
472
|
+
def compute(self):
|
|
473
|
+
"""
|
|
474
|
+
This method can be used for computing one MaxSAT solution,
|
|
475
|
+
i.e. for computing an assignment satisfying all hard
|
|
476
|
+
clauses of the input formula and maximizing the sum of
|
|
477
|
+
weights of satisfied soft clauses. It is a wrapper for the
|
|
478
|
+
internal :func:`compute_` method, which does the job,
|
|
479
|
+
followed by the model extraction.
|
|
480
|
+
|
|
481
|
+
Note that the method returns ``None`` if no MaxSAT model
|
|
482
|
+
exists. The method can be called multiple times, each
|
|
483
|
+
being followed by blocking the last model. This way one
|
|
484
|
+
can enumerate top-:math:`k` MaxSAT solutions (this can
|
|
485
|
+
also be done by calling :meth:`enumerate()`).
|
|
486
|
+
|
|
487
|
+
:return: a MaxSAT model
|
|
488
|
+
:rtype: list(int)
|
|
489
|
+
|
|
490
|
+
.. code-block:: python
|
|
491
|
+
|
|
492
|
+
>>> from pysat.examples.rc2 import RC2
|
|
493
|
+
>>> from pysat.formula import WCNF
|
|
494
|
+
>>>
|
|
495
|
+
>>> rc2 = RC2(WCNF()) # passing an empty WCNF() formula
|
|
496
|
+
>>> rc2.add_clause([-1, -2])
|
|
497
|
+
>>> rc2.add_clause([-1, -3])
|
|
498
|
+
>>> rc2.add_clause([-2, -3])
|
|
499
|
+
>>>
|
|
500
|
+
>>> rc2.add_clause([1], weight=1)
|
|
501
|
+
>>> rc2.add_clause([2], weight=1)
|
|
502
|
+
>>> rc2.add_clause([3], weight=1)
|
|
503
|
+
>>>
|
|
504
|
+
>>> model = rc2.compute()
|
|
505
|
+
>>> print(model)
|
|
506
|
+
[-1, -2, 3]
|
|
507
|
+
>>> print(rc2.cost)
|
|
508
|
+
2
|
|
509
|
+
>>> rc2.delete()
|
|
510
|
+
"""
|
|
511
|
+
|
|
512
|
+
# simply apply MaxSAT only once
|
|
513
|
+
res = self.compute_()
|
|
514
|
+
|
|
515
|
+
if res:
|
|
516
|
+
# extracting a model
|
|
517
|
+
self.model = self.oracle.get_model()
|
|
518
|
+
|
|
519
|
+
if self.model is None and self.pool.top == 0:
|
|
520
|
+
# we seem to have been given an empty formula
|
|
521
|
+
# so let's transform the None model returned to []
|
|
522
|
+
self.model = []
|
|
523
|
+
|
|
524
|
+
self.model = filter(lambda l: abs(l) in self.vmap.i2e, self.model)
|
|
525
|
+
self.model = map(lambda l: int(copysign(self.vmap.i2e[abs(l)], l)), self.model)
|
|
526
|
+
self.model = sorted(self.model, key=lambda l: abs(l))
|
|
527
|
+
|
|
528
|
+
# if formula processing was used, we should
|
|
529
|
+
# restore the model for the original formula
|
|
530
|
+
if self.processor:
|
|
531
|
+
self.model = self.processor.restore(self.model)
|
|
532
|
+
|
|
533
|
+
return self.model
|
|
534
|
+
|
|
535
|
+
def enumerate(self, block=0):
|
|
536
|
+
"""
|
|
537
|
+
Enumerate top MaxSAT solutions (from best to worst). The
|
|
538
|
+
method works as a generator, which iteratively calls
|
|
539
|
+
:meth:`compute` to compute a MaxSAT model, blocks it
|
|
540
|
+
internally and returns it.
|
|
541
|
+
|
|
542
|
+
An optional parameter can be used to enforce computation of MaxSAT
|
|
543
|
+
models corresponding to different maximal satisfiable subsets
|
|
544
|
+
(MSSes) or minimal correction subsets (MCSes). To block MSSes, one
|
|
545
|
+
should set the ``block`` parameter to ``1``. To block MCSes, set
|
|
546
|
+
it to ``-1``. By the default (for blocking MaxSAT models),
|
|
547
|
+
``block`` is set to ``0``.
|
|
548
|
+
|
|
549
|
+
:param block: preferred way to block solutions when enumerating
|
|
550
|
+
:type block: int
|
|
551
|
+
|
|
552
|
+
:return: a MaxSAT model
|
|
553
|
+
:rtype: list(int)
|
|
554
|
+
|
|
555
|
+
.. code-block:: python
|
|
556
|
+
|
|
557
|
+
>>> from pysat.examples.rc2 import RC2
|
|
558
|
+
>>> from pysat.formula import WCNF
|
|
559
|
+
>>>
|
|
560
|
+
>>> rc2 = RC2(WCNF()) # passing an empty WCNF() formula
|
|
561
|
+
>>> rc2.add_clause([-1, -2]) # adding clauses "on the fly"
|
|
562
|
+
>>> rc2.add_clause([-1, -3])
|
|
563
|
+
>>> rc2.add_clause([-2, -3])
|
|
564
|
+
>>>
|
|
565
|
+
>>> rc2.add_clause([1], weight=1)
|
|
566
|
+
>>> rc2.add_clause([2], weight=1)
|
|
567
|
+
>>> rc2.add_clause([3], weight=1)
|
|
568
|
+
>>>
|
|
569
|
+
>>> for model in rc2.enumerate():
|
|
570
|
+
... print(model, rc2.cost)
|
|
571
|
+
[-1, -2, 3] 2
|
|
572
|
+
[1, -2, -3] 2
|
|
573
|
+
[-1, 2, -3] 2
|
|
574
|
+
[-1, -2, -3] 3
|
|
575
|
+
>>> rc2.delete()
|
|
576
|
+
"""
|
|
577
|
+
|
|
578
|
+
done = False
|
|
579
|
+
while not done:
|
|
580
|
+
model = self.compute()
|
|
581
|
+
|
|
582
|
+
if model != None:
|
|
583
|
+
if block == 1:
|
|
584
|
+
# to block an MSS corresponding to the model, we add
|
|
585
|
+
# a clause enforcing at least one of the MSS clauses
|
|
586
|
+
# to be falsified next time
|
|
587
|
+
m, cl = set(self.oracle.get_model()), []
|
|
588
|
+
|
|
589
|
+
for selv in self.sall:
|
|
590
|
+
if selv in m:
|
|
591
|
+
# clause is satisfied
|
|
592
|
+
cl.append(-selv)
|
|
593
|
+
|
|
594
|
+
# next time we want to falsify one of these
|
|
595
|
+
# clauses, i.e. we should encode the negation
|
|
596
|
+
# of each of these selectors
|
|
597
|
+
if selv in self.s2cl and not selv in self.sneg:
|
|
598
|
+
self.sneg.add(selv)
|
|
599
|
+
for il in self.s2cl[selv]:
|
|
600
|
+
self.oracle.add_clause([selv, -il])
|
|
601
|
+
|
|
602
|
+
self.oracle.add_clause(cl)
|
|
603
|
+
elif block == -1:
|
|
604
|
+
# a similar (but simpler) piece of code goes here,
|
|
605
|
+
# to block the MCS corresponding to the model
|
|
606
|
+
# (this blocking is stronger than MSS blocking above)
|
|
607
|
+
m = set(self.oracle.get_model())
|
|
608
|
+
self.oracle.add_clause([l for l in filter(lambda l: -l in m, self.sall)])
|
|
609
|
+
else:
|
|
610
|
+
# here, we simply block a previous MaxSAT model
|
|
611
|
+
self.add_clause([-l for l in model])
|
|
612
|
+
|
|
613
|
+
yield model
|
|
614
|
+
else:
|
|
615
|
+
done = True
|
|
616
|
+
|
|
617
|
+
def compute_(self):
|
|
618
|
+
"""
|
|
619
|
+
Main core-guided loop, which iteratively calls a SAT
|
|
620
|
+
oracle, extracts a new unsatisfiable core and processes
|
|
621
|
+
it. The loop finishes as soon as a satisfiable formula is
|
|
622
|
+
obtained. If specified in the command line, the method
|
|
623
|
+
additionally calls :meth:`adapt_am1` to detect and adapt
|
|
624
|
+
intrinsic AtMost1 constraints before executing the loop.
|
|
625
|
+
|
|
626
|
+
:rtype: bool
|
|
627
|
+
"""
|
|
628
|
+
|
|
629
|
+
# trying to adapt (simplify) the formula
|
|
630
|
+
# by detecting and using atmost1 constraints
|
|
631
|
+
if self.adapt:
|
|
632
|
+
self.adapt_am1()
|
|
633
|
+
|
|
634
|
+
# main solving loop
|
|
635
|
+
while not self.oracle.solve(assumptions=self.sels + self.sums):
|
|
636
|
+
self.get_core()
|
|
637
|
+
|
|
638
|
+
if not self.core:
|
|
639
|
+
# core is empty, i.e. hard part is unsatisfiable
|
|
640
|
+
return False
|
|
641
|
+
|
|
642
|
+
self.process_core()
|
|
643
|
+
|
|
644
|
+
if self.verbose > 1:
|
|
645
|
+
print('c cost: {0}; core sz: {1}; soft sz: {2}'.format(self.cost,
|
|
646
|
+
len(self.core), len(self.sels) + len(self.sums)))
|
|
647
|
+
|
|
648
|
+
return True
|
|
649
|
+
|
|
650
|
+
def get_core(self):
|
|
651
|
+
"""
|
|
652
|
+
Extract unsatisfiable core. The result of the procedure is
|
|
653
|
+
stored in variable ``self.core``. If necessary, core
|
|
654
|
+
trimming and also heuristic core reduction is applied
|
|
655
|
+
depending on the command-line options. A *minimum weight*
|
|
656
|
+
of the core is computed and stored in ``self.minw``.
|
|
657
|
+
Finally, the core is divided into two parts:
|
|
658
|
+
|
|
659
|
+
1. clause selectors (``self.core_sels``),
|
|
660
|
+
2. sum assumptions (``self.core_sums``).
|
|
661
|
+
"""
|
|
662
|
+
|
|
663
|
+
# extracting the core
|
|
664
|
+
self.core = self.oracle.get_core()
|
|
665
|
+
|
|
666
|
+
if self.core:
|
|
667
|
+
# try to reduce the core by trimming
|
|
668
|
+
self.trim_core()
|
|
669
|
+
|
|
670
|
+
# and by heuristic minimization
|
|
671
|
+
self.minimize_core()
|
|
672
|
+
|
|
673
|
+
# the core may be empty after core minimization
|
|
674
|
+
if not self.core:
|
|
675
|
+
return
|
|
676
|
+
|
|
677
|
+
# core weight
|
|
678
|
+
self.minw = min(map(lambda l: self.wght[l], self.core))
|
|
679
|
+
|
|
680
|
+
# dividing the core into two parts
|
|
681
|
+
iter1, iter2 = itertools.tee(self.core)
|
|
682
|
+
self.core_sels = list(l for l in iter1 if l in self.sels_set)
|
|
683
|
+
self.core_sums = list(l for l in iter2 if l not in self.sels_set)
|
|
684
|
+
|
|
685
|
+
def process_core(self):
|
|
686
|
+
"""
|
|
687
|
+
The method deals with a core found previously in
|
|
688
|
+
:func:`get_core`. Clause selectors ``self.core_sels`` and
|
|
689
|
+
sum assumptions involved in the core are treated
|
|
690
|
+
separately of each other. This is handled by calling
|
|
691
|
+
methods :func:`process_sels` and :func:`process_sums`,
|
|
692
|
+
respectively. Whenever necessary, both methods relax the
|
|
693
|
+
core literals, which is followed by creating a new
|
|
694
|
+
totalizer object encoding the sum of the new relaxation
|
|
695
|
+
variables. The totalizer object can be "exhausted"
|
|
696
|
+
depending on the option.
|
|
697
|
+
"""
|
|
698
|
+
|
|
699
|
+
# updating the cost
|
|
700
|
+
self.cost += self.minw
|
|
701
|
+
|
|
702
|
+
if len(self.core_sels) != 1 or len(self.core_sums) > 0:
|
|
703
|
+
# process selectors in the core
|
|
704
|
+
self.process_sels()
|
|
705
|
+
|
|
706
|
+
# process previously introducded sums in the core
|
|
707
|
+
self.process_sums()
|
|
708
|
+
|
|
709
|
+
if len(self.rels) > 1:
|
|
710
|
+
# create a new cardunality constraint
|
|
711
|
+
t = self.create_sum()
|
|
712
|
+
|
|
713
|
+
# apply core exhaustion if required
|
|
714
|
+
b = self.exhaust_core(t) if self.exhaust else 1
|
|
715
|
+
|
|
716
|
+
if b:
|
|
717
|
+
# save the info about this sum and
|
|
718
|
+
# add its assumption literal
|
|
719
|
+
self.set_bound(t, b, self.minw)
|
|
720
|
+
else:
|
|
721
|
+
# impossible to satisfy any of these clauses
|
|
722
|
+
# they must become hard
|
|
723
|
+
for relv in self.rels:
|
|
724
|
+
self.oracle.add_clause([relv])
|
|
725
|
+
else:
|
|
726
|
+
# unit cores are treated differently
|
|
727
|
+
# (their negation is added to the hard part)
|
|
728
|
+
self.oracle.add_clause([-self.core_sels[0]])
|
|
729
|
+
self.garbage.add(self.core_sels[0])
|
|
730
|
+
|
|
731
|
+
# remove unnecessary assumptions
|
|
732
|
+
self.filter_assumps()
|
|
733
|
+
|
|
734
|
+
def adapt_am1(self):
|
|
735
|
+
"""
|
|
736
|
+
Detect and adapt intrinsic AtMost1 constraints. Assume
|
|
737
|
+
there is a subset of soft clauses
|
|
738
|
+
:math:`\\mathcal{S}'\\subseteq \\mathcal{S}` s.t.
|
|
739
|
+
:math:`\\sum_{c\\in\\mathcal{S}'}{c\\leq 1}`, i.e. at most
|
|
740
|
+
one of the clauses of :math:`\\mathcal{S}'` can be
|
|
741
|
+
satisfied.
|
|
742
|
+
|
|
743
|
+
Each AtMost1 relationship between the soft clauses can be
|
|
744
|
+
detected in the following way. The method traverses all
|
|
745
|
+
soft clauses of the formula one by one, sets one
|
|
746
|
+
respective selector literal to true and checks whether
|
|
747
|
+
some other soft clauses are forced to be false. This is
|
|
748
|
+
checked by testing if selectors for other soft clauses are
|
|
749
|
+
unit-propagated to be false. Note that this method for
|
|
750
|
+
detection of AtMost1 constraints is *incomplete*, because
|
|
751
|
+
in general unit propagation does not suffice to test
|
|
752
|
+
whether or not :math:`\\mathcal{F}\\wedge l_i\\models
|
|
753
|
+
\\neg{l_j}`.
|
|
754
|
+
|
|
755
|
+
Each intrinsic AtMost1 constraint detected this way is
|
|
756
|
+
handled by calling :func:`process_am1`.
|
|
757
|
+
"""
|
|
758
|
+
|
|
759
|
+
# literal connections
|
|
760
|
+
conns = collections.defaultdict(lambda: set([]))
|
|
761
|
+
confl = []
|
|
762
|
+
|
|
763
|
+
# prepare connections
|
|
764
|
+
for l1 in self.sels:
|
|
765
|
+
st, props = self.oracle.propagate(assumptions=[l1], phase_saving=2)
|
|
766
|
+
if st:
|
|
767
|
+
for l2 in props:
|
|
768
|
+
if -l2 in self.sels_set:
|
|
769
|
+
conns[l1].add(-l2)
|
|
770
|
+
conns[-l2].add(l1)
|
|
771
|
+
else:
|
|
772
|
+
# propagating this literal results in a conflict
|
|
773
|
+
confl.append(l1)
|
|
774
|
+
|
|
775
|
+
if confl: # filtering out unnecessary connections
|
|
776
|
+
ccopy = {}
|
|
777
|
+
confl = set(confl)
|
|
778
|
+
|
|
779
|
+
for l in conns:
|
|
780
|
+
if l not in confl:
|
|
781
|
+
cc = conns[l].difference(confl)
|
|
782
|
+
if cc:
|
|
783
|
+
ccopy[l] = cc
|
|
784
|
+
|
|
785
|
+
conns = ccopy
|
|
786
|
+
confl = list(confl)
|
|
787
|
+
|
|
788
|
+
# processing unit size cores
|
|
789
|
+
for l in confl:
|
|
790
|
+
self.core, self.minw = [l], self.wght[l]
|
|
791
|
+
self.core_sels, self.core_sums = [l], []
|
|
792
|
+
self.process_core()
|
|
793
|
+
|
|
794
|
+
if self.verbose > 1:
|
|
795
|
+
print('c unit cores found: {0}; cost: {1}'.format(len(confl),
|
|
796
|
+
self.cost))
|
|
797
|
+
|
|
798
|
+
nof_am1 = 0
|
|
799
|
+
len_am1 = []
|
|
800
|
+
lits = set(conns.keys())
|
|
801
|
+
while lits:
|
|
802
|
+
am1 = [min(lits, key=lambda l: len(conns[l]))]
|
|
803
|
+
|
|
804
|
+
for l in sorted(conns[am1[0]], key=lambda l: len(conns[l])):
|
|
805
|
+
if l in lits:
|
|
806
|
+
for l_added in am1[1:]:
|
|
807
|
+
if l_added not in conns[l]:
|
|
808
|
+
break
|
|
809
|
+
else:
|
|
810
|
+
am1.append(l)
|
|
811
|
+
|
|
812
|
+
# updating remaining lits and connections
|
|
813
|
+
lits.difference_update(set(am1))
|
|
814
|
+
for l in conns:
|
|
815
|
+
conns[l] = conns[l].difference(set(am1))
|
|
816
|
+
|
|
817
|
+
if len(am1) > 1:
|
|
818
|
+
# treat the new atmost1 relation
|
|
819
|
+
self.process_am1(am1)
|
|
820
|
+
nof_am1 += 1
|
|
821
|
+
len_am1.append(len(am1))
|
|
822
|
+
|
|
823
|
+
# updating the set of selectors
|
|
824
|
+
self.sels_set = set(self.sels)
|
|
825
|
+
|
|
826
|
+
if self.verbose > 1 and nof_am1:
|
|
827
|
+
print('c am1s found: {0}; avgsz: {1:.1f}; cost: {2}'.format(nof_am1,
|
|
828
|
+
sum(len_am1) / float(nof_am1), self.cost))
|
|
829
|
+
|
|
830
|
+
def process_am1(self, am1):
|
|
831
|
+
"""
|
|
832
|
+
Process an AtMost1 relation detected by :func:`adapt_am1`.
|
|
833
|
+
Note that given a set of soft clauses
|
|
834
|
+
:math:`\\mathcal{S}'` at most one of which can be
|
|
835
|
+
satisfied, one can immediately conclude that the formula
|
|
836
|
+
has cost at least :math:`|\\mathcal{S}'|-1` (assuming
|
|
837
|
+
*unweighted* MaxSAT). Furthermore, it is safe to replace
|
|
838
|
+
all clauses of :math:`\\mathcal{S}'` with a single soft
|
|
839
|
+
clause :math:`\\sum_{c\\in\\mathcal{S}'}{c}`.
|
|
840
|
+
|
|
841
|
+
Here, input parameter ``am1`` plays the role of subset
|
|
842
|
+
:math:`\\mathcal{S}'` mentioned above. The procedure bumps
|
|
843
|
+
the MaxSAT cost by ``self.minw * (len(am1) - 1)``.
|
|
844
|
+
|
|
845
|
+
All soft clauses involved in ``am1`` are replaced by a
|
|
846
|
+
single soft clause, which is a disjunction of the
|
|
847
|
+
selectors of clauses in ``am1``. The weight of the new
|
|
848
|
+
soft clause is set to ``self.minw``.
|
|
849
|
+
|
|
850
|
+
:param am1: a list of selectors connected by an AtMost1 constraint
|
|
851
|
+
|
|
852
|
+
:type am1: list(int)
|
|
853
|
+
"""
|
|
854
|
+
|
|
855
|
+
while len(am1) > 1:
|
|
856
|
+
# computing am1's weight
|
|
857
|
+
self.minw = min(map(lambda l: self.wght[l], am1))
|
|
858
|
+
|
|
859
|
+
# pretending am1 to be a core, and the bound is its size - 1
|
|
860
|
+
self.core_sels, b = am1, len(am1) - 1
|
|
861
|
+
|
|
862
|
+
# incrementing the cost
|
|
863
|
+
self.cost += b * self.minw
|
|
864
|
+
|
|
865
|
+
# splitting and relaxing if needed
|
|
866
|
+
self.process_sels()
|
|
867
|
+
|
|
868
|
+
# updating the list of literals in am1 after splitting the weights
|
|
869
|
+
am1 = [l for l in am1 if l not in self.garbage]
|
|
870
|
+
|
|
871
|
+
# new selector
|
|
872
|
+
selv = self.pool.id()
|
|
873
|
+
|
|
874
|
+
# adding a new clause
|
|
875
|
+
self.oracle.add_clause([-l for l in self.rels] + [-selv])
|
|
876
|
+
|
|
877
|
+
# integrating the new selector
|
|
878
|
+
self.sels.append(selv)
|
|
879
|
+
self.wght[selv] = self.minw
|
|
880
|
+
self.smap[selv] = len(self.wght) - 1
|
|
881
|
+
|
|
882
|
+
# removing unnecessary assumptions
|
|
883
|
+
self.filter_assumps()
|
|
884
|
+
|
|
885
|
+
def trim_core(self):
|
|
886
|
+
"""
|
|
887
|
+
This method trims a previously extracted unsatisfiable
|
|
888
|
+
core at most a given number of times. If a fixed point is
|
|
889
|
+
reached before that, the method returns.
|
|
890
|
+
"""
|
|
891
|
+
|
|
892
|
+
for i in range(self.trim):
|
|
893
|
+
# call solver with core assumption only
|
|
894
|
+
# it must return 'unsatisfiable'
|
|
895
|
+
self.oracle.solve(assumptions=self.core)
|
|
896
|
+
|
|
897
|
+
# extract a new core
|
|
898
|
+
new_core = self.oracle.get_core()
|
|
899
|
+
|
|
900
|
+
if len(new_core) == len(self.core):
|
|
901
|
+
# stop if new core is not better than the previous one
|
|
902
|
+
break
|
|
903
|
+
|
|
904
|
+
# otherwise, update core
|
|
905
|
+
self.core = new_core
|
|
906
|
+
|
|
907
|
+
def minimize_core(self):
|
|
908
|
+
"""
|
|
909
|
+
Reduce a previously extracted core and compute an
|
|
910
|
+
over-approximation of an MUS. This is done using the
|
|
911
|
+
simple deletion-based MUS extraction algorithm.
|
|
912
|
+
|
|
913
|
+
The idea is to try to deactivate soft clauses of the
|
|
914
|
+
unsatisfiable core one by one while checking if the
|
|
915
|
+
remaining soft clauses together with the hard part of the
|
|
916
|
+
formula are unsatisfiable. Clauses that are necessary for
|
|
917
|
+
preserving unsatisfiability comprise an MUS of the input
|
|
918
|
+
formula (it is contained in the given unsatisfiable core)
|
|
919
|
+
and are reported as a result of the procedure.
|
|
920
|
+
|
|
921
|
+
During this core minimization procedure, all SAT calls are
|
|
922
|
+
dropped after obtaining 1000 conflicts.
|
|
923
|
+
"""
|
|
924
|
+
|
|
925
|
+
if self.minz and len(self.core) > 1:
|
|
926
|
+
self.core = sorted(self.core, key=lambda l: self.wght[l])
|
|
927
|
+
self.oracle.conf_budget(1000)
|
|
928
|
+
|
|
929
|
+
i = 0
|
|
930
|
+
while i < len(self.core):
|
|
931
|
+
to_test = self.core[:i] + self.core[(i + 1):]
|
|
932
|
+
|
|
933
|
+
if self.oracle.solve_limited(assumptions=to_test) == False:
|
|
934
|
+
self.core = to_test
|
|
935
|
+
elif self.oracle.get_status() == True:
|
|
936
|
+
i += 1
|
|
937
|
+
else:
|
|
938
|
+
break
|
|
939
|
+
|
|
940
|
+
def exhaust_core(self, tobj):
|
|
941
|
+
"""
|
|
942
|
+
Exhaust core by increasing its bound as much as possible.
|
|
943
|
+
Core exhaustion was originally referred to as *cover
|
|
944
|
+
optimization* in [6]_.
|
|
945
|
+
|
|
946
|
+
Given a totalizer object ``tobj`` representing a sum of
|
|
947
|
+
some *relaxation* variables :math:`r\\in R` that augment
|
|
948
|
+
soft clauses :math:`\\mathcal{C}_r`, the idea is to
|
|
949
|
+
increase the right-hand side of the sum (which is equal to
|
|
950
|
+
1 by default) as much as possible, reaching a value
|
|
951
|
+
:math:`k` s.t. formula
|
|
952
|
+
:math:`\\mathcal{H}\\wedge\\mathcal{C}_r\\wedge(\\sum_{r\\in
|
|
953
|
+
R}{r\\leq k})` is still unsatisfiable while increasing it
|
|
954
|
+
further makes the formula satisfiable (here
|
|
955
|
+
:math:`\\mathcal{H}` denotes the hard part of the
|
|
956
|
+
formula).
|
|
957
|
+
|
|
958
|
+
The rationale is that calling an oracle incrementally on a
|
|
959
|
+
series of slightly modified formulas focusing only on the
|
|
960
|
+
recently computed unsatisfiable core and disregarding the
|
|
961
|
+
rest of the formula may be practically effective.
|
|
962
|
+
"""
|
|
963
|
+
|
|
964
|
+
# the first case is simpler
|
|
965
|
+
if self.oracle.solve(assumptions=[-tobj.rhs[1]]):
|
|
966
|
+
return 1
|
|
967
|
+
else:
|
|
968
|
+
self.cost += self.minw
|
|
969
|
+
|
|
970
|
+
for i in range(2, len(self.rels)):
|
|
971
|
+
# saving the previous bound
|
|
972
|
+
self.tobj[-tobj.rhs[i - 1]] = tobj
|
|
973
|
+
self.bnds[-tobj.rhs[i - 1]] = i - 1
|
|
974
|
+
|
|
975
|
+
# increasing the bound
|
|
976
|
+
self.update_sum(-tobj.rhs[i - 1])
|
|
977
|
+
|
|
978
|
+
if self.oracle.solve(assumptions=[-tobj.rhs[i]]):
|
|
979
|
+
# the bound should be equal to i
|
|
980
|
+
return i
|
|
981
|
+
|
|
982
|
+
# the cost should increase further
|
|
983
|
+
self.cost += self.minw
|
|
984
|
+
|
|
985
|
+
return None
|
|
986
|
+
|
|
987
|
+
def process_sels(self):
|
|
988
|
+
"""
|
|
989
|
+
Process soft clause selectors participating in a new core.
|
|
990
|
+
The negation :math:`\\neg{s}` of each selector literal
|
|
991
|
+
:math:`s` participating in the unsatisfiable core is added
|
|
992
|
+
to the list of relaxation literals, which will be later
|
|
993
|
+
used to create a new totalizer object in
|
|
994
|
+
:func:`create_sum`.
|
|
995
|
+
|
|
996
|
+
If the weight associated with a selector is equal to the
|
|
997
|
+
minimal weight of the core, e.g. ``self.minw``, the
|
|
998
|
+
selector is marked as garbage and will be removed in
|
|
999
|
+
:func:`filter_assumps`. Otherwise, the clause is split as
|
|
1000
|
+
described in [1]_.
|
|
1001
|
+
"""
|
|
1002
|
+
|
|
1003
|
+
# new relaxation variables
|
|
1004
|
+
self.rels = []
|
|
1005
|
+
|
|
1006
|
+
for l in self.core_sels:
|
|
1007
|
+
if self.wght[l] == self.minw:
|
|
1008
|
+
# marking variable as being a part of the core
|
|
1009
|
+
# so that next time it is not used as an assump
|
|
1010
|
+
self.garbage.add(l)
|
|
1011
|
+
else:
|
|
1012
|
+
# do not remove this variable from assumps
|
|
1013
|
+
# since it has a remaining non-zero weight
|
|
1014
|
+
self.wght[l] -= self.minw
|
|
1015
|
+
|
|
1016
|
+
# reuse assumption variable as relaxation
|
|
1017
|
+
self.rels.append(-l)
|
|
1018
|
+
|
|
1019
|
+
def process_sums(self):
|
|
1020
|
+
"""
|
|
1021
|
+
Process cardinality sums participating in a new core.
|
|
1022
|
+
Whenever necessary, some of the sum assumptions are
|
|
1023
|
+
removed or split (depending on the value of
|
|
1024
|
+
``self.minw``). Deleted sums are marked as garbage and are
|
|
1025
|
+
dealt with in :func:`filter_assumps`.
|
|
1026
|
+
|
|
1027
|
+
In some cases, the process involves updating the
|
|
1028
|
+
right-hand sides of the existing cardinality sums (see the
|
|
1029
|
+
call to :func:`update_sum`). The overall procedure is
|
|
1030
|
+
detailed in [1]_.
|
|
1031
|
+
"""
|
|
1032
|
+
|
|
1033
|
+
for l in self.core_sums:
|
|
1034
|
+
if self.wght[l] == self.minw:
|
|
1035
|
+
# marking variable as being a part of the core
|
|
1036
|
+
# so that next time it is not used as an assump
|
|
1037
|
+
self.garbage.add(l)
|
|
1038
|
+
else:
|
|
1039
|
+
# do not remove this variable from assumps
|
|
1040
|
+
# since it has a remaining non-zero weight
|
|
1041
|
+
self.wght[l] -= self.minw
|
|
1042
|
+
|
|
1043
|
+
# increase bound for the sum
|
|
1044
|
+
t, b = self.update_sum(l)
|
|
1045
|
+
|
|
1046
|
+
# updating bounds and weights
|
|
1047
|
+
if b < len(t.rhs):
|
|
1048
|
+
lnew = -t.rhs[b]
|
|
1049
|
+
if lnew not in self.swgt:
|
|
1050
|
+
self.set_bound(t, b, self.swgt[l])
|
|
1051
|
+
|
|
1052
|
+
# put this assumption to relaxation vars
|
|
1053
|
+
self.rels.append(-l)
|
|
1054
|
+
|
|
1055
|
+
def create_sum(self, bound=1):
|
|
1056
|
+
"""
|
|
1057
|
+
Create a totalizer object encoding a cardinality
|
|
1058
|
+
constraint on the new list of relaxation literals obtained
|
|
1059
|
+
in :func:`process_sels` and :func:`process_sums`. The
|
|
1060
|
+
clauses encoding the sum of the relaxation literals are
|
|
1061
|
+
added to the SAT oracle. The sum of the totalizer object
|
|
1062
|
+
is encoded up to the value of the input parameter
|
|
1063
|
+
``bound``, which is set to ``1`` by default.
|
|
1064
|
+
|
|
1065
|
+
:param bound: right-hand side for the sum to be created
|
|
1066
|
+
:type bound: int
|
|
1067
|
+
|
|
1068
|
+
:rtype: :class:`.ITotalizer`
|
|
1069
|
+
|
|
1070
|
+
Note that if Minicard is used as a SAT oracle, native
|
|
1071
|
+
cardinality constraints are used instead of
|
|
1072
|
+
:class:`.ITotalizer`.
|
|
1073
|
+
"""
|
|
1074
|
+
|
|
1075
|
+
if not self.oracle.supports_atmost(): # standard totalizer-based encoding
|
|
1076
|
+
# new totalizer sum
|
|
1077
|
+
t = ITotalizer(lits=self.rels, ubound=bound, top_id=self.pool.top)
|
|
1078
|
+
|
|
1079
|
+
# updating top variable id
|
|
1080
|
+
self.pool.top = t.top_id
|
|
1081
|
+
|
|
1082
|
+
# adding its clauses to oracle
|
|
1083
|
+
for cl in t.cnf.clauses:
|
|
1084
|
+
self.oracle.add_clause(cl)
|
|
1085
|
+
else:
|
|
1086
|
+
# for minicard, use native cardinality constraints instead of the
|
|
1087
|
+
# standard totalizer, i.e. create a new (empty) totalizer sum and
|
|
1088
|
+
# fill it with the necessary data supported by minicard
|
|
1089
|
+
t = ITotalizer()
|
|
1090
|
+
t.lits = self.rels
|
|
1091
|
+
|
|
1092
|
+
# a new variable will represent the bound
|
|
1093
|
+
bvar = self.pool.id()
|
|
1094
|
+
|
|
1095
|
+
# proper initial bound
|
|
1096
|
+
t.rhs = [None] * (len(t.lits))
|
|
1097
|
+
t.rhs[bound] = bvar
|
|
1098
|
+
|
|
1099
|
+
# new atmostb constraint instrumented with
|
|
1100
|
+
# an implication and represented natively
|
|
1101
|
+
rhs = len(t.lits)
|
|
1102
|
+
amb = [[-bvar] * (rhs - bound) + t.lits, rhs]
|
|
1103
|
+
|
|
1104
|
+
# add constraint to the solver
|
|
1105
|
+
self.oracle.add_atmost(*amb)
|
|
1106
|
+
|
|
1107
|
+
return t
|
|
1108
|
+
|
|
1109
|
+
def update_sum(self, assump):
|
|
1110
|
+
"""
|
|
1111
|
+
The method is used to increase the bound for a given
|
|
1112
|
+
totalizer sum. The totalizer object is identified by the
|
|
1113
|
+
input parameter ``assump``, which is an assumption literal
|
|
1114
|
+
associated with the totalizer object.
|
|
1115
|
+
|
|
1116
|
+
The method increases the bound for the totalizer sum,
|
|
1117
|
+
which involves adding the corresponding new clauses to the
|
|
1118
|
+
internal SAT oracle.
|
|
1119
|
+
|
|
1120
|
+
The method returns the totalizer object followed by the
|
|
1121
|
+
new bound obtained.
|
|
1122
|
+
|
|
1123
|
+
:param assump: assumption literal associated with the sum
|
|
1124
|
+
:type assump: int
|
|
1125
|
+
|
|
1126
|
+
:rtype: :class:`.ITotalizer`, int
|
|
1127
|
+
|
|
1128
|
+
Note that if Minicard is used as a SAT oracle, native
|
|
1129
|
+
cardinality constraints are used instead of
|
|
1130
|
+
:class:`.ITotalizer`.
|
|
1131
|
+
"""
|
|
1132
|
+
|
|
1133
|
+
# getting a totalizer object corresponding to assumption
|
|
1134
|
+
t = self.tobj[assump]
|
|
1135
|
+
|
|
1136
|
+
# increment the current bound
|
|
1137
|
+
b = self.bnds[assump] + 1
|
|
1138
|
+
|
|
1139
|
+
if not self.oracle.supports_atmost(): # the case of standard totalizer encoding
|
|
1140
|
+
# increasing its bound
|
|
1141
|
+
t.increase(ubound=b, top_id=self.pool.top)
|
|
1142
|
+
|
|
1143
|
+
# updating top variable id
|
|
1144
|
+
self.pool.top = t.top_id
|
|
1145
|
+
|
|
1146
|
+
# adding its clauses to oracle
|
|
1147
|
+
if t.nof_new:
|
|
1148
|
+
for cl in t.cnf.clauses[-t.nof_new:]:
|
|
1149
|
+
self.oracle.add_clause(cl)
|
|
1150
|
+
else: # the case of cardinality constraints represented natively
|
|
1151
|
+
# right-hand side is always equal to the number of input literals
|
|
1152
|
+
rhs = len(t.lits)
|
|
1153
|
+
|
|
1154
|
+
if b < rhs:
|
|
1155
|
+
# creating an additional bound
|
|
1156
|
+
if not t.rhs[b]:
|
|
1157
|
+
t.rhs[b] = self.pool.id()
|
|
1158
|
+
|
|
1159
|
+
# a new at-most-b constraint
|
|
1160
|
+
amb = [[-t.rhs[b]] * (rhs - b) + t.lits, rhs]
|
|
1161
|
+
self.oracle.add_atmost(*amb)
|
|
1162
|
+
|
|
1163
|
+
return t, b
|
|
1164
|
+
|
|
1165
|
+
def set_bound(self, tobj, rhs, weight=None):
|
|
1166
|
+
"""
|
|
1167
|
+
Given a totalizer sum, its right-hand side to be enforced, and a
|
|
1168
|
+
weight, the method creates a new sum assumption literal, which
|
|
1169
|
+
will be used in the following SAT oracle calls. If ``weight`` is
|
|
1170
|
+
left unspecified, the current core's weight, i.e. ``self.minw``,
|
|
1171
|
+
is used.
|
|
1172
|
+
|
|
1173
|
+
:param tobj: totalizer sum
|
|
1174
|
+
:param rhs: right-hand side
|
|
1175
|
+
:param weight: numeric weight of the assumption
|
|
1176
|
+
|
|
1177
|
+
:type tobj: :class:`.ITotalizer`
|
|
1178
|
+
:type rhs: int
|
|
1179
|
+
:type weight: int
|
|
1180
|
+
"""
|
|
1181
|
+
|
|
1182
|
+
if weight is None:
|
|
1183
|
+
# if no specific weight is provided, use the core's weight
|
|
1184
|
+
weight = self.minw
|
|
1185
|
+
|
|
1186
|
+
# saving the sum and its weight in a mapping
|
|
1187
|
+
self.tobj[-tobj.rhs[rhs]] = tobj
|
|
1188
|
+
self.bnds[-tobj.rhs[rhs]] = rhs
|
|
1189
|
+
self.wght[-tobj.rhs[rhs]] = weight
|
|
1190
|
+
self.swgt[-tobj.rhs[rhs]] = weight
|
|
1191
|
+
|
|
1192
|
+
# adding a new assumption to force the sum to be at most rhs
|
|
1193
|
+
self.sums.append(-tobj.rhs[rhs])
|
|
1194
|
+
|
|
1195
|
+
def filter_assumps(self):
|
|
1196
|
+
"""
|
|
1197
|
+
Filter out unnecessary selectors and sums from the list of
|
|
1198
|
+
assumption literals. The corresponding values are also
|
|
1199
|
+
removed from the dictionaries of bounds and weights.
|
|
1200
|
+
|
|
1201
|
+
Note that assumptions marked as garbage are collected in
|
|
1202
|
+
the core processing methods, i.e. in :func:`process_core`,
|
|
1203
|
+
:func:`process_sels`, and :func:`process_sums`.
|
|
1204
|
+
"""
|
|
1205
|
+
|
|
1206
|
+
# updating the list of selectors and sums
|
|
1207
|
+
self.sels = [l for l in self.sels if l not in self.garbage]
|
|
1208
|
+
self.sums = [l for l in self.sums if l not in self.garbage]
|
|
1209
|
+
|
|
1210
|
+
# cleaning the dictionaries
|
|
1211
|
+
for l in list(self.garbage):
|
|
1212
|
+
if l in self.bnds:
|
|
1213
|
+
del self.bnds[l]
|
|
1214
|
+
if l in self.wght:
|
|
1215
|
+
del self.wght[l]
|
|
1216
|
+
|
|
1217
|
+
# removing garbage from the set of selectors
|
|
1218
|
+
self.sels_set.difference_update(set(self.garbage))
|
|
1219
|
+
|
|
1220
|
+
self.garbage.clear()
|
|
1221
|
+
|
|
1222
|
+
def oracle_time(self):
|
|
1223
|
+
"""
|
|
1224
|
+
Report the total SAT solving time.
|
|
1225
|
+
"""
|
|
1226
|
+
|
|
1227
|
+
return self.oracle.time_accum()
|
|
1228
|
+
|
|
1229
|
+
def _map_extlit(self, l):
|
|
1230
|
+
"""
|
|
1231
|
+
Map an external variable to an internal one if necessary.
|
|
1232
|
+
|
|
1233
|
+
This method is used when new clauses are added to the
|
|
1234
|
+
formula incrementally, which may result in introducing new
|
|
1235
|
+
variables clashing with the previously used *clause
|
|
1236
|
+
selectors*. The method makes sure no clash occurs, i.e. it
|
|
1237
|
+
maps the original variables used in the new problem
|
|
1238
|
+
clauses to the newly introduced auxiliary variables (see
|
|
1239
|
+
:func:`add_clause`).
|
|
1240
|
+
|
|
1241
|
+
Given an integer literal, a fresh literal is returned. The
|
|
1242
|
+
returned integer has the same sign as the input literal.
|
|
1243
|
+
|
|
1244
|
+
:param l: literal to map
|
|
1245
|
+
:type l: int
|
|
1246
|
+
|
|
1247
|
+
:rtype: int
|
|
1248
|
+
"""
|
|
1249
|
+
|
|
1250
|
+
v = abs(l)
|
|
1251
|
+
|
|
1252
|
+
if v in self.vmap.e2i:
|
|
1253
|
+
return int(copysign(self.vmap.e2i[v], l))
|
|
1254
|
+
else:
|
|
1255
|
+
i = self.pool.id()
|
|
1256
|
+
|
|
1257
|
+
self.vmap.e2i[v] = i
|
|
1258
|
+
self.vmap.i2e[i] = v
|
|
1259
|
+
|
|
1260
|
+
return int(copysign(i, l))
|
|
1261
|
+
|
|
1262
|
+
|
|
1263
|
+
#
|
|
1264
|
+
#==============================================================================
|
|
1265
|
+
class RC2Stratified(RC2, object):
|
|
1266
|
+
"""
|
|
1267
|
+
RC2 augmented with BLO and stratification techniques. Although
|
|
1268
|
+
class :class:`RC2` can deal with weighted formulas, there are
|
|
1269
|
+
situations when it is necessary to apply additional heuristics
|
|
1270
|
+
to improve the performance of the solver on weighted MaxSAT
|
|
1271
|
+
formulas. This class extends capabilities of :class:`RC2` with
|
|
1272
|
+
two heuristics, namely
|
|
1273
|
+
|
|
1274
|
+
1. Boolean lexicographic optimization (BLO) [5]_
|
|
1275
|
+
2. diversity-based stratification [6]_
|
|
1276
|
+
3. cluster-based stratification
|
|
1277
|
+
|
|
1278
|
+
To specify which heuristics to apply, a user can assign the ``blo``
|
|
1279
|
+
parameter to one of the values (by default it is set to ``'div'``):
|
|
1280
|
+
|
|
1281
|
+
- ``'basic'`` ('BLO' only)
|
|
1282
|
+
- ``div`` ('BLO' + diversity-based stratification)
|
|
1283
|
+
- ``cluster`` ('BLO' + cluster-based stratification)
|
|
1284
|
+
- ``full`` ('BLO' + diversity- + cluster-based stratification)
|
|
1285
|
+
|
|
1286
|
+
Except for the aforementioned additional techniques, every other
|
|
1287
|
+
component of the solver remains as in the base class :class:`RC2`.
|
|
1288
|
+
Therefore, a user is referred to the documentation of :class:`RC2` for
|
|
1289
|
+
details.
|
|
1290
|
+
"""
|
|
1291
|
+
|
|
1292
|
+
def __init__(self, formula, solver='g3', adapt=False, blo='div',
|
|
1293
|
+
exhaust=False, incr=False, minz=False, nohard=False, process=0,
|
|
1294
|
+
trim=0, verbose=0):
|
|
1295
|
+
"""
|
|
1296
|
+
Constructor.
|
|
1297
|
+
"""
|
|
1298
|
+
|
|
1299
|
+
# calling the constructor for the basic version
|
|
1300
|
+
super(RC2Stratified, self).__init__(formula, solver=solver,
|
|
1301
|
+
adapt=adapt, exhaust=exhaust, incr=incr, minz=minz,
|
|
1302
|
+
process=process, trim=trim, verbose=verbose)
|
|
1303
|
+
|
|
1304
|
+
self.levl = 0 # initial optimization level
|
|
1305
|
+
self.blop = [] # a list of blo levels
|
|
1306
|
+
|
|
1307
|
+
# BLO strategy
|
|
1308
|
+
assert blo and blo in blomap, 'Unknown BLO strategy'
|
|
1309
|
+
self.bstr = blomap[blo]
|
|
1310
|
+
|
|
1311
|
+
# do clause hardening
|
|
1312
|
+
self.hard = nohard == False
|
|
1313
|
+
|
|
1314
|
+
# backing up selectors
|
|
1315
|
+
self.bckp, self.bckp_set = self.sels, self.sels_set
|
|
1316
|
+
self.sels = []
|
|
1317
|
+
|
|
1318
|
+
# initialize Boolean lexicographic optimization
|
|
1319
|
+
self.init_wstr()
|
|
1320
|
+
|
|
1321
|
+
def init_wstr(self):
|
|
1322
|
+
"""
|
|
1323
|
+
Compute and initialize optimization levels for BLO and
|
|
1324
|
+
stratification. This method is invoked once, from the
|
|
1325
|
+
constructor of an object of :class:`RC2Stratified`. Given
|
|
1326
|
+
the weights of the soft clauses, the method divides the
|
|
1327
|
+
MaxSAT problem into several optimization levels.
|
|
1328
|
+
"""
|
|
1329
|
+
|
|
1330
|
+
# a mapping for stratified problem solving,
|
|
1331
|
+
# i.e. from a weight to a list of selectors
|
|
1332
|
+
self.wstr = collections.defaultdict(lambda: [])
|
|
1333
|
+
|
|
1334
|
+
for s, w in six.iteritems(self.wght):
|
|
1335
|
+
self.wstr[w].append(s)
|
|
1336
|
+
|
|
1337
|
+
# sorted list of distinct weight levels
|
|
1338
|
+
self.blop = sorted([w for w in self.wstr], reverse=True)
|
|
1339
|
+
|
|
1340
|
+
# diversity parameter for stratification
|
|
1341
|
+
self.sdiv = len(self.blop) / 2.0
|
|
1342
|
+
|
|
1343
|
+
# number of finished levels
|
|
1344
|
+
self.done = 0
|
|
1345
|
+
|
|
1346
|
+
def compute(self):
|
|
1347
|
+
"""
|
|
1348
|
+
This method solves the MaxSAT problem iteratively. Each
|
|
1349
|
+
optimization level is tackled the standard way, i.e. by
|
|
1350
|
+
calling :func:`compute_`. A new level is started by
|
|
1351
|
+
calling :func:`next_level` and finished by calling
|
|
1352
|
+
:func:`finish_level`. Each new optimization level
|
|
1353
|
+
activates more soft clauses by invoking
|
|
1354
|
+
:func:`activate_clauses`.
|
|
1355
|
+
"""
|
|
1356
|
+
|
|
1357
|
+
if self.done == 0 and self.levl != None:
|
|
1358
|
+
# it is a fresh start of the solver
|
|
1359
|
+
# i.e. no optimization level is finished yet
|
|
1360
|
+
|
|
1361
|
+
# first attempt to get an optimization level
|
|
1362
|
+
self.next_level()
|
|
1363
|
+
|
|
1364
|
+
while self.levl != None and self.done < len(self.blop):
|
|
1365
|
+
# add more clauses
|
|
1366
|
+
self.done = self.activate_clauses(self.done)
|
|
1367
|
+
|
|
1368
|
+
if self.verbose > 1:
|
|
1369
|
+
print('c wght str:', self.blop[self.levl])
|
|
1370
|
+
|
|
1371
|
+
# call RC2
|
|
1372
|
+
if self.compute_() == False:
|
|
1373
|
+
return
|
|
1374
|
+
|
|
1375
|
+
# updating the list of distinct weight levels
|
|
1376
|
+
self.blop = sorted([w for w in self.wstr], reverse=True)
|
|
1377
|
+
|
|
1378
|
+
if self.done < len(self.blop):
|
|
1379
|
+
if self.verbose > 1:
|
|
1380
|
+
print('c curr opt:', self.cost)
|
|
1381
|
+
|
|
1382
|
+
# done with this level
|
|
1383
|
+
if self.hard:
|
|
1384
|
+
# harden the clauses if necessary
|
|
1385
|
+
self.finish_level()
|
|
1386
|
+
|
|
1387
|
+
self.levl += 1
|
|
1388
|
+
|
|
1389
|
+
# get another level
|
|
1390
|
+
self.next_level()
|
|
1391
|
+
|
|
1392
|
+
if self.verbose > 1:
|
|
1393
|
+
print('c')
|
|
1394
|
+
else:
|
|
1395
|
+
# we seem to be in the model enumeration mode
|
|
1396
|
+
# with the first model being already computed
|
|
1397
|
+
# i.e. all levels are finished and so all clauses are present
|
|
1398
|
+
# thus, we need to simply call RC2 for the next model
|
|
1399
|
+
self.done = -1 # we are done with stratification, disabling it
|
|
1400
|
+
if self.compute_() == False:
|
|
1401
|
+
return
|
|
1402
|
+
|
|
1403
|
+
# extracting a model
|
|
1404
|
+
self.model = self.oracle.get_model()
|
|
1405
|
+
|
|
1406
|
+
if self.model is None and self.pool.top == 0:
|
|
1407
|
+
# we seem to have been given an empty formula
|
|
1408
|
+
# so let's transform the None model returned to []
|
|
1409
|
+
self.model = []
|
|
1410
|
+
|
|
1411
|
+
self.model = filter(lambda l: abs(l) in self.vmap.i2e, self.model)
|
|
1412
|
+
self.model = map(lambda l: int(copysign(self.vmap.i2e[abs(l)], l)), self.model)
|
|
1413
|
+
self.model = sorted(self.model, key=lambda l: abs(l))
|
|
1414
|
+
|
|
1415
|
+
# if formula processing was used, we should
|
|
1416
|
+
# restore the model for the original formula
|
|
1417
|
+
if self.processor:
|
|
1418
|
+
self.model = self.processor.restore(self.model)
|
|
1419
|
+
|
|
1420
|
+
return self.model
|
|
1421
|
+
|
|
1422
|
+
def next_level(self):
|
|
1423
|
+
"""
|
|
1424
|
+
Compute the next optimization level (starting from the
|
|
1425
|
+
current one). The procedure represents a loop, each
|
|
1426
|
+
iteration of which checks whether or not one of the
|
|
1427
|
+
conditions holds:
|
|
1428
|
+
|
|
1429
|
+
- partial BLO condition
|
|
1430
|
+
- diversity-based stratification condition
|
|
1431
|
+
- cluster-based stratification condition
|
|
1432
|
+
|
|
1433
|
+
If any of these holds, the loop stops.
|
|
1434
|
+
"""
|
|
1435
|
+
|
|
1436
|
+
if self.levl >= len(self.blop):
|
|
1437
|
+
self.levl = None
|
|
1438
|
+
return
|
|
1439
|
+
|
|
1440
|
+
# determining which heuristics to use
|
|
1441
|
+
div_str, clu_str = (self.bstr >> 1) & 1, (self.bstr >> 2) & 1
|
|
1442
|
+
|
|
1443
|
+
# cluster of weights to build (if needed)
|
|
1444
|
+
cluster = [self.levl]
|
|
1445
|
+
|
|
1446
|
+
while self.levl < len(self.blop) - 1:
|
|
1447
|
+
# current weight
|
|
1448
|
+
wght = self.blop[self.levl]
|
|
1449
|
+
|
|
1450
|
+
# number of selectors with weight less than current weight
|
|
1451
|
+
numr = sum([len(self.wstr[w]) for w in self.blop[(self.levl + 1):]])
|
|
1452
|
+
|
|
1453
|
+
# sum of their weights
|
|
1454
|
+
sumr = sum([w * len(self.wstr[w]) for w in self.blop[(self.levl + 1):]])
|
|
1455
|
+
|
|
1456
|
+
# partial BLO
|
|
1457
|
+
if wght > sumr and sumr != 0:
|
|
1458
|
+
break
|
|
1459
|
+
|
|
1460
|
+
# diversity-based stratification
|
|
1461
|
+
if div_str and numr / float(len(self.blop) - self.levl - 1) > self.sdiv:
|
|
1462
|
+
break
|
|
1463
|
+
|
|
1464
|
+
# last resort = cluster-based stratification
|
|
1465
|
+
if clu_str:
|
|
1466
|
+
# is the distance from current weight to the cluster
|
|
1467
|
+
# being built larger than the distance to the mean of
|
|
1468
|
+
# smaller weights?
|
|
1469
|
+
numc = sum([len(self.wstr[self.blop[l]]) for l in cluster])
|
|
1470
|
+
sumc = sum([self.blop[l] * len(self.wstr[self.blop[l]]) for l in cluster])
|
|
1471
|
+
|
|
1472
|
+
if abs(wght - sumc / numc) > abs(wght - sumr / numr):
|
|
1473
|
+
# remaining weights are too far from the cluster; stop
|
|
1474
|
+
# here and report the splitting to be last-added weight
|
|
1475
|
+
self.levl = cluster[-1]
|
|
1476
|
+
break
|
|
1477
|
+
|
|
1478
|
+
cluster.append(self.levl)
|
|
1479
|
+
|
|
1480
|
+
self.levl += 1
|
|
1481
|
+
|
|
1482
|
+
def activate_clauses(self, beg):
|
|
1483
|
+
"""
|
|
1484
|
+
This method is used for activating the clauses that belong
|
|
1485
|
+
to optimization levels up to the newly computed level. It
|
|
1486
|
+
also reactivates previously deactivated clauses (see
|
|
1487
|
+
:func:`process_sels` and :func:`process_sums` for
|
|
1488
|
+
details).
|
|
1489
|
+
"""
|
|
1490
|
+
|
|
1491
|
+
end = min(self.levl + 1, len(self.blop))
|
|
1492
|
+
|
|
1493
|
+
for l in range(beg, end):
|
|
1494
|
+
for sel in self.wstr[self.blop[l]]:
|
|
1495
|
+
if sel in self.bckp_set:
|
|
1496
|
+
self.sels.append(sel)
|
|
1497
|
+
else:
|
|
1498
|
+
self.sums.append(sel)
|
|
1499
|
+
|
|
1500
|
+
# updating set of selectors
|
|
1501
|
+
self.sels_set = set(self.sels)
|
|
1502
|
+
|
|
1503
|
+
return end
|
|
1504
|
+
|
|
1505
|
+
def finish_level(self):
|
|
1506
|
+
"""
|
|
1507
|
+
This method does postprocessing of the current
|
|
1508
|
+
optimization level after it is solved. This includes
|
|
1509
|
+
*hardening* some of the soft clauses (depending on their
|
|
1510
|
+
remaining weights) and also garbage collection.
|
|
1511
|
+
"""
|
|
1512
|
+
|
|
1513
|
+
# assumptions to remove
|
|
1514
|
+
self.garbage = set()
|
|
1515
|
+
|
|
1516
|
+
# sum of weights of the remaining levels
|
|
1517
|
+
sumw = sum([w * len(self.wstr[w]) for w in self.blop[(self.levl + 1):]])
|
|
1518
|
+
|
|
1519
|
+
# trying to harden selectors and sums
|
|
1520
|
+
for s in self.sels + self.sums:
|
|
1521
|
+
if self.wght[s] > sumw:
|
|
1522
|
+
self.oracle.add_clause([s])
|
|
1523
|
+
self.garbage.add(s)
|
|
1524
|
+
|
|
1525
|
+
if self.verbose > 1:
|
|
1526
|
+
print('c hardened:', len(self.garbage))
|
|
1527
|
+
|
|
1528
|
+
# remove unnecessary assumptions
|
|
1529
|
+
self.filter_assumps()
|
|
1530
|
+
|
|
1531
|
+
def process_am1(self, am1):
|
|
1532
|
+
"""
|
|
1533
|
+
Due to the solving process involving multiple optimization
|
|
1534
|
+
levels to be treated individually, new soft clauses for
|
|
1535
|
+
the detected intrinsic AtMost1 constraints should be
|
|
1536
|
+
remembered. The method is a slightly modified version of
|
|
1537
|
+
the base method :func:`RC2.process_am1` taking care of
|
|
1538
|
+
this.
|
|
1539
|
+
"""
|
|
1540
|
+
|
|
1541
|
+
# assumptions to remove
|
|
1542
|
+
self.garbage = set()
|
|
1543
|
+
|
|
1544
|
+
# clauses to deactivate
|
|
1545
|
+
to_deactivate = set([])
|
|
1546
|
+
|
|
1547
|
+
while len(am1) > 1:
|
|
1548
|
+
# computing am1's weight
|
|
1549
|
+
self.minw = min(map(lambda l: self.wght[l], am1))
|
|
1550
|
+
|
|
1551
|
+
# pretending am1 to be a core, and the bound is its size - 1
|
|
1552
|
+
self.core_sels, b = am1, len(am1) - 1
|
|
1553
|
+
|
|
1554
|
+
# incrementing the cost
|
|
1555
|
+
self.cost += b * self.minw
|
|
1556
|
+
|
|
1557
|
+
# splitting and relaxing if needed
|
|
1558
|
+
super(RC2Stratified, self).process_sels()
|
|
1559
|
+
|
|
1560
|
+
# updating the list of literals in am1 after splitting the weights
|
|
1561
|
+
am1 = [l for l in am1 if l not in self.garbage]
|
|
1562
|
+
|
|
1563
|
+
# new selector
|
|
1564
|
+
selv = self.pool.id()
|
|
1565
|
+
|
|
1566
|
+
# adding a new clause
|
|
1567
|
+
self.oracle.add_clause([-l for l in self.rels] + [-selv])
|
|
1568
|
+
|
|
1569
|
+
# integrating the new selector
|
|
1570
|
+
self.sels.append(selv)
|
|
1571
|
+
self.wght[selv] = self.minw
|
|
1572
|
+
self.smap[selv] = len(self.wght) - 1
|
|
1573
|
+
|
|
1574
|
+
# do not forget this newly selector!
|
|
1575
|
+
self.bckp_set.add(selv)
|
|
1576
|
+
|
|
1577
|
+
if self.done != -1 and self.wght[selv] < self.blop[self.levl]:
|
|
1578
|
+
self.wstr[self.wght[selv]].append(selv)
|
|
1579
|
+
to_deactivate.add(selv)
|
|
1580
|
+
|
|
1581
|
+
# marking all remaining literals with small weights to be deactivated
|
|
1582
|
+
for l in am1:
|
|
1583
|
+
if self.done != -1 and self.wght[l] < self.blop[self.levl]:
|
|
1584
|
+
self.wstr[self.wght[l]].append(l)
|
|
1585
|
+
to_deactivate.add(l)
|
|
1586
|
+
|
|
1587
|
+
# deactivating unnecessary selectors
|
|
1588
|
+
self.sels = [l for l in self.sels if l not in to_deactivate]
|
|
1589
|
+
|
|
1590
|
+
# removing unnecessary assumptions
|
|
1591
|
+
self.filter_assumps()
|
|
1592
|
+
|
|
1593
|
+
def process_sels(self):
|
|
1594
|
+
"""
|
|
1595
|
+
A redefined version of :func:`RC2.process_sels`. The only
|
|
1596
|
+
modification affects the clauses whose weight after
|
|
1597
|
+
splitting becomes less than the weight of the current
|
|
1598
|
+
optimization level. Such clauses are deactivated and to be
|
|
1599
|
+
reactivated at a later stage.
|
|
1600
|
+
"""
|
|
1601
|
+
|
|
1602
|
+
# new relaxation variables
|
|
1603
|
+
self.rels = []
|
|
1604
|
+
|
|
1605
|
+
# selectors that should be deactivated (but not removed completely)
|
|
1606
|
+
to_deactivate = set([])
|
|
1607
|
+
|
|
1608
|
+
for l in self.core_sels:
|
|
1609
|
+
if self.wght[l] == self.minw:
|
|
1610
|
+
# marking variable as being a part of the core
|
|
1611
|
+
# so that next time it is not used as an assump
|
|
1612
|
+
self.garbage.add(l)
|
|
1613
|
+
else:
|
|
1614
|
+
# do not remove this variable from assumps
|
|
1615
|
+
# since it has a remaining non-zero weight
|
|
1616
|
+
self.wght[l] -= self.minw
|
|
1617
|
+
|
|
1618
|
+
# deactivate this assumption and put at a lower level
|
|
1619
|
+
# if self.done != -1, i.e. if stratification is disabled
|
|
1620
|
+
if self.done != -1 and self.wght[l] < self.blop[self.levl]:
|
|
1621
|
+
self.wstr[self.wght[l]].append(l)
|
|
1622
|
+
to_deactivate.add(l)
|
|
1623
|
+
|
|
1624
|
+
# reuse assumption variable as relaxation
|
|
1625
|
+
self.rels.append(-l)
|
|
1626
|
+
|
|
1627
|
+
# deactivating unnecessary selectors
|
|
1628
|
+
self.sels = [l for l in self.sels if l not in to_deactivate]
|
|
1629
|
+
|
|
1630
|
+
def process_sums(self):
|
|
1631
|
+
"""
|
|
1632
|
+
A redefined version of :func:`RC2.process_sums`. The only
|
|
1633
|
+
modification affects the clauses whose weight after
|
|
1634
|
+
splitting becomes less than the weight of the current
|
|
1635
|
+
optimization level. Such clauses are deactivated and to be
|
|
1636
|
+
reactivated at a later stage.
|
|
1637
|
+
"""
|
|
1638
|
+
|
|
1639
|
+
# sums that should be deactivated (but not removed completely)
|
|
1640
|
+
to_deactivate = set([])
|
|
1641
|
+
|
|
1642
|
+
for l in self.core_sums:
|
|
1643
|
+
if self.wght[l] == self.minw:
|
|
1644
|
+
# marking variable as being a part of the core
|
|
1645
|
+
# so that next time it is not used as an assump
|
|
1646
|
+
self.garbage.add(l)
|
|
1647
|
+
else:
|
|
1648
|
+
# do not remove this variable from assumps
|
|
1649
|
+
# since it has a remaining non-zero weight
|
|
1650
|
+
self.wght[l] -= self.minw
|
|
1651
|
+
|
|
1652
|
+
# deactivate this assumption and put at a lower level
|
|
1653
|
+
# if self.done != -1, i.e. if stratification is disabled
|
|
1654
|
+
if self.done != -1 and self.wght[l] < self.blop[self.levl]:
|
|
1655
|
+
self.wstr[self.wght[l]].append(l)
|
|
1656
|
+
to_deactivate.add(l)
|
|
1657
|
+
|
|
1658
|
+
# increase bound for the sum
|
|
1659
|
+
t, b = self.update_sum(l)
|
|
1660
|
+
|
|
1661
|
+
# updating bounds and weights
|
|
1662
|
+
if b < len(t.rhs):
|
|
1663
|
+
lnew = -t.rhs[b]
|
|
1664
|
+
if lnew not in self.swgt:
|
|
1665
|
+
self.set_bound(t, b, self.swgt[l])
|
|
1666
|
+
|
|
1667
|
+
# put this assumption to relaxation vars
|
|
1668
|
+
self.rels.append(-l)
|
|
1669
|
+
|
|
1670
|
+
# deactivating unnecessary sums
|
|
1671
|
+
self.sums = [l for l in self.sums if l not in to_deactivate]
|
|
1672
|
+
|
|
1673
|
+
|
|
1674
|
+
#
|
|
1675
|
+
#==============================================================================
|
|
1676
|
+
def parse_options():
|
|
1677
|
+
"""
|
|
1678
|
+
Parses command-line option
|
|
1679
|
+
"""
|
|
1680
|
+
|
|
1681
|
+
try:
|
|
1682
|
+
opts, args = getopt.getopt(sys.argv[1:], 'ab:c:e:hil:mp:s:t:vx',
|
|
1683
|
+
['adapt', 'block=', 'comp=', 'enum=', 'exhaust', 'help',
|
|
1684
|
+
'incr', 'blo=', 'minimize', 'process=', 'solver=',
|
|
1685
|
+
'trim=', 'verbose', 'vnew'])
|
|
1686
|
+
except getopt.GetoptError as err:
|
|
1687
|
+
sys.stderr.write(str(err).capitalize())
|
|
1688
|
+
usage()
|
|
1689
|
+
sys.exit(1)
|
|
1690
|
+
|
|
1691
|
+
adapt = False
|
|
1692
|
+
block = 'model'
|
|
1693
|
+
exhaust = False
|
|
1694
|
+
cmode = None
|
|
1695
|
+
to_enum = 1
|
|
1696
|
+
incr = False
|
|
1697
|
+
blo = 'none'
|
|
1698
|
+
minz = False
|
|
1699
|
+
process = 0
|
|
1700
|
+
solver = 'g3'
|
|
1701
|
+
trim = 0
|
|
1702
|
+
verbose = 1
|
|
1703
|
+
vnew = False
|
|
1704
|
+
|
|
1705
|
+
for opt, arg in opts:
|
|
1706
|
+
if opt in ('-a', '--adapt'):
|
|
1707
|
+
adapt = True
|
|
1708
|
+
elif opt in ('-b', '--block'):
|
|
1709
|
+
block = str(arg)
|
|
1710
|
+
elif opt in ('-c', '--comp'):
|
|
1711
|
+
cmode = str(arg)
|
|
1712
|
+
elif opt in ('-e', '--enum'):
|
|
1713
|
+
to_enum = str(arg)
|
|
1714
|
+
if to_enum != 'all':
|
|
1715
|
+
to_enum = int(to_enum)
|
|
1716
|
+
else:
|
|
1717
|
+
to_enum = 0
|
|
1718
|
+
elif opt in ('-h', '--help'):
|
|
1719
|
+
usage()
|
|
1720
|
+
sys.exit(0)
|
|
1721
|
+
elif opt in ('-i', '--incr'):
|
|
1722
|
+
incr = True
|
|
1723
|
+
elif opt in ('-l', '--blo'):
|
|
1724
|
+
blo = str(arg)
|
|
1725
|
+
elif opt in ('-m', '--minimize'):
|
|
1726
|
+
minz = True
|
|
1727
|
+
elif opt in ('-p', '--process'):
|
|
1728
|
+
process = int(arg)
|
|
1729
|
+
elif opt in ('-s', '--solver'):
|
|
1730
|
+
solver = str(arg)
|
|
1731
|
+
elif opt in ('-t', '--trim'):
|
|
1732
|
+
trim = int(arg)
|
|
1733
|
+
elif opt in ('-v', '--verbose'):
|
|
1734
|
+
verbose += 1
|
|
1735
|
+
elif opt == '--vnew':
|
|
1736
|
+
vnew = True
|
|
1737
|
+
elif opt in ('-x', '--exhaust'):
|
|
1738
|
+
exhaust = True
|
|
1739
|
+
else:
|
|
1740
|
+
assert False, 'Unhandled option: {0} {1}'.format(opt, arg)
|
|
1741
|
+
|
|
1742
|
+
# solution blocking
|
|
1743
|
+
bmap = {'mcs': -1, 'mcses': -1, 'model': 0, 'models': 0, 'mss': 1, 'msses': 1}
|
|
1744
|
+
assert block in bmap, 'Unknown solution blocking'
|
|
1745
|
+
block = bmap[block]
|
|
1746
|
+
|
|
1747
|
+
return adapt, blo, block, cmode, to_enum, exhaust, incr, minz, \
|
|
1748
|
+
process, solver, trim, verbose, vnew, args
|
|
1749
|
+
|
|
1750
|
+
|
|
1751
|
+
#
|
|
1752
|
+
#==============================================================================
|
|
1753
|
+
def usage():
|
|
1754
|
+
"""
|
|
1755
|
+
Prints usage message.
|
|
1756
|
+
"""
|
|
1757
|
+
|
|
1758
|
+
print('Usage:', os.path.basename(sys.argv[0]), '[options] dimacs-file')
|
|
1759
|
+
print('Options:')
|
|
1760
|
+
print(' -a, --adapt Try to adapt (simplify) input formula')
|
|
1761
|
+
print(' -b, --block=<string> When enumerating MaxSAT models, how to block previous solutions')
|
|
1762
|
+
print(' Available values: mcs, model, mss (default = model)')
|
|
1763
|
+
print(' -c, --comp=<string> Enable one of the MSE18 configurations')
|
|
1764
|
+
print(' Available values: a, b, none (default = none)')
|
|
1765
|
+
print(' -e, --enum=<int> Number of MaxSAT models to compute')
|
|
1766
|
+
print(' Available values: [1 .. INT_MAX], all (default = 1)')
|
|
1767
|
+
print(' -h, --help Show this message')
|
|
1768
|
+
print(' -i, --incr Use SAT solver incrementally (only for g3 and g4)')
|
|
1769
|
+
print(' -l, --blo=<string> Use BLO and stratification')
|
|
1770
|
+
print(' Available values: basic, div, cluster, none, full (default = none)')
|
|
1771
|
+
print(' -m, --minimize Use a heuristic unsatisfiable core minimizer')
|
|
1772
|
+
print(' -p, --process=<int> Number of processing rounds')
|
|
1773
|
+
print(' Available values: [0 .. INT_MAX] (default = 0)')
|
|
1774
|
+
print(' -s, --solver=<string> SAT solver to use')
|
|
1775
|
+
print(' Available values: cd15, cd19, g3, g4, lgl, mcb, mcm, mpl, m22, mc, mgh (default = g3)')
|
|
1776
|
+
print(' -t, --trim=<int> How many times to trim unsatisfiable cores')
|
|
1777
|
+
print(' Available values: [0 .. INT_MAX] (default = 0)')
|
|
1778
|
+
print(' -v, --verbose Be verbose')
|
|
1779
|
+
print(' --vnew Print v-line in the new format')
|
|
1780
|
+
print(' -x, --exhaust Exhaust new unsatisfiable cores')
|
|
1781
|
+
|
|
1782
|
+
|
|
1783
|
+
#
|
|
1784
|
+
#==============================================================================
|
|
1785
|
+
if __name__ == '__main__':
|
|
1786
|
+
adapt, blo, block, cmode, to_enum, exhaust, incr, minz, process, solver, \
|
|
1787
|
+
trim, verbose, vnew, files = parse_options()
|
|
1788
|
+
|
|
1789
|
+
if files:
|
|
1790
|
+
# parsing the input formula
|
|
1791
|
+
if re.search(r'\.wcnf[p|+]?(\.(gz|bz2|lzma|xz|zst))?$', files[0]):
|
|
1792
|
+
formula = WCNFPlus(from_file=files[0])
|
|
1793
|
+
else: # expecting '*.cnf[,p,+].*'
|
|
1794
|
+
formula = CNFPlus(from_file=files[0]).weighted()
|
|
1795
|
+
|
|
1796
|
+
# enabling the competition mode
|
|
1797
|
+
if cmode:
|
|
1798
|
+
assert cmode in ('a', 'b'), 'Wrong MSE18 mode chosen: {0}'.format(cmode)
|
|
1799
|
+
adapt, blo, exhaust, solver, verbose = True, 'div', True, 'g3', 3
|
|
1800
|
+
|
|
1801
|
+
if cmode == 'a':
|
|
1802
|
+
trim = 5 if max(formula.wght) > min(formula.wght) else 0
|
|
1803
|
+
minz = False
|
|
1804
|
+
else:
|
|
1805
|
+
trim, minz = 0, True
|
|
1806
|
+
|
|
1807
|
+
# trying to use unbuffered standard output
|
|
1808
|
+
if sys.version_info.major == 2:
|
|
1809
|
+
sys.stdout = os.fdopen(sys.stdout.fileno(), 'w', 0)
|
|
1810
|
+
|
|
1811
|
+
# deciding whether or not to stratify
|
|
1812
|
+
if blo != 'none' and max(formula.wght) > min(formula.wght):
|
|
1813
|
+
MXS = RC2Stratified
|
|
1814
|
+
else:
|
|
1815
|
+
MXS = RC2
|
|
1816
|
+
|
|
1817
|
+
# starting the solver
|
|
1818
|
+
with MXS(formula, solver=solver, adapt=adapt, exhaust=exhaust,
|
|
1819
|
+
incr=incr, minz=minz, process=process, trim=trim,
|
|
1820
|
+
verbose=verbose) as rc2:
|
|
1821
|
+
|
|
1822
|
+
if isinstance(rc2, RC2Stratified):
|
|
1823
|
+
rc2.bstr = blomap[blo] # select blo strategy
|
|
1824
|
+
if to_enum != 1:
|
|
1825
|
+
# no clause hardening in case we enumerate multiple models
|
|
1826
|
+
print('c hardening is disabled for model enumeration')
|
|
1827
|
+
rc2.hard = False
|
|
1828
|
+
|
|
1829
|
+
optimum_found = False
|
|
1830
|
+
for i, model in enumerate(rc2.enumerate(block=block), 1):
|
|
1831
|
+
optimum_found = True
|
|
1832
|
+
|
|
1833
|
+
if verbose:
|
|
1834
|
+
if i == 1:
|
|
1835
|
+
print('s OPTIMUM FOUND')
|
|
1836
|
+
print('o {0}'.format(rc2.cost))
|
|
1837
|
+
|
|
1838
|
+
if verbose > 2:
|
|
1839
|
+
if vnew: # new format of the v-line
|
|
1840
|
+
print('v', ''.join(str(int(l > 0)) for l in model))
|
|
1841
|
+
else:
|
|
1842
|
+
print('v', ' '.join([str(l) for l in model]))
|
|
1843
|
+
|
|
1844
|
+
if i == to_enum:
|
|
1845
|
+
break
|
|
1846
|
+
else:
|
|
1847
|
+
# needed for MSE'20
|
|
1848
|
+
if verbose > 2 and vnew and to_enum != 1 and block == 1:
|
|
1849
|
+
print('v')
|
|
1850
|
+
|
|
1851
|
+
if verbose:
|
|
1852
|
+
if not optimum_found:
|
|
1853
|
+
print('s UNSATISFIABLE')
|
|
1854
|
+
elif to_enum != 1:
|
|
1855
|
+
print('c models found:', i)
|
|
1856
|
+
|
|
1857
|
+
if verbose > 1:
|
|
1858
|
+
print('c oracle time: {0:.4f}'.format(rc2.oracle_time()))
|