python-sat 1.8.dev15__cp313-cp313-win_amd64.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.

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