python-sat 1.8.dev25__cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of python-sat might be problematic. Click here for more details.
- pycard.cpython-312-x86_64-linux-gnu.so +0 -0
- pysat/__init__.py +24 -0
- pysat/_fileio.py +209 -0
- pysat/_utils.py +58 -0
- pysat/allies/__init__.py +0 -0
- pysat/allies/approxmc.py +385 -0
- pysat/allies/unigen.py +435 -0
- pysat/card.py +802 -0
- pysat/engines.py +1302 -0
- pysat/examples/__init__.py +0 -0
- pysat/examples/bbscan.py +663 -0
- pysat/examples/bica.py +691 -0
- pysat/examples/fm.py +527 -0
- pysat/examples/genhard.py +516 -0
- pysat/examples/hitman.py +653 -0
- pysat/examples/lbx.py +638 -0
- pysat/examples/lsu.py +496 -0
- pysat/examples/mcsls.py +610 -0
- pysat/examples/models.py +189 -0
- pysat/examples/musx.py +344 -0
- pysat/examples/optux.py +710 -0
- pysat/examples/primer.py +620 -0
- pysat/examples/rc2.py +1858 -0
- pysat/examples/usage.py +63 -0
- pysat/formula.py +5619 -0
- pysat/pb.py +463 -0
- pysat/process.py +363 -0
- pysat/solvers.py +7591 -0
- pysolvers.cpython-312-x86_64-linux-gnu.so +0 -0
- python_sat-1.8.dev25.data/scripts/approxmc.py +385 -0
- python_sat-1.8.dev25.data/scripts/bbscan.py +663 -0
- python_sat-1.8.dev25.data/scripts/bica.py +691 -0
- python_sat-1.8.dev25.data/scripts/fm.py +527 -0
- python_sat-1.8.dev25.data/scripts/genhard.py +516 -0
- python_sat-1.8.dev25.data/scripts/lbx.py +638 -0
- python_sat-1.8.dev25.data/scripts/lsu.py +496 -0
- python_sat-1.8.dev25.data/scripts/mcsls.py +610 -0
- python_sat-1.8.dev25.data/scripts/models.py +189 -0
- python_sat-1.8.dev25.data/scripts/musx.py +344 -0
- python_sat-1.8.dev25.data/scripts/optux.py +710 -0
- python_sat-1.8.dev25.data/scripts/primer.py +620 -0
- python_sat-1.8.dev25.data/scripts/rc2.py +1858 -0
- python_sat-1.8.dev25.data/scripts/unigen.py +435 -0
- python_sat-1.8.dev25.dist-info/METADATA +45 -0
- python_sat-1.8.dev25.dist-info/RECORD +48 -0
- python_sat-1.8.dev25.dist-info/WHEEL +6 -0
- python_sat-1.8.dev25.dist-info/licenses/LICENSE.txt +21 -0
- python_sat-1.8.dev25.dist-info/top_level.txt +3 -0
pysat/engines.py
ADDED
|
@@ -0,0 +1,1302 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
#-*- coding:utf-8 -*-
|
|
3
|
+
##
|
|
4
|
+
## engines.py
|
|
5
|
+
##
|
|
6
|
+
## Created on: Sep 20, 2023
|
|
7
|
+
## Author: Alexey Ignatiev, Zi Li Tan
|
|
8
|
+
## E-mail: alexey.ignatiev@monash.edu, ztan0050@student.monash.edu
|
|
9
|
+
##
|
|
10
|
+
|
|
11
|
+
"""
|
|
12
|
+
===============
|
|
13
|
+
List of classes
|
|
14
|
+
===============
|
|
15
|
+
|
|
16
|
+
.. autosummary::
|
|
17
|
+
:nosignatures:
|
|
18
|
+
|
|
19
|
+
Propagator
|
|
20
|
+
BooleanEngine
|
|
21
|
+
LinearConstraint
|
|
22
|
+
ParityConstraint
|
|
23
|
+
|
|
24
|
+
==================
|
|
25
|
+
Module description
|
|
26
|
+
==================
|
|
27
|
+
|
|
28
|
+
This module provides a user with the possibility to define their own
|
|
29
|
+
propagation engines, i.e. constraint propagators, attachable to a SAT
|
|
30
|
+
solver. The implementation of this functionality builds on the use of the
|
|
31
|
+
IPASIR-UP interface [1]_. This may come in handy when it is beneficial to
|
|
32
|
+
reason over non-clausal constraints, for example, in the settings of
|
|
33
|
+
satisfiability modulo theories (SMT), constraint programming (CP) and lazy
|
|
34
|
+
clause generation (LCG).
|
|
35
|
+
|
|
36
|
+
.. [1] Katalin Fazekas, Aina Niemetz, Mathias Preiner, Markus Kirchweger,
|
|
37
|
+
Stefan Szeider, Armin Biere. *IPASIR-UP: User Propagators for CDCL*.
|
|
38
|
+
SAT. 2023. pp. 8:1-8:13
|
|
39
|
+
|
|
40
|
+
.. note::
|
|
41
|
+
|
|
42
|
+
Currently, the only SAT solver available in PySAT supporting the
|
|
43
|
+
interface is CaDiCaL 1.9.5.
|
|
44
|
+
|
|
45
|
+
The interface allows a user to attach a single reasoning engine to the
|
|
46
|
+
solver. This means that if one needs to support multiple kinds of
|
|
47
|
+
constraints simultaneously, the implementation of the engine may need to
|
|
48
|
+
be sophisticated enough to make it work.
|
|
49
|
+
|
|
50
|
+
It is imperative that any propagator a user defines must inherit the
|
|
51
|
+
interface of the abstract class :class:`Propagator` and defines all the
|
|
52
|
+
required methods for the correct operation of the engine.
|
|
53
|
+
|
|
54
|
+
An example propagator is shown in the class :class:`BooleanEngine`. It
|
|
55
|
+
currently supports two kinds of example constraints: linear (cardinality
|
|
56
|
+
and pseudo-Boolean) constraints and parity (exclusive OR, XOR)
|
|
57
|
+
constraints. The engine can run in the *adaptive mode*, i.e. it can enable
|
|
58
|
+
and disable itself on the fly.
|
|
59
|
+
|
|
60
|
+
Once an engine is implemented, it should be attached to a solver object by
|
|
61
|
+
calling :meth:`connect_propagator` of :class:`Cadical195`. The propagator
|
|
62
|
+
will then need to inform the solver what variable it requires to observe.
|
|
63
|
+
|
|
64
|
+
.. code-block:: python
|
|
65
|
+
|
|
66
|
+
solver = Solver(name='cadical195', bootstrap_with=some_formula)
|
|
67
|
+
|
|
68
|
+
engine = MyPowerfulEngine(...)
|
|
69
|
+
solver.connect_propagator(engine)
|
|
70
|
+
|
|
71
|
+
# attached propagator wants to observe these variables
|
|
72
|
+
for var in range(some_variables):
|
|
73
|
+
solver.observe(var)
|
|
74
|
+
|
|
75
|
+
...
|
|
76
|
+
|
|
77
|
+
.. note::
|
|
78
|
+
|
|
79
|
+
A user is encouraged to examine the source code of
|
|
80
|
+
:class:`BooleanEngine` in order to see how an external reasoning
|
|
81
|
+
engine can be implemented and attached to CaDiCaL 1.9.5. Also consult
|
|
82
|
+
the implementation of the corresponding methods of
|
|
83
|
+
:class:`.Cadical195`.
|
|
84
|
+
|
|
85
|
+
==============
|
|
86
|
+
Module details
|
|
87
|
+
==============
|
|
88
|
+
"""
|
|
89
|
+
|
|
90
|
+
#
|
|
91
|
+
#==============================================================================
|
|
92
|
+
from collections import Counter, defaultdict
|
|
93
|
+
import functools
|
|
94
|
+
import itertools
|
|
95
|
+
from typing import List
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
#
|
|
99
|
+
#==============================================================================
|
|
100
|
+
class LinearConstraint:
|
|
101
|
+
"""
|
|
102
|
+
A possible implementation of linear constraints over Boolean
|
|
103
|
+
variables, including cardinality and pseudo-Boolean constraints. Each
|
|
104
|
+
such constraint is meant to be in the less-than form, i.e. a user
|
|
105
|
+
should transform the literals, weights and the right-hand side of the
|
|
106
|
+
constraint into this form before creating an object of
|
|
107
|
+
:class:`LinearConstraint`. The class is designed to work with
|
|
108
|
+
:class:`BooleanEngine`.
|
|
109
|
+
|
|
110
|
+
The implementation of linear constraint propagation builds on the use
|
|
111
|
+
of counters. Basically, each time a literal is assigned to a positive
|
|
112
|
+
value, it is assumed to contribute to the total weight on the
|
|
113
|
+
left-hand side of the constraint, which is calculated and compared to
|
|
114
|
+
the right-hand side.
|
|
115
|
+
|
|
116
|
+
The constructor receives three arguments: ``lits``, ``weights``, and
|
|
117
|
+
``bound``. Argument ``lits`` represents a list of literals on the
|
|
118
|
+
left-hand side of the constraint while argument ``weights`` contains
|
|
119
|
+
either a list of their weights or a dictionary mapping literals to
|
|
120
|
+
weights. Finally, argument ``bound`` is the right-hand side of the
|
|
121
|
+
constraint.
|
|
122
|
+
|
|
123
|
+
Note that if no weights are provided, each occurrence of a literal is
|
|
124
|
+
assumed to have weight 1.
|
|
125
|
+
|
|
126
|
+
.. note::
|
|
127
|
+
|
|
128
|
+
All weights are supposed to be non-negative values.
|
|
129
|
+
|
|
130
|
+
:param lits: list of literals (left-hand side)
|
|
131
|
+
:param weights: weights of the literals
|
|
132
|
+
:param bound: right-hand side of the constraint
|
|
133
|
+
|
|
134
|
+
:type lits:
|
|
135
|
+
:type weights: list or dict
|
|
136
|
+
:type bound: int or float
|
|
137
|
+
"""
|
|
138
|
+
|
|
139
|
+
def __init__(self, lits=[], weights={}, bound=1):
|
|
140
|
+
"""
|
|
141
|
+
Constraint initialiser.
|
|
142
|
+
"""
|
|
143
|
+
|
|
144
|
+
if not weights or type(weights) == dict:
|
|
145
|
+
# the same literal may appear multiple times in the list
|
|
146
|
+
# so we need to calculate it's actual weight
|
|
147
|
+
cntr = Counter(lits)
|
|
148
|
+
|
|
149
|
+
# left-hand side in terms of literals and their weights
|
|
150
|
+
self.lits = sorted(cntr.keys())
|
|
151
|
+
self.wght = {l: cntr[l] * (weights[l] if weights else 1) for l in self.lits}
|
|
152
|
+
else:
|
|
153
|
+
assert len(lits) == len(weights), 'The number of weights differs from the number of literals'
|
|
154
|
+
|
|
155
|
+
# first, computing the weights (there may be literal repetition)
|
|
156
|
+
self.wght = defaultdict(lambda: 0)
|
|
157
|
+
for l, w in zip(lits, weights):
|
|
158
|
+
self.wght[l] += w
|
|
159
|
+
|
|
160
|
+
# second, extracting the list of unique literals
|
|
161
|
+
self.lits = sorted(self.wght.keys())
|
|
162
|
+
|
|
163
|
+
# right-hand side and value counter
|
|
164
|
+
self.rbnd = bound
|
|
165
|
+
self.vcnt = 0
|
|
166
|
+
|
|
167
|
+
# auxiliary data used during propagation
|
|
168
|
+
# None is needed here for propagation without an assignment
|
|
169
|
+
self.lset = set(self.lits + [None])
|
|
170
|
+
self.lval = None
|
|
171
|
+
|
|
172
|
+
# a model that falsifies the constraint (if any)
|
|
173
|
+
self.fmod = None
|
|
174
|
+
|
|
175
|
+
if min(self.wght.values()) == max(self.wght.values()):
|
|
176
|
+
if self.wght[self.lits[0]] != 1:
|
|
177
|
+
self.rbnd //= self.wght[self.lits[0]]
|
|
178
|
+
self.wght = {l: 1 for l in lits}
|
|
179
|
+
|
|
180
|
+
# unweighted / cardinality propagation
|
|
181
|
+
self.propagate = self.propagate_unweighted
|
|
182
|
+
|
|
183
|
+
# constraint propagates all literals at once,
|
|
184
|
+
# i.e. they all share the same reason
|
|
185
|
+
self.expl = []
|
|
186
|
+
self.justify = self.justify_unweighted
|
|
187
|
+
self.abandon = self.abandon_unweighted
|
|
188
|
+
else:
|
|
189
|
+
# weighted / pseudo-Boolean propagation
|
|
190
|
+
self.propagate = self.propagate_weighted
|
|
191
|
+
|
|
192
|
+
# constraint may propagate different literals
|
|
193
|
+
# at different points in time, reasons may differ
|
|
194
|
+
self.expl = defaultdict(lambda: [])
|
|
195
|
+
self.justify = self.justify_weighted
|
|
196
|
+
self.abandon = self.abandon_weighted
|
|
197
|
+
|
|
198
|
+
# for weighted case only: a flag for indicating whether we can propagate
|
|
199
|
+
self.done = False
|
|
200
|
+
|
|
201
|
+
# adding literal 'None' with 0-weight
|
|
202
|
+
self.wght[None] = 0
|
|
203
|
+
|
|
204
|
+
def register_watched(self, to_watch):
|
|
205
|
+
"""
|
|
206
|
+
Add self to the centralised watched literals lists in
|
|
207
|
+
:class:`BooleanEngine`.
|
|
208
|
+
"""
|
|
209
|
+
|
|
210
|
+
for lit in self.lits:
|
|
211
|
+
to_watch[lit].append(self)
|
|
212
|
+
|
|
213
|
+
def attach_values(self, values):
|
|
214
|
+
"""
|
|
215
|
+
Give the constraint access to centralised values exposed from
|
|
216
|
+
:class:`BooleanEngine`.
|
|
217
|
+
"""
|
|
218
|
+
|
|
219
|
+
self.lval = values
|
|
220
|
+
|
|
221
|
+
def propagate_unweighted(self, lit=None):
|
|
222
|
+
"""
|
|
223
|
+
Get all the consequences of a given literal in the unweighted
|
|
224
|
+
case. The implementation *counts* how many literals on the
|
|
225
|
+
left-hand side are assigned to true.
|
|
226
|
+
"""
|
|
227
|
+
|
|
228
|
+
if not self.expl:
|
|
229
|
+
self.vcnt += self.wght[lit]
|
|
230
|
+
|
|
231
|
+
if self.vcnt == self.rbnd: # this is when we should propagate
|
|
232
|
+
iter1, iter2 = itertools.tee(self.lits)
|
|
233
|
+
self.expl = list(-l for l in iter1 if self.lval[abs(l)] == l)
|
|
234
|
+
to_entail = list(-l for l in iter2 if self.lval[abs(l)] is None)
|
|
235
|
+
|
|
236
|
+
return to_entail
|
|
237
|
+
|
|
238
|
+
return []
|
|
239
|
+
|
|
240
|
+
def justify_unweighted(self, dummy_lit):
|
|
241
|
+
"""
|
|
242
|
+
Provide a reason for a literal propagated by this constraint. In
|
|
243
|
+
the unweighted case, all the literals propagated by this
|
|
244
|
+
constraint share the same reason.
|
|
245
|
+
"""
|
|
246
|
+
|
|
247
|
+
return self.expl
|
|
248
|
+
|
|
249
|
+
def abandon_unweighted(self, dummy_lit):
|
|
250
|
+
"""
|
|
251
|
+
Clear the reason of a given literal.
|
|
252
|
+
"""
|
|
253
|
+
|
|
254
|
+
self.expl.clear()
|
|
255
|
+
|
|
256
|
+
def propagate_weighted(self, lit=None):
|
|
257
|
+
"""
|
|
258
|
+
Get all the consequences of a given literal in the weighted case.
|
|
259
|
+
The implementation counts the weights of all the literals assigned
|
|
260
|
+
to true and propagates all the other literals (yet unassigned)
|
|
261
|
+
such that adding their weights to the total sum would exceed the
|
|
262
|
+
right-hand side of the constraint.
|
|
263
|
+
"""
|
|
264
|
+
|
|
265
|
+
if not self.done:
|
|
266
|
+
self.vcnt += self.wght[lit]
|
|
267
|
+
|
|
268
|
+
iter1, iter2, iter3 = itertools.tee(self.lits, 3)
|
|
269
|
+
expl = list(-l for l in iter1 if self.lval[abs(l)] == l)
|
|
270
|
+
to_entail = list(-l for l in iter2 if self.lval[abs(l)] is None and self.vcnt + self.wght[l] > self.rbnd and not self.expl[-l])
|
|
271
|
+
|
|
272
|
+
# counting the number of unassigned literals to check
|
|
273
|
+
# whether there is something left to do in the future
|
|
274
|
+
nunknown = functools.reduce(lambda x, y: x + 1 if self.lval[abs(y)] is not None else x, iter3, 0)
|
|
275
|
+
|
|
276
|
+
# are we done with constraint for now?
|
|
277
|
+
if len(to_entail) == nunknown:
|
|
278
|
+
self.done = True
|
|
279
|
+
|
|
280
|
+
# setting the reason for each of the propagated literals
|
|
281
|
+
for l in to_entail:
|
|
282
|
+
self.expl[l] = expl
|
|
283
|
+
|
|
284
|
+
return to_entail
|
|
285
|
+
|
|
286
|
+
return []
|
|
287
|
+
|
|
288
|
+
def justify_weighted(self, lit):
|
|
289
|
+
"""
|
|
290
|
+
Provide a reason for a literal propagated by this constraint. In
|
|
291
|
+
the case of weighted constraints, a literal may have a reason
|
|
292
|
+
different from the other literals propagated by the same
|
|
293
|
+
constraint.
|
|
294
|
+
"""
|
|
295
|
+
|
|
296
|
+
return self.expl[lit]
|
|
297
|
+
|
|
298
|
+
def abandon_weighted(self, lit):
|
|
299
|
+
"""
|
|
300
|
+
Clear the reason of a given literal.
|
|
301
|
+
"""
|
|
302
|
+
|
|
303
|
+
self.expl[lit].clear()
|
|
304
|
+
|
|
305
|
+
def unassign(self, lit):
|
|
306
|
+
"""
|
|
307
|
+
Unassign a given literal, which is done by decrementing the
|
|
308
|
+
literal's contribution to the total sum of the weights of assigned
|
|
309
|
+
literals.
|
|
310
|
+
"""
|
|
311
|
+
|
|
312
|
+
self.vcnt -= self.wght[lit]
|
|
313
|
+
self.done = False
|
|
314
|
+
|
|
315
|
+
def falsified_by(self, model):
|
|
316
|
+
"""
|
|
317
|
+
Check if the constraint is violated by a given assignment. Upon
|
|
318
|
+
receiving such an input assignment, the method counts the sum of
|
|
319
|
+
the weights of all satisfied literals and checks if it exceeds the
|
|
320
|
+
right-hand side.
|
|
321
|
+
"""
|
|
322
|
+
|
|
323
|
+
value = functools.reduce(lambda x, y: x + self.wght[y] if y in self.lset else x, model, 0)
|
|
324
|
+
|
|
325
|
+
if value > self.rbnd:
|
|
326
|
+
self.fmod = model
|
|
327
|
+
return True
|
|
328
|
+
|
|
329
|
+
return False
|
|
330
|
+
|
|
331
|
+
def explain_failure(self):
|
|
332
|
+
"""
|
|
333
|
+
Provide a reason clause for why the previous model falsified
|
|
334
|
+
the constraint. This will clause will be added to the solver.
|
|
335
|
+
"""
|
|
336
|
+
|
|
337
|
+
return [-l for l in self.fmod if l in self.lset]
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
#
|
|
341
|
+
#==============================================================================
|
|
342
|
+
class ParityConstraint:
|
|
343
|
+
"""
|
|
344
|
+
A possible implementation of parity constraints. These are constraints
|
|
345
|
+
of the form :math:`l_1 \\oplus l_2 \\oplus \\ldots \\oplus l_n = b`
|
|
346
|
+
where each :math:`l_i` is a Boolean literal while :math:`b` is a
|
|
347
|
+
Boolean constant. The class is designed to exemplify the work with
|
|
348
|
+
:class:`BooleanEngine`.
|
|
349
|
+
|
|
350
|
+
The implementation is pretty naive. It propagates a last unassigned
|
|
351
|
+
literal if all but one literals got their values. The value the
|
|
352
|
+
propagated literals is assigned to depends on the other values in the
|
|
353
|
+
constraint as well as on the right-hand side value.
|
|
354
|
+
|
|
355
|
+
The constructor receives two arguments: ``lits`` and ``value``.
|
|
356
|
+
Argument ``lits`` represents a list of literals on the left-hand side
|
|
357
|
+
of the constraint while argument ``value`` represents the right-hand
|
|
358
|
+
side. By default, ``value`` equals ``1``.
|
|
359
|
+
|
|
360
|
+
:param lits: list of literals (left-hand side)
|
|
361
|
+
:param value: right-hand side of the constraint
|
|
362
|
+
|
|
363
|
+
:type lits:
|
|
364
|
+
:type value: bool
|
|
365
|
+
"""
|
|
366
|
+
|
|
367
|
+
def __init__(self, lits=[], value=1):
|
|
368
|
+
"""
|
|
369
|
+
Constraint initialiser.
|
|
370
|
+
"""
|
|
371
|
+
|
|
372
|
+
# checking whether the right-hand side is correct
|
|
373
|
+
assert value in (0, 1), 'incorrect right-hand side value'
|
|
374
|
+
|
|
375
|
+
# left-hand side of the parity constraint
|
|
376
|
+
# trivially removing all the negative literals
|
|
377
|
+
self.lits = []
|
|
378
|
+
for lit in lits:
|
|
379
|
+
if lit < 0:
|
|
380
|
+
lit = -lit
|
|
381
|
+
value ^= value
|
|
382
|
+
self.lits.append(lit)
|
|
383
|
+
|
|
384
|
+
# right-hand side, current value, and variable counter
|
|
385
|
+
self.rval = value
|
|
386
|
+
self.curr = 0
|
|
387
|
+
self.vcnt = 0
|
|
388
|
+
|
|
389
|
+
# auxiliary data used during propagation
|
|
390
|
+
# None is needed here for propagation without an assignment
|
|
391
|
+
self.lset = set(self.lits + [None])
|
|
392
|
+
self.lval = None
|
|
393
|
+
|
|
394
|
+
# reason for the propagated literals
|
|
395
|
+
self.expl = []
|
|
396
|
+
|
|
397
|
+
# a model that falsifies the constraint (if any)
|
|
398
|
+
self.fmod = None
|
|
399
|
+
|
|
400
|
+
# a copy of watched literals, watched length, and
|
|
401
|
+
# a mapping to the index of an opposite literal
|
|
402
|
+
self.wlst = None
|
|
403
|
+
self.wopp = defaultdict(lambda: None)
|
|
404
|
+
|
|
405
|
+
# if it is unit clause then propagate its sole literal at level 0
|
|
406
|
+
self.zlev = self.lits[0] * (2 * self.rval - 1) if len(self.lits) == 1 else None
|
|
407
|
+
|
|
408
|
+
def register_watched(self, to_watch):
|
|
409
|
+
"""
|
|
410
|
+
Add self to the centralised watched literals lists in
|
|
411
|
+
:class:`BooleanEngine`.
|
|
412
|
+
"""
|
|
413
|
+
|
|
414
|
+
if len(self.lits) > 1:
|
|
415
|
+
# copying the watched lists
|
|
416
|
+
if self.wlst is None:
|
|
417
|
+
self.wlst = to_watch
|
|
418
|
+
|
|
419
|
+
# we need to trigger this constraint
|
|
420
|
+
# no matter what value a variable gets
|
|
421
|
+
for lit in self.lits[:2]:
|
|
422
|
+
self.wopp[+lit] = len(self.wlst[-lit])
|
|
423
|
+
self.wopp[-lit] = len(self.wlst[+lit])
|
|
424
|
+
|
|
425
|
+
self.wlst[+lit].append(self)
|
|
426
|
+
self.wlst[-lit].append(self)
|
|
427
|
+
|
|
428
|
+
def replace_watched(self, old, new):
|
|
429
|
+
"""
|
|
430
|
+
Register this constraint as to be watched for a new literal.
|
|
431
|
+
"""
|
|
432
|
+
|
|
433
|
+
# clean up the old literal
|
|
434
|
+
p, n = self.wopp[-old], self.wopp[+old]
|
|
435
|
+
self.wlst[+old][p] = n
|
|
436
|
+
self.wopp[+old] = None # technically, we don't have to remove these
|
|
437
|
+
self.wopp[-old] = None
|
|
438
|
+
|
|
439
|
+
# register the new literal
|
|
440
|
+
self.wopp[+new] = len(self.wlst[-new])
|
|
441
|
+
self.wopp[-new] = len(self.wlst[+new])
|
|
442
|
+
self.wlst[+new].append(self)
|
|
443
|
+
self.wlst[-new].append(self)
|
|
444
|
+
|
|
445
|
+
def attach_values(self, values):
|
|
446
|
+
"""
|
|
447
|
+
Give the constraint access to centralised values exposed from
|
|
448
|
+
:class:`BooleanEngine`.
|
|
449
|
+
"""
|
|
450
|
+
|
|
451
|
+
self.lval = values
|
|
452
|
+
|
|
453
|
+
def propagate(self, lit=None):
|
|
454
|
+
"""
|
|
455
|
+
Get all the consequences of a given literal. Propagation here
|
|
456
|
+
should be similar to that of normal clauses (2-watched scheme).
|
|
457
|
+
"""
|
|
458
|
+
|
|
459
|
+
if not self.expl:
|
|
460
|
+
if lit:
|
|
461
|
+
if self.lits[0] in (lit, -lit): # this is the first literal
|
|
462
|
+
# so make it the second one
|
|
463
|
+
self.lits[0], self.lits[1] = self.lits[1], self.lits[0]
|
|
464
|
+
|
|
465
|
+
# find the new watch
|
|
466
|
+
value = self.lval[abs(self.lits[1])] in self.lset
|
|
467
|
+
for i in range(2, len(self.lits)):
|
|
468
|
+
v = abs(self.lits[i])
|
|
469
|
+
|
|
470
|
+
if self.lval[v] is None: # v is going to be a new watch
|
|
471
|
+
self.replace_watched(lit, self.lits[i])
|
|
472
|
+
self.lits[1], self.lits[i] = self.lits[i], self.lits[1]
|
|
473
|
+
break
|
|
474
|
+
|
|
475
|
+
value ^= self.lval[v] in self.lset
|
|
476
|
+
else: # failed to find a new watch!
|
|
477
|
+
self.expl = [-self.lval[abs(self.lits[i])] for i in range(1, len(self.lits))]
|
|
478
|
+
to_entail = self.lits[0]
|
|
479
|
+
if value == self.rval:
|
|
480
|
+
to_entail *= -1
|
|
481
|
+
return [to_entail]
|
|
482
|
+
elif self.zlev is not None:
|
|
483
|
+
self.expl, to_entail, self.zlev = [], self.zlev, None
|
|
484
|
+
return [to_entail]
|
|
485
|
+
|
|
486
|
+
return []
|
|
487
|
+
|
|
488
|
+
def justify(self, dummy_lit):
|
|
489
|
+
"""
|
|
490
|
+
Provide a reason for the literal propagated from here.
|
|
491
|
+
"""
|
|
492
|
+
|
|
493
|
+
return self.expl
|
|
494
|
+
|
|
495
|
+
def abandon(self, dummy_lit):
|
|
496
|
+
"""
|
|
497
|
+
Clear the reason of a given literal.
|
|
498
|
+
"""
|
|
499
|
+
|
|
500
|
+
self.expl.clear()
|
|
501
|
+
|
|
502
|
+
def unassign(self, lit):
|
|
503
|
+
"""
|
|
504
|
+
Unassign a given literal. A dummy method, which does nothing.
|
|
505
|
+
"""
|
|
506
|
+
|
|
507
|
+
pass
|
|
508
|
+
|
|
509
|
+
def falsified_by(self, model):
|
|
510
|
+
"""
|
|
511
|
+
Check if the constraint is violated by a given assignment, which
|
|
512
|
+
is done by xor'ing up all the values on the left-hand side (given
|
|
513
|
+
a model) and comparing with the right-hand side value.
|
|
514
|
+
"""
|
|
515
|
+
|
|
516
|
+
value = functools.reduce(lambda x, y: x ^ 1 if y in self.lset else x, model, 0)
|
|
517
|
+
|
|
518
|
+
if value != self.rval:
|
|
519
|
+
self.fmod = model
|
|
520
|
+
return True
|
|
521
|
+
|
|
522
|
+
return False
|
|
523
|
+
|
|
524
|
+
def explain_failure(self):
|
|
525
|
+
"""
|
|
526
|
+
Provide a reason clause for why the previous model falsified
|
|
527
|
+
the constraint. This will clause will be added to the solver.
|
|
528
|
+
"""
|
|
529
|
+
|
|
530
|
+
return [-l for l in self.fmod if l in self.lset or -l in self.lset]
|
|
531
|
+
|
|
532
|
+
|
|
533
|
+
#
|
|
534
|
+
#==============================================================================
|
|
535
|
+
class Propagator(object):
|
|
536
|
+
"""
|
|
537
|
+
An abstract class for creating external user-defined propagators /
|
|
538
|
+
reasoning engines to be used with solver :class:`Cadical195` through
|
|
539
|
+
the IPASIR-UP interface. All user-defined propagators should inherit
|
|
540
|
+
the interface of this abstract class, i.e. all the below methods need
|
|
541
|
+
to be properly defined. The interface is as follows:
|
|
542
|
+
|
|
543
|
+
.. code-block:: python
|
|
544
|
+
|
|
545
|
+
class Propagator(object):
|
|
546
|
+
def on_assignment(self, lit: int, fixed: bool = False) -> None:
|
|
547
|
+
pass # receive a new literal assigned by the solver
|
|
548
|
+
|
|
549
|
+
def on_new_level(self) -> None:
|
|
550
|
+
pass # get notified about a new decision level
|
|
551
|
+
|
|
552
|
+
def on_backtrack(self, to: int) -> None:
|
|
553
|
+
pass # process backtracking to a given level
|
|
554
|
+
|
|
555
|
+
def check_model(self, model: List[int]) -> bool:
|
|
556
|
+
pass # check if a given assignment is indeed a model
|
|
557
|
+
|
|
558
|
+
def decide(self) -> int:
|
|
559
|
+
return 0 # make a decision and (if any) inform the solver
|
|
560
|
+
|
|
561
|
+
def propagate(self) -> List[int]:
|
|
562
|
+
return [] # propagate and return inferred literals (if any)
|
|
563
|
+
|
|
564
|
+
def provide_reason(self, lit: int) -> List[int]:
|
|
565
|
+
pass # explain why a given literal was propagated
|
|
566
|
+
|
|
567
|
+
def add_clause(self) -> List[int]:
|
|
568
|
+
return [] # add an(y) external clause to the solver
|
|
569
|
+
"""
|
|
570
|
+
|
|
571
|
+
def __init__(self):
|
|
572
|
+
"""
|
|
573
|
+
Initialiser / constructor. Declare/define all the variables
|
|
574
|
+
required for the propagator's functionality here.
|
|
575
|
+
"""
|
|
576
|
+
|
|
577
|
+
# internal flag to declare the propagator "lazy"
|
|
578
|
+
# (a lazy propagator only checks complete assignments;
|
|
579
|
+
# currently, checked only when it gets connected)
|
|
580
|
+
self.is_lazy = False
|
|
581
|
+
|
|
582
|
+
def __delete__(self):
|
|
583
|
+
"""
|
|
584
|
+
Destructor.
|
|
585
|
+
"""
|
|
586
|
+
|
|
587
|
+
pass
|
|
588
|
+
|
|
589
|
+
def on_assignment(self, lit: int, fixed: bool = False) -> None:
|
|
590
|
+
"""
|
|
591
|
+
The method is called to notify the propagator about an assignment
|
|
592
|
+
made for one of the observed variables. An assignment is set to be
|
|
593
|
+
"fixed" if it is permanent, i.e. the propagator is not allowed to
|
|
594
|
+
undo it.
|
|
595
|
+
|
|
596
|
+
:param lit: assigned literal
|
|
597
|
+
:param fixed: a flag to mark the assignment as "fixed"
|
|
598
|
+
|
|
599
|
+
:type lit: int
|
|
600
|
+
:type fixed: bool
|
|
601
|
+
"""
|
|
602
|
+
|
|
603
|
+
pass
|
|
604
|
+
|
|
605
|
+
def on_new_level(self) -> None:
|
|
606
|
+
"""
|
|
607
|
+
The method called to notify the propagator about a new decision
|
|
608
|
+
level created by the solver.
|
|
609
|
+
"""
|
|
610
|
+
|
|
611
|
+
pass
|
|
612
|
+
|
|
613
|
+
def on_backtrack(self, to: int) -> None:
|
|
614
|
+
"""
|
|
615
|
+
The method for notifying the propagator about backtracking to a
|
|
616
|
+
given decision level. Accepts a single argument ``to`` signifying
|
|
617
|
+
the backtrack level.
|
|
618
|
+
|
|
619
|
+
:param to: backtrack level
|
|
620
|
+
:type to: int
|
|
621
|
+
"""
|
|
622
|
+
|
|
623
|
+
pass
|
|
624
|
+
|
|
625
|
+
def check_model(self, model: List[int]) -> bool:
|
|
626
|
+
"""
|
|
627
|
+
The method is used for checking if a given (complete) truth
|
|
628
|
+
assignment satisfies the constraint managed by the propagator.
|
|
629
|
+
Receives a single argument storing the truth assignment found by
|
|
630
|
+
the solver.
|
|
631
|
+
|
|
632
|
+
.. note::
|
|
633
|
+
|
|
634
|
+
If this method returns ``False``, the propagator must be ready
|
|
635
|
+
to provide an external clause in the following callback.
|
|
636
|
+
|
|
637
|
+
:param model: a list of integers representing the current model
|
|
638
|
+
:type model: iterable(int)
|
|
639
|
+
|
|
640
|
+
:rtype: bool
|
|
641
|
+
"""
|
|
642
|
+
|
|
643
|
+
pass
|
|
644
|
+
|
|
645
|
+
def decide(self) -> int:
|
|
646
|
+
"""
|
|
647
|
+
This method allows the propagator to influence the decision
|
|
648
|
+
process. Namely, it is used when the solver asks the propagator
|
|
649
|
+
for the next decision literal (if any). If the method returns
|
|
650
|
+
``0``, the solver will make its own choice.
|
|
651
|
+
|
|
652
|
+
:rtype: int
|
|
653
|
+
"""
|
|
654
|
+
|
|
655
|
+
return 0
|
|
656
|
+
|
|
657
|
+
def propagate(self) -> List[int]:
|
|
658
|
+
"""
|
|
659
|
+
The method should invoke propagation under the current assignment.
|
|
660
|
+
It can return either a list of literals propagated or an empty
|
|
661
|
+
list ``[]``, informing the solver that no propagation is made
|
|
662
|
+
under the current assignment.
|
|
663
|
+
|
|
664
|
+
:rtype: int
|
|
665
|
+
"""
|
|
666
|
+
|
|
667
|
+
return []
|
|
668
|
+
|
|
669
|
+
def provide_reason(self, lit: int) -> List[int]:
|
|
670
|
+
"""
|
|
671
|
+
The method is called by the solver when asking the propagator for
|
|
672
|
+
the reason / antecedent clause for a literal the propagator
|
|
673
|
+
previously inferred. This clause will be used in the following
|
|
674
|
+
conflict analysis.
|
|
675
|
+
|
|
676
|
+
.. note::
|
|
677
|
+
|
|
678
|
+
The clause must contain the propagated literal.
|
|
679
|
+
|
|
680
|
+
:param lit: literal to provide reason for
|
|
681
|
+
:type lit: int
|
|
682
|
+
|
|
683
|
+
:rtype: iterable(int)
|
|
684
|
+
"""
|
|
685
|
+
|
|
686
|
+
pass
|
|
687
|
+
|
|
688
|
+
def add_clause(self) -> List[int]:
|
|
689
|
+
"""
|
|
690
|
+
The method is called by the solver to add an external clause if
|
|
691
|
+
there is any. The clause can be arbitrary but if it is
|
|
692
|
+
root-satisfied or tautological, the solver will ignore it without
|
|
693
|
+
learning it.
|
|
694
|
+
|
|
695
|
+
Root-falsified literals are eagerly removed from the clause.
|
|
696
|
+
Falsified clauses trigger conflict analysis, propagating clauses
|
|
697
|
+
trigger propagation. Unit clauses always (unless root-satisfied,
|
|
698
|
+
see above) trigger backtracking to level 0.
|
|
699
|
+
|
|
700
|
+
An empty clause (or root falsified clause, see above) makes the
|
|
701
|
+
formula unsatisfiable and stops the search immediately.
|
|
702
|
+
|
|
703
|
+
:rtype: iterable(int)
|
|
704
|
+
"""
|
|
705
|
+
|
|
706
|
+
pass
|
|
707
|
+
|
|
708
|
+
|
|
709
|
+
#
|
|
710
|
+
#==============================================================================
|
|
711
|
+
class BooleanEngine(Propagator):
|
|
712
|
+
"""
|
|
713
|
+
A simple *example* Boolean constraint propagator inheriting from the
|
|
714
|
+
class :class:`Propagator`. The idea is to exemplify the use of
|
|
715
|
+
external reasoning engines. The engine should be general enough to
|
|
716
|
+
support various constraints over Boolean variables.
|
|
717
|
+
|
|
718
|
+
.. note::
|
|
719
|
+
|
|
720
|
+
Note that this is not meant to be a model implementation of an
|
|
721
|
+
external engine. One can devise a more efficient implementation
|
|
722
|
+
with the same functionality.
|
|
723
|
+
|
|
724
|
+
The initialiser of of the class object may be provided with a list of
|
|
725
|
+
constraints, each being a tuple ('type', constraint), as a value for
|
|
726
|
+
parameter ``bootstrap_with``.
|
|
727
|
+
|
|
728
|
+
Currently, there are two types of constraints supported (to be
|
|
729
|
+
specified) in the constraints passed in: ``'linear'`` and ``'parity'``
|
|
730
|
+
(exclusive OR). The former will be handled as objects of class
|
|
731
|
+
:class:`LinearConstraint` while the latter will be transformed into
|
|
732
|
+
objects of :class:`ParityConstraint`.
|
|
733
|
+
|
|
734
|
+
Here, each type of constraint is meant to have a list of literals
|
|
735
|
+
stored in variable ``.lits``. This is required to set up watched lists
|
|
736
|
+
properly.
|
|
737
|
+
|
|
738
|
+
The second keyword argument ``adaptive`` (set to ``True`` by default)
|
|
739
|
+
denotes the fact that the engine should check its own efficiency and
|
|
740
|
+
disable or enable itself on the fly. This functionality is meant to
|
|
741
|
+
exemplify how adaptive external engines can be created. A user is
|
|
742
|
+
referred to the source code of the implementation for the details.
|
|
743
|
+
"""
|
|
744
|
+
|
|
745
|
+
def __init__(self, bootstrap_with=[], adaptive=True):
|
|
746
|
+
"""
|
|
747
|
+
Initialiser.
|
|
748
|
+
"""
|
|
749
|
+
|
|
750
|
+
self.vars = [] # all known variables
|
|
751
|
+
self.vset = set() # a set of all variables
|
|
752
|
+
self.cons = [] # all known constraints
|
|
753
|
+
self.lins = [] # linear constraints
|
|
754
|
+
self.xors = [] # parity constraints
|
|
755
|
+
self.wlst = defaultdict(lambda: []) # constraints to watch
|
|
756
|
+
|
|
757
|
+
# this propagator may disable itself depending on the circumstances
|
|
758
|
+
self.is_adaptive = adaptive
|
|
759
|
+
|
|
760
|
+
# setting up individual constraint handlers
|
|
761
|
+
if bootstrap_with:
|
|
762
|
+
for cs in bootstrap_with:
|
|
763
|
+
cs = self._add_constraint(cs)
|
|
764
|
+
|
|
765
|
+
# updating the set of known variables
|
|
766
|
+
self.vset.update(set(abs(l) for l in cs.lits))
|
|
767
|
+
|
|
768
|
+
# setting all known variables
|
|
769
|
+
self.vars = sorted(self.vset)
|
|
770
|
+
|
|
771
|
+
# run the preprocessing techniques (if any)
|
|
772
|
+
self.preprocess()
|
|
773
|
+
|
|
774
|
+
# attaching new constraints after processing to the watched lists
|
|
775
|
+
for cs in self.cons:
|
|
776
|
+
cs.register_watched(self.wlst)
|
|
777
|
+
|
|
778
|
+
# value and trail handling
|
|
779
|
+
self.value = {v: None for v in self.vars}
|
|
780
|
+
self.fixed = {v: False for v in self.vars}
|
|
781
|
+
self.trail = []
|
|
782
|
+
self.trlim = []
|
|
783
|
+
self.props = defaultdict(lambda: [])
|
|
784
|
+
self.qhead = None
|
|
785
|
+
|
|
786
|
+
# decision level
|
|
787
|
+
self.decision_level = 0
|
|
788
|
+
self.level = {v: None for v in self.vars}
|
|
789
|
+
|
|
790
|
+
# reasons for propagated literals
|
|
791
|
+
self.origin = defaultdict(lambda: None)
|
|
792
|
+
|
|
793
|
+
# this variable points to the constraint previously violated by a
|
|
794
|
+
# given model offered by the solver
|
|
795
|
+
self.falsified = None
|
|
796
|
+
|
|
797
|
+
# giving constraints access to the values
|
|
798
|
+
for cs in self.cons:
|
|
799
|
+
cs.attach_values(self.value)
|
|
800
|
+
|
|
801
|
+
# currently, no solver is known to this propagator
|
|
802
|
+
# this will be update upon calling self.setup_observe()
|
|
803
|
+
self.solver = None
|
|
804
|
+
|
|
805
|
+
# the following constants are used in adaptive mode:
|
|
806
|
+
# the number of calls to propagate() between model checks and
|
|
807
|
+
# the number of propagated literals found between those checks
|
|
808
|
+
# followed by decaying propagation ratio-related constants
|
|
809
|
+
self.nprops, self.ncalls = 0, 0
|
|
810
|
+
self.pratio, self.pdecay, self.pbound = 0, 2, 0.2
|
|
811
|
+
|
|
812
|
+
# decaying ratio of falsifying assignments
|
|
813
|
+
# over all assignments offered by the solver
|
|
814
|
+
self.mratio, self.mdecay, self.mbound = 0, 2, 2
|
|
815
|
+
|
|
816
|
+
def adaptive_constants(self, pdecay, pbound, mdecay, mbound):
|
|
817
|
+
"""
|
|
818
|
+
Set magic numeric constants used in adaptive mode.
|
|
819
|
+
"""
|
|
820
|
+
|
|
821
|
+
self.pdecay = pdecay
|
|
822
|
+
self.pbound = pbound
|
|
823
|
+
|
|
824
|
+
self.mdecay = mdecay
|
|
825
|
+
self.mbound = mbound
|
|
826
|
+
|
|
827
|
+
def enable(self):
|
|
828
|
+
"""
|
|
829
|
+
Notify the solver that the propagator is willing to become active
|
|
830
|
+
from now on.
|
|
831
|
+
"""
|
|
832
|
+
|
|
833
|
+
assert self.solver, 'Solver is not connected!'
|
|
834
|
+
self.solver.enable_propagator()
|
|
835
|
+
|
|
836
|
+
def disable(self):
|
|
837
|
+
"""
|
|
838
|
+
Notify the solver that the propagator should become inactive as it
|
|
839
|
+
does not contribute much to the inference process. From now on, it
|
|
840
|
+
will only be called to check complete models obtained by the
|
|
841
|
+
solver (see :meth:`check_model`).
|
|
842
|
+
"""
|
|
843
|
+
|
|
844
|
+
assert self.solver, 'Solver is not connected!'
|
|
845
|
+
self.solver.disable_propagator()
|
|
846
|
+
|
|
847
|
+
def is_active(self):
|
|
848
|
+
"""
|
|
849
|
+
Return engine's status. It is deemed active if the method returns
|
|
850
|
+
``True`` and passive otherwise.
|
|
851
|
+
"""
|
|
852
|
+
|
|
853
|
+
assert self.solver, 'Solver is not connected!'
|
|
854
|
+
return self.solver.propagator_active()
|
|
855
|
+
|
|
856
|
+
def add_constraint(self, constraint):
|
|
857
|
+
"""
|
|
858
|
+
Add a new constraint to the engine and integrate it to the
|
|
859
|
+
internal structures, i.e. watched lists. Also, return the newly
|
|
860
|
+
added constraint to the callee.
|
|
861
|
+
"""
|
|
862
|
+
|
|
863
|
+
cs = self._add_constraint(constraint)
|
|
864
|
+
|
|
865
|
+
# adding it to the watched lists
|
|
866
|
+
cs.register_watched(self.wlst)
|
|
867
|
+
|
|
868
|
+
# updating the other structures of the propagator
|
|
869
|
+
for lit in cs.lits:
|
|
870
|
+
var = abs(lit)
|
|
871
|
+
if var not in self.vset:
|
|
872
|
+
# this variable is currently unknown
|
|
873
|
+
self.vset.add(var)
|
|
874
|
+
self.value[var] = None
|
|
875
|
+
self.level[var] = None
|
|
876
|
+
self.fixed[var] = False
|
|
877
|
+
|
|
878
|
+
# letting the solver know we observe this variable
|
|
879
|
+
self.solver.observe(var)
|
|
880
|
+
|
|
881
|
+
# passing values to the constraint
|
|
882
|
+
cs.attach_values(self.value)
|
|
883
|
+
|
|
884
|
+
def _add_constraint(self, cs):
|
|
885
|
+
"""
|
|
886
|
+
Create a new constraint, add to the list and return for further
|
|
887
|
+
processing.
|
|
888
|
+
"""
|
|
889
|
+
|
|
890
|
+
if cs[0] == 'linear':
|
|
891
|
+
cs = LinearConstraint(lits=cs[1][0],
|
|
892
|
+
weights={} if len(cs[1]) == 2 else cs[1][2],
|
|
893
|
+
bound=cs[1][1])
|
|
894
|
+
self.lins.append(cs)
|
|
895
|
+
elif cs[0] == 'parity':
|
|
896
|
+
cs = ParityConstraint(lits=cs[1][0], value=cs[1][1])
|
|
897
|
+
self.xors.append(cs)
|
|
898
|
+
else:
|
|
899
|
+
assert 0, 'Unknown type of constraint'
|
|
900
|
+
|
|
901
|
+
# adding the newly created constraint
|
|
902
|
+
# to the list of known constraints
|
|
903
|
+
self.cons.append(cs)
|
|
904
|
+
|
|
905
|
+
return cs
|
|
906
|
+
|
|
907
|
+
def setup_observe(self, solver):
|
|
908
|
+
"""
|
|
909
|
+
Inform the solver about all the variables the engine is interested
|
|
910
|
+
in. The solver will mark them as observed by the propagator.
|
|
911
|
+
"""
|
|
912
|
+
|
|
913
|
+
# saving the solver for the future
|
|
914
|
+
self.solver = solver
|
|
915
|
+
|
|
916
|
+
for var in self.vars:
|
|
917
|
+
self.solver.observe(var)
|
|
918
|
+
|
|
919
|
+
def preprocess(self):
|
|
920
|
+
"""
|
|
921
|
+
Run some (naive) preprocessing techniques if available for the
|
|
922
|
+
types of constraints under considerations. Each type of
|
|
923
|
+
constraints is handled separately of the rest of constraints.
|
|
924
|
+
"""
|
|
925
|
+
|
|
926
|
+
if self.lins:
|
|
927
|
+
self.process_linear()
|
|
928
|
+
|
|
929
|
+
if self.xors:
|
|
930
|
+
self.process_parity()
|
|
931
|
+
|
|
932
|
+
def process_linear(self):
|
|
933
|
+
"""
|
|
934
|
+
Process linear constraints. Here we apply simple pairwise
|
|
935
|
+
summation of constraints. As the number of result constraints is
|
|
936
|
+
quadratic, we stop the process as soon as we get 100 new
|
|
937
|
+
constraints. Also, if a result of the sum is longer than each of
|
|
938
|
+
the summands, the result constraint is ignored.
|
|
939
|
+
|
|
940
|
+
This is trivial procedure is made to illustrate how constraint
|
|
941
|
+
processing can be done. It can be made dependent on user-specified
|
|
942
|
+
parameters, e.g. the number of rounds or a numeric value
|
|
943
|
+
indicating when a pair of constraints should be added and when
|
|
944
|
+
they should not be added. For consideration in the future.
|
|
945
|
+
"""
|
|
946
|
+
|
|
947
|
+
newc = []
|
|
948
|
+
|
|
949
|
+
for cs1, cs2 in itertools.combinations(self.lins, 2):
|
|
950
|
+
seen, wght, remd = set(), defaultdict(lambda: 0), 0
|
|
951
|
+
|
|
952
|
+
# traversing the first constraint and handling clashes
|
|
953
|
+
for l in cs1.lits:
|
|
954
|
+
if l in cs2.lset:
|
|
955
|
+
# the literal appears in both constraints
|
|
956
|
+
wght[l] = cs1.wght[l] + cs2.wght[l]
|
|
957
|
+
elif -l in cs2.lset:
|
|
958
|
+
# the literal appears in opposite phases
|
|
959
|
+
maxp = max([(l, cs1.wght[l]), (-l, cs2.wght[-l])], key=lambda pair: pair[1])
|
|
960
|
+
minp = min([(l, cs1.wght[l]), (-l, cs2.wght[-l])], key=lambda pair: pair[1])
|
|
961
|
+
|
|
962
|
+
if maxp[1] != minp[1]: # the weights aren't equal
|
|
963
|
+
wght[maxp[0]] = maxp[1] - minp[1]
|
|
964
|
+
|
|
965
|
+
# saving the remainder
|
|
966
|
+
remd += minp[1]
|
|
967
|
+
else:
|
|
968
|
+
# no clash
|
|
969
|
+
wght[l] = cs1.wght[l]
|
|
970
|
+
|
|
971
|
+
seen.add(abs(l))
|
|
972
|
+
|
|
973
|
+
# traversing the remainder of 2nd constraints; no clashes
|
|
974
|
+
for l in cs2.lits:
|
|
975
|
+
if abs(l) in seen:
|
|
976
|
+
continue
|
|
977
|
+
wght[l] = cs2.wght[l]
|
|
978
|
+
|
|
979
|
+
# right-hand side of the result constraint
|
|
980
|
+
rbnd = cs1.rbnd + cs2.rbnd - remd
|
|
981
|
+
|
|
982
|
+
if wght and rbnd >= 0:
|
|
983
|
+
# left-hand size of the result constraint
|
|
984
|
+
lits = sorted(wght.keys())
|
|
985
|
+
|
|
986
|
+
if len(lits) > len(cs1.lits) and len(lits) > len(cs2.lits):
|
|
987
|
+
# the result constraint is not valuable -> ignore it
|
|
988
|
+
continue
|
|
989
|
+
|
|
990
|
+
# all good, recording the constraint
|
|
991
|
+
newc.append(['linear', [lits, rbnd, wght]])
|
|
992
|
+
|
|
993
|
+
if len(newc) == 100:
|
|
994
|
+
break
|
|
995
|
+
|
|
996
|
+
elif rbnd < 0: # negative bound signifies unsatisfiability
|
|
997
|
+
# stopping immediately
|
|
998
|
+
newc = [('linear', [[cs1.lits[0]], 0]), ('linear', [[-cs1.lits[0]], 0])]
|
|
999
|
+
break
|
|
1000
|
+
|
|
1001
|
+
if newc: # there are some new constraints to add
|
|
1002
|
+
for cs in newc:
|
|
1003
|
+
# adding constraint to the engine
|
|
1004
|
+
cs = self._add_constraint(cs)
|
|
1005
|
+
|
|
1006
|
+
# updating the set of known variables
|
|
1007
|
+
self.vset.update(set(abs(l) for l in cs.lits))
|
|
1008
|
+
|
|
1009
|
+
# updating all known variables
|
|
1010
|
+
self.vars = sorted(self.vset)
|
|
1011
|
+
|
|
1012
|
+
def process_parity(self):
|
|
1013
|
+
"""
|
|
1014
|
+
Process parity/XOR constraints. Basically, this runs Gaussian
|
|
1015
|
+
elimination and see if anything can be derived from it.
|
|
1016
|
+
"""
|
|
1017
|
+
|
|
1018
|
+
status = True
|
|
1019
|
+
|
|
1020
|
+
# removing None from all sets
|
|
1021
|
+
for cs in self.xors:
|
|
1022
|
+
cs.lset.remove(None)
|
|
1023
|
+
|
|
1024
|
+
# forward simplification
|
|
1025
|
+
for i, csi in enumerate(self.xors):
|
|
1026
|
+
v = csi.lits[0] # pivot variable
|
|
1027
|
+
|
|
1028
|
+
# going over all the other constraints and eliminating the pivot
|
|
1029
|
+
for j in range(i + 1, len(self.xors)):
|
|
1030
|
+
csj = self.xors[j]
|
|
1031
|
+
|
|
1032
|
+
if v not in csj.lset: # do nothing if pivot var is not present
|
|
1033
|
+
continue
|
|
1034
|
+
|
|
1035
|
+
# summing i'th and j'th constraints up
|
|
1036
|
+
csj.lset ^= csi.lset
|
|
1037
|
+
csj.rval ^= csi.rval
|
|
1038
|
+
|
|
1039
|
+
if not csj.lset and csj.rval:
|
|
1040
|
+
# inconsistency detected
|
|
1041
|
+
status = False
|
|
1042
|
+
|
|
1043
|
+
# adding two inconsistent level-0 literals: 1 and -1
|
|
1044
|
+
csi.lits, csj.lits = [1], [-1]
|
|
1045
|
+
csi.zlev, csj.zlev = 1, -1
|
|
1046
|
+
|
|
1047
|
+
break
|
|
1048
|
+
|
|
1049
|
+
# final result
|
|
1050
|
+
csj.lits = sorted(csj.lset)
|
|
1051
|
+
|
|
1052
|
+
if status:
|
|
1053
|
+
# back substitution if no inconsistency was detected
|
|
1054
|
+
vals = {}
|
|
1055
|
+
for i in range(len(self.xors) - 1, -1, -1):
|
|
1056
|
+
cs = self.xors[i]
|
|
1057
|
+
|
|
1058
|
+
lits = []
|
|
1059
|
+
for l in cs.lits:
|
|
1060
|
+
if l in vals:
|
|
1061
|
+
cs.rval ^= vals[l] # substitution
|
|
1062
|
+
else:
|
|
1063
|
+
lits.append(l)
|
|
1064
|
+
cs.lits = lits
|
|
1065
|
+
|
|
1066
|
+
if len(cs.lits) == 1: # checking if substitution propagates
|
|
1067
|
+
vals[cs.lits[0]] = cs.rval
|
|
1068
|
+
cs.zlev = cs.lits[0] * (2 * cs.rval - 1)
|
|
1069
|
+
|
|
1070
|
+
# updating the sets of literals and putting None back
|
|
1071
|
+
for cs in self.xors:
|
|
1072
|
+
cs.lset = set(cs.lits + [None])
|
|
1073
|
+
|
|
1074
|
+
def on_assignment(self, lit, fixed):
|
|
1075
|
+
"""
|
|
1076
|
+
Update the propagator's state given a new assignment.
|
|
1077
|
+
"""
|
|
1078
|
+
|
|
1079
|
+
self.level[abs(lit)] = self.decision_level
|
|
1080
|
+
|
|
1081
|
+
if self.qhead is None:
|
|
1082
|
+
# this is the first assignment we get after backtracking
|
|
1083
|
+
# marking that next propagation should start from here
|
|
1084
|
+
self.qhead = len(self.trail)
|
|
1085
|
+
|
|
1086
|
+
self.trail.append(lit)
|
|
1087
|
+
|
|
1088
|
+
if fixed:
|
|
1089
|
+
self.fixed[abs(lit)] = True
|
|
1090
|
+
|
|
1091
|
+
def on_new_level(self):
|
|
1092
|
+
"""
|
|
1093
|
+
Keep track of decision level updates.
|
|
1094
|
+
"""
|
|
1095
|
+
|
|
1096
|
+
self.decision_level += 1
|
|
1097
|
+
self.trlim.append(len(self.trail))
|
|
1098
|
+
|
|
1099
|
+
def on_backtrack(self, to):
|
|
1100
|
+
"""
|
|
1101
|
+
Cancel all the decisions up to a certain level.
|
|
1102
|
+
"""
|
|
1103
|
+
|
|
1104
|
+
# undoing the assignments
|
|
1105
|
+
while len(self.trlim) > to:
|
|
1106
|
+
while len(self.trail) > self.trlim[-1]:
|
|
1107
|
+
lit = self.trail.pop()
|
|
1108
|
+
var = abs(lit)
|
|
1109
|
+
|
|
1110
|
+
if self.value[var] is not None and not self.fixed[var]:
|
|
1111
|
+
# informing all the constraints
|
|
1112
|
+
for cs in self.wlst[lit]:
|
|
1113
|
+
cs.unassign(lit)
|
|
1114
|
+
|
|
1115
|
+
self.value[var] = None
|
|
1116
|
+
|
|
1117
|
+
self.level[var] = None
|
|
1118
|
+
|
|
1119
|
+
# cleaning all the consequences
|
|
1120
|
+
for l in self.props[lit]:
|
|
1121
|
+
self.origin[l].abandon(l)
|
|
1122
|
+
self.origin[l] = None
|
|
1123
|
+
self.props[lit] = []
|
|
1124
|
+
|
|
1125
|
+
self.trlim.pop()
|
|
1126
|
+
|
|
1127
|
+
# updating decision level
|
|
1128
|
+
self.decision_level = to
|
|
1129
|
+
|
|
1130
|
+
# updating queue head
|
|
1131
|
+
self.qhead = None
|
|
1132
|
+
|
|
1133
|
+
def check_model(self, model):
|
|
1134
|
+
"""
|
|
1135
|
+
Check if a given model satisfies all the constraints.
|
|
1136
|
+
"""
|
|
1137
|
+
|
|
1138
|
+
st = True # no falsified constraints by default
|
|
1139
|
+
|
|
1140
|
+
# checking all constraints one by one
|
|
1141
|
+
for cs in self.cons:
|
|
1142
|
+
if cs.falsified_by(model):
|
|
1143
|
+
self.falsified = cs
|
|
1144
|
+
st = False
|
|
1145
|
+
break
|
|
1146
|
+
|
|
1147
|
+
if self.is_adaptive:
|
|
1148
|
+
self.adaptive_update(st)
|
|
1149
|
+
|
|
1150
|
+
return st
|
|
1151
|
+
|
|
1152
|
+
def adaptive_update(self, satisfied):
|
|
1153
|
+
"""
|
|
1154
|
+
Update adaptive mode: either enable or disable the engine. This
|
|
1155
|
+
depends on the statistics accumulated in the current run and
|
|
1156
|
+
whether or not the previous assignment found by the solver
|
|
1157
|
+
satisfied the constraints.
|
|
1158
|
+
"""
|
|
1159
|
+
|
|
1160
|
+
# decaying increment
|
|
1161
|
+
self.pratio = self.pratio / self.pdecay + (self.nprops / self.ncalls if self.ncalls else 0)
|
|
1162
|
+
|
|
1163
|
+
# taking into account whether the current model falsified something
|
|
1164
|
+
self.mratio = self.mratio / self.mdecay + (0 if satisfied else 1)
|
|
1165
|
+
|
|
1166
|
+
if satisfied and self.is_active():
|
|
1167
|
+
# we are currently active and it does not pay off
|
|
1168
|
+
# in terms of propagation; should we slack off a bit?
|
|
1169
|
+
if self.pratio < self.pbound:
|
|
1170
|
+
self.disable()
|
|
1171
|
+
elif not satisfied and not self.is_active():
|
|
1172
|
+
# we are currently passive and there are many conflicts
|
|
1173
|
+
# let's put more effort into propagation to avoid them?
|
|
1174
|
+
if self.mratio > self.mbound:
|
|
1175
|
+
self.enable()
|
|
1176
|
+
|
|
1177
|
+
# resetting the moving average stats
|
|
1178
|
+
self.nprops = self.ncalls = 0
|
|
1179
|
+
|
|
1180
|
+
def propagate(self):
|
|
1181
|
+
"""
|
|
1182
|
+
Run the propagator given the current assignment.
|
|
1183
|
+
"""
|
|
1184
|
+
|
|
1185
|
+
result = []
|
|
1186
|
+
|
|
1187
|
+
if self.qhead is not None:
|
|
1188
|
+
while self.qhead < len(self.trail):
|
|
1189
|
+
lit = self.trail[self.qhead]
|
|
1190
|
+
|
|
1191
|
+
# if we have just propagated an opposite value,
|
|
1192
|
+
# we stop and expect conflict analysis to be performed
|
|
1193
|
+
if self.origin[-lit] is not None:
|
|
1194
|
+
break
|
|
1195
|
+
|
|
1196
|
+
self.value[abs(lit)] = lit # actually setting the value
|
|
1197
|
+
|
|
1198
|
+
watched_holes = []
|
|
1199
|
+
for csid, cs in enumerate(self.wlst[lit]):
|
|
1200
|
+
# checking the constraint
|
|
1201
|
+
propagated = cs.propagate(lit)
|
|
1202
|
+
|
|
1203
|
+
# extracting the reason
|
|
1204
|
+
for l in propagated:
|
|
1205
|
+
if self.origin[l] is None:
|
|
1206
|
+
# this literal is currently unknown
|
|
1207
|
+
self.origin[l] = cs
|
|
1208
|
+
|
|
1209
|
+
# augment the list of entailed literals to return
|
|
1210
|
+
result.append(l)
|
|
1211
|
+
|
|
1212
|
+
# recording all the dependencies
|
|
1213
|
+
self.props[lit].append(l)
|
|
1214
|
+
|
|
1215
|
+
# checking whether or not this cs is still watched
|
|
1216
|
+
if self.wlst[lit][csid] is None or type(self.wlst[lit][csid]) == int:
|
|
1217
|
+
# instead of cs, we have None or an integer, i.e. we
|
|
1218
|
+
# removed cs from the watched list for this literal
|
|
1219
|
+
watched_holes.append((csid, self.wlst[lit][csid]))
|
|
1220
|
+
|
|
1221
|
+
# cleaning up watches, should be more-or-less cheap,
|
|
1222
|
+
# doing so for +lit but also for -lit if it is affected
|
|
1223
|
+
if watched_holes:
|
|
1224
|
+
self.cleanup_watched(lit, watched_holes)
|
|
1225
|
+
|
|
1226
|
+
self.qhead += 1
|
|
1227
|
+
|
|
1228
|
+
if self.is_adaptive:
|
|
1229
|
+
# collecting the stats
|
|
1230
|
+
self.nprops += len(result)
|
|
1231
|
+
self.ncalls += 1
|
|
1232
|
+
else:
|
|
1233
|
+
# propagating without an assignment
|
|
1234
|
+
for cs in self.cons:
|
|
1235
|
+
propagated = cs.propagate()
|
|
1236
|
+
|
|
1237
|
+
# extracting the (empty) reason
|
|
1238
|
+
for l in propagated:
|
|
1239
|
+
if self.origin[l] is None:
|
|
1240
|
+
# this literal is currently unknown
|
|
1241
|
+
self.origin[l] = cs
|
|
1242
|
+
|
|
1243
|
+
# augment the list of entailed literals to return
|
|
1244
|
+
result.append(l)
|
|
1245
|
+
|
|
1246
|
+
return result
|
|
1247
|
+
|
|
1248
|
+
def cleanup_watched(self, lit, garbage):
|
|
1249
|
+
"""
|
|
1250
|
+
Garbage collect holes in the watched list for +lit
|
|
1251
|
+
(and potentially for -lit).
|
|
1252
|
+
"""
|
|
1253
|
+
|
|
1254
|
+
otherside = []
|
|
1255
|
+
|
|
1256
|
+
# going backwards, to make it work
|
|
1257
|
+
for i in range(len(garbage) - 1, -1, -1):
|
|
1258
|
+
pid, nid = garbage[i]
|
|
1259
|
+
|
|
1260
|
+
self.wlst[lit][pid] = self.wlst[lit][-1]
|
|
1261
|
+
self.wlst[lit].pop()
|
|
1262
|
+
|
|
1263
|
+
if nid is not None:
|
|
1264
|
+
# this one has a negative counterpart
|
|
1265
|
+
if pid < len(self.wlst[lit]):
|
|
1266
|
+
# and it's affected
|
|
1267
|
+
self.wlst[lit][pid].wopp[-lit] = pid
|
|
1268
|
+
|
|
1269
|
+
otherside.append(nid)
|
|
1270
|
+
|
|
1271
|
+
# taking care of the negative counterparts (if those are affected)
|
|
1272
|
+
for nid in sorted(otherside, reverse=True):
|
|
1273
|
+
self.wlst[-lit][nid] = self.wlst[-lit][-1]
|
|
1274
|
+
self.wlst[-lit].pop()
|
|
1275
|
+
|
|
1276
|
+
if nid < len(self.wlst[-lit]):
|
|
1277
|
+
self.wlst[-lit][nid].wopp[lit] = nid
|
|
1278
|
+
|
|
1279
|
+
def provide_reason(self, lit):
|
|
1280
|
+
"""
|
|
1281
|
+
Return the reason clause for a given literal.
|
|
1282
|
+
"""
|
|
1283
|
+
|
|
1284
|
+
return [lit] + self.origin[lit].justify(lit)
|
|
1285
|
+
|
|
1286
|
+
def add_clause(self):
|
|
1287
|
+
"""
|
|
1288
|
+
Extract a new clause to add to the solver if one exists; return an
|
|
1289
|
+
empty clause ``[]`` otherwise.
|
|
1290
|
+
"""
|
|
1291
|
+
|
|
1292
|
+
if self.falsified is None:
|
|
1293
|
+
return []
|
|
1294
|
+
|
|
1295
|
+
to_add = self.falsified.explain_failure()
|
|
1296
|
+
|
|
1297
|
+
# cleaning the previous failure (currently being added)
|
|
1298
|
+
# technically, we should clean self.falsified.fmod too
|
|
1299
|
+
# but it should be safe enough not to do so
|
|
1300
|
+
self.falsified = None
|
|
1301
|
+
|
|
1302
|
+
return to_add
|