python-sat 0.1.8.dev10__cp310-cp310-win_amd64.whl → 1.8.dev26__cp310-cp310-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.
- pycard.cp310-win_amd64.pyd +0 -0
- pysat/__init__.py +4 -4
- pysat/_fileio.py +30 -14
- pysat/allies/approxmc.py +22 -22
- pysat/allies/unigen.py +435 -0
- pysat/card.py +13 -12
- pysat/engines.py +1302 -0
- pysat/examples/bbscan.py +663 -0
- pysat/examples/bica.py +691 -0
- pysat/examples/fm.py +12 -8
- pysat/examples/genhard.py +24 -23
- pysat/examples/hitman.py +53 -37
- pysat/examples/lbx.py +56 -15
- pysat/examples/lsu.py +28 -14
- pysat/examples/mcsls.py +53 -15
- pysat/examples/models.py +6 -4
- pysat/examples/musx.py +15 -7
- pysat/examples/optux.py +71 -32
- pysat/examples/primer.py +620 -0
- pysat/examples/rc2.py +268 -69
- pysat/formula.py +3241 -229
- pysat/pb.py +85 -37
- pysat/process.py +16 -2
- pysat/solvers.py +2119 -724
- pysolvers.cp310-win_amd64.pyd +0 -0
- {python_sat-0.1.8.dev10.data → python_sat-1.8.dev26.data}/scripts/approxmc.py +22 -22
- python_sat-1.8.dev26.data/scripts/bbscan.py +663 -0
- python_sat-1.8.dev26.data/scripts/bica.py +691 -0
- {python_sat-0.1.8.dev10.data → python_sat-1.8.dev26.data}/scripts/fm.py +12 -8
- {python_sat-0.1.8.dev10.data → python_sat-1.8.dev26.data}/scripts/genhard.py +24 -23
- {python_sat-0.1.8.dev10.data → python_sat-1.8.dev26.data}/scripts/lbx.py +56 -15
- {python_sat-0.1.8.dev10.data → python_sat-1.8.dev26.data}/scripts/lsu.py +28 -14
- {python_sat-0.1.8.dev10.data → python_sat-1.8.dev26.data}/scripts/mcsls.py +53 -15
- {python_sat-0.1.8.dev10.data → python_sat-1.8.dev26.data}/scripts/models.py +6 -4
- {python_sat-0.1.8.dev10.data → python_sat-1.8.dev26.data}/scripts/musx.py +15 -7
- {python_sat-0.1.8.dev10.data → python_sat-1.8.dev26.data}/scripts/optux.py +71 -32
- python_sat-1.8.dev26.data/scripts/primer.py +620 -0
- {python_sat-0.1.8.dev10.data → python_sat-1.8.dev26.data}/scripts/rc2.py +268 -69
- python_sat-1.8.dev26.data/scripts/unigen.py +435 -0
- {python_sat-0.1.8.dev10.dist-info → python_sat-1.8.dev26.dist-info}/METADATA +19 -5
- python_sat-1.8.dev26.dist-info/RECORD +48 -0
- {python_sat-0.1.8.dev10.dist-info → python_sat-1.8.dev26.dist-info}/WHEEL +1 -1
- python_sat-0.1.8.dev10.dist-info/RECORD +0 -39
- {python_sat-0.1.8.dev10.dist-info → python_sat-1.8.dev26.dist-info/licenses}/LICENSE.txt +0 -0
- {python_sat-0.1.8.dev10.dist-info → python_sat-1.8.dev26.dist-info}/top_level.txt +0 -0
pysat/formula.py
CHANGED
|
@@ -17,6 +17,15 @@
|
|
|
17
17
|
:nosignatures:
|
|
18
18
|
|
|
19
19
|
IDPool
|
|
20
|
+
Formula
|
|
21
|
+
Atom
|
|
22
|
+
And
|
|
23
|
+
Or
|
|
24
|
+
Neg
|
|
25
|
+
Implies
|
|
26
|
+
Equals
|
|
27
|
+
XOr
|
|
28
|
+
ITE
|
|
20
29
|
CNF
|
|
21
30
|
CNFPlus
|
|
22
31
|
WCNF
|
|
@@ -27,11 +36,13 @@
|
|
|
27
36
|
==================
|
|
28
37
|
|
|
29
38
|
This module is designed to facilitate fast and easy PySAT-development by
|
|
30
|
-
providing a simple way to manipulate formulas in PySAT.
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
39
|
+
providing a simple way to manipulate formulas in PySAT. The toolkit
|
|
40
|
+
implements several facilities to manupulate Boolean formulas. Namely, one
|
|
41
|
+
can opt for creating arbitrary non-clausal formulas suitable for simple
|
|
42
|
+
problem encodings requiring no or little knowledge about the process of
|
|
43
|
+
logical encoding. However, the main and most often used kind of formula in
|
|
44
|
+
PySAT is represented by the :class:`CNF` class, which can be used to
|
|
45
|
+
define a formula in `conjunctive normal form (CNF)
|
|
35
46
|
<https://en.wikipedia.org/wiki/Conjunctive_normal_form>`__.
|
|
36
47
|
|
|
37
48
|
Recall that a CNF formula is conventionally seen as a set of clauses, each
|
|
@@ -114,19 +125,19 @@
|
|
|
114
125
|
additional class for dealing with *partial* and *weighted partial* CNF
|
|
115
126
|
formulas, i.e. WCNF formulas. A WCNF formula is a conjunction of two sets
|
|
116
127
|
of clauses: *hard* clauses and *soft* clauses, i.e.
|
|
117
|
-
:math
|
|
128
|
+
:math:`\\mathcal{F}=\\mathcal{H}\\wedge\\mathcal{S}`. Soft clauses of a WCNF
|
|
118
129
|
are labeled with integer *weights*, i.e. a soft clause of
|
|
119
|
-
:math
|
|
130
|
+
:math:`\\mathcal{S}` is a pair :math:`(c_i, w_i)`. In partial (unweighted)
|
|
120
131
|
formulas, all soft clauses have weight 1.
|
|
121
132
|
|
|
122
133
|
WCNF can be of help when solving optimization problems using the SAT
|
|
123
134
|
technology. A typical example of where a WCNF formula can be used is
|
|
124
135
|
`maximum satisfiability (MaxSAT)
|
|
125
136
|
<https://en.wikipedia.org/wiki/Maximum_satisfiability_problem>`__, which
|
|
126
|
-
given a WCNF formula :math
|
|
127
|
-
targets satisfying all its hard clauses :math
|
|
137
|
+
given a WCNF formula :math:`\\mathcal{F}=\\mathcal{H}\\wedge\\mathcal{S}`
|
|
138
|
+
targets satisfying all its hard clauses :math:`\\mathcal{H}` and maximizing
|
|
128
139
|
the sum of weights of satisfied soft clauses, i.e. maximizing the value of
|
|
129
|
-
:math
|
|
140
|
+
:math:`\\sum_{c_i\\in\\mathcal{S}}{w_i\\cdot c_i}`.
|
|
130
141
|
|
|
131
142
|
An object of class :class:`WCNF` has two variables to access the hard and
|
|
132
143
|
soft clauses of the corresponding formula: ``hard`` and ``soft``. The
|
|
@@ -149,7 +160,7 @@
|
|
|
149
160
|
[1, 3]
|
|
150
161
|
|
|
151
162
|
A properly constructed WCNF formula must have a *top weight*, which should
|
|
152
|
-
be equal to :math:`1
|
|
163
|
+
be equal to :math:`1+\\sum_{c_i\\in\\mathcal{S}}{w_i}`. Top weight of a
|
|
153
164
|
formula can be accessed through variable ``topw``.
|
|
154
165
|
|
|
155
166
|
.. code-block:: python
|
|
@@ -166,16 +177,55 @@
|
|
|
166
177
|
*floating point numbers*. Moreover, *negative weights* are also
|
|
167
178
|
supported.
|
|
168
179
|
|
|
169
|
-
Additionally to classes :class:`CNF` and :class:`WCNF`, the module
|
|
170
|
-
the extended classes :class:`CNFPlus` and :class:`WCNFPlus`. The
|
|
171
|
-
difference between ``?CNF`` and ``?CNFPlus`` is the support for
|
|
172
|
-
cardinality constraints provided by the `MiniCard solver
|
|
180
|
+
Additionally to classes :class:`CNF` and :class:`WCNF`, the module
|
|
181
|
+
provides the extended classes :class:`CNFPlus` and :class:`WCNFPlus`. The
|
|
182
|
+
only difference between ``?CNF`` and ``?CNFPlus`` is the support for
|
|
183
|
+
*native* cardinality constraints provided by the `MiniCard solver
|
|
173
184
|
<https://github.com/liffiton/minicard>`__ (see :mod:`pysat.card` for
|
|
174
185
|
details). The corresponding variable in objects of ``CNFPlus``
|
|
175
186
|
(``WCNFPlus``, resp.) responsible for storing the AtMostK constraints is
|
|
176
187
|
``atmosts`` (``atms``, resp.). **Note** that at this point, AtMostK
|
|
177
188
|
constraints in ``WCNF`` can be *hard* only.
|
|
178
189
|
|
|
190
|
+
Apart from the aforementioned variants of (W)CNF formulas, the module now
|
|
191
|
+
offers a few additional classes for managing non-clausal Boolean formulas.
|
|
192
|
+
Namely, a user may create complex formulas using variables (atomic
|
|
193
|
+
formulas implemented as objects of class :class:`Atom`), and the following
|
|
194
|
+
logic connectives: :class:`And`, :class:`Or`, :class:`Neg`,
|
|
195
|
+
:class:`Implies`, :class:`Equals`, :class:`XOr`, and :class:`ITE`. (All of
|
|
196
|
+
these classes inherit from the base class :class:`Formula`.) Arbitrary
|
|
197
|
+
combinations of these logic connectives are allowed. Importantly, the
|
|
198
|
+
module provides seamless integration of :class:`CNF` and various
|
|
199
|
+
subclasses of :class:`Formula` with the possibility to clausify these on
|
|
200
|
+
demand.
|
|
201
|
+
|
|
202
|
+
.. code-block:: python
|
|
203
|
+
|
|
204
|
+
>>> from pysat.formula import *
|
|
205
|
+
>>> from pysat.solvers import Solver
|
|
206
|
+
|
|
207
|
+
# creating two formulas: CNF and XOr
|
|
208
|
+
>>> cnf = CNF(from_clauses=[[-1, 2], [-2, 3]])
|
|
209
|
+
>>> xor = Atom(1) ^ Atom(2) ^ Atom(4)
|
|
210
|
+
|
|
211
|
+
# passing the conjunction of these to the solver
|
|
212
|
+
>>> with Solver(bootstrap_with=xor & cnf) as solver:
|
|
213
|
+
... # setting Atom(3) to false results in only one model
|
|
214
|
+
... for model in solver.enum_models(assumptions=Formula.literals([~Atom(3)])):
|
|
215
|
+
... print(Formula.formulas(model, atoms_only=True)) # translating the model back to atoms
|
|
216
|
+
>>>
|
|
217
|
+
[Neg(Atom(1)), Neg(Atom(2)), Neg(Atom(3)), Atom(4)]
|
|
218
|
+
|
|
219
|
+
.. note::
|
|
220
|
+
|
|
221
|
+
Combining CNF formulas with non-CNF ones will not necessarily result
|
|
222
|
+
in the best possible encoding of the complex formula. The on-the-fly
|
|
223
|
+
encoder may introduce variables that a human would avoid using, e.g.
|
|
224
|
+
if ``cnf1`` and ``cnf2`` are :class:`CNF` formulas then ``And(cnf1,
|
|
225
|
+
cnf2)`` will introduce auxiliary variables ``v1`` and ``v2`` encoding
|
|
226
|
+
them, respectively (although it is enough to join their sets of
|
|
227
|
+
clauses).
|
|
228
|
+
|
|
179
229
|
Besides the implementations of CNF and WCNF formulas in PySAT, the
|
|
180
230
|
:mod:`pysat.formula` module also provides a way to manage variable
|
|
181
231
|
identifiers. This can be done with the use of the :class:`IDPool` manager.
|
|
@@ -215,8 +265,10 @@
|
|
|
215
265
|
#==============================================================================
|
|
216
266
|
from __future__ import print_function
|
|
217
267
|
import collections
|
|
268
|
+
from collections.abc import Iterable
|
|
218
269
|
import copy
|
|
219
270
|
import decimal
|
|
271
|
+
from enum import Enum
|
|
220
272
|
import itertools
|
|
221
273
|
import os
|
|
222
274
|
from pysat._fileio import FileObject
|
|
@@ -235,167 +287,2863 @@ except ImportError: # for Python3
|
|
|
235
287
|
from io import StringIO
|
|
236
288
|
|
|
237
289
|
|
|
238
|
-
#
|
|
239
|
-
#==============================================================================
|
|
240
|
-
class IDPool(object):
|
|
241
|
-
"""
|
|
242
|
-
A simple manager of variable IDs. It can be used as a pool of integers
|
|
243
|
-
assigning an ID to any object. Identifiers are to start from ``1`` by
|
|
244
|
-
default. The list of occupied intervals is empty be default. If
|
|
245
|
-
necessary the top variable ID can be accessed directly using the
|
|
246
|
-
``top`` variable.
|
|
290
|
+
#
|
|
291
|
+
#==============================================================================
|
|
292
|
+
class IDPool(object):
|
|
293
|
+
"""
|
|
294
|
+
A simple manager of variable IDs. It can be used as a pool of integers
|
|
295
|
+
assigning an ID to any object. Identifiers are to start from ``1`` by
|
|
296
|
+
default. The list of occupied intervals is empty be default. If
|
|
297
|
+
necessary the top variable ID can be accessed directly using the
|
|
298
|
+
``top`` variable.
|
|
299
|
+
|
|
300
|
+
The final parameter ``with_neg``, if set to ``True``, indicates that
|
|
301
|
+
the *negation* of an object assigned a variable ID ``n`` is to be
|
|
302
|
+
represented using the negative integer ``-n``. For this to work, the
|
|
303
|
+
object must have the method ``__neg__()`` implemented. This behaviour
|
|
304
|
+
is disabled by default.
|
|
305
|
+
|
|
306
|
+
:param start_from: the smallest ID to assign.
|
|
307
|
+
:param occupied: a list of occupied intervals.
|
|
308
|
+
:param with_neg: whether to support automatic negation handling
|
|
309
|
+
|
|
310
|
+
:type start_from: int
|
|
311
|
+
:type occupied: list(list(int))
|
|
312
|
+
:type with_neg: bool
|
|
313
|
+
"""
|
|
314
|
+
|
|
315
|
+
def __init__(self, start_from=1, occupied=[], with_neg=False):
|
|
316
|
+
"""
|
|
317
|
+
Constructor.
|
|
318
|
+
"""
|
|
319
|
+
|
|
320
|
+
self.restart(start_from=start_from, occupied=occupied,
|
|
321
|
+
with_neg=with_neg)
|
|
322
|
+
|
|
323
|
+
def __repr__(self):
|
|
324
|
+
"""
|
|
325
|
+
State reproducible string representaion of object.
|
|
326
|
+
"""
|
|
327
|
+
|
|
328
|
+
return f'IDPool(start_from={self.top+1}, occupied={self._occupied})'
|
|
329
|
+
|
|
330
|
+
def restart(self, start_from=1, occupied=[], with_neg=False):
|
|
331
|
+
"""
|
|
332
|
+
Restart the manager from scratch. The arguments replicate those of
|
|
333
|
+
the constructor of :class:`IDPool`.
|
|
334
|
+
"""
|
|
335
|
+
|
|
336
|
+
# initial ID
|
|
337
|
+
self.top = start_from - 1
|
|
338
|
+
|
|
339
|
+
# occupied IDs
|
|
340
|
+
self._occupied = sorted(occupied, key=lambda x: x[0])
|
|
341
|
+
|
|
342
|
+
# main dictionary storing the mapping from objects to variable IDs
|
|
343
|
+
self.obj2id = collections.defaultdict(lambda: self._next())
|
|
344
|
+
|
|
345
|
+
# mapping back from variable IDs to objects
|
|
346
|
+
# (if for whatever reason necessary)
|
|
347
|
+
self.id2obj = {}
|
|
348
|
+
|
|
349
|
+
# flag to indicate whether this IDPool object
|
|
350
|
+
# should support automatic negation handling
|
|
351
|
+
self.with_neg = with_neg
|
|
352
|
+
|
|
353
|
+
def id(self, obj=None):
|
|
354
|
+
"""
|
|
355
|
+
The method is to be used to assign an integer variable ID for a
|
|
356
|
+
given new object. If the object already has an ID, no new ID is
|
|
357
|
+
created and the old one is returned instead.
|
|
358
|
+
|
|
359
|
+
An object can be anything. In some cases it is convenient to use
|
|
360
|
+
string variable names. Note that if the object is not provided,
|
|
361
|
+
the method will return a new id unassigned to any object.
|
|
362
|
+
|
|
363
|
+
:param obj: an object to assign an ID to.
|
|
364
|
+
|
|
365
|
+
:rtype: int.
|
|
366
|
+
|
|
367
|
+
Example:
|
|
368
|
+
|
|
369
|
+
.. code-block:: python
|
|
370
|
+
|
|
371
|
+
>>> from pysat.formula import IDPool
|
|
372
|
+
>>> vpool = IDPool(occupied=[[12, 18], [3, 10]])
|
|
373
|
+
>>>
|
|
374
|
+
>>> # creating 5 unique variables for the following strings
|
|
375
|
+
>>> for i in range(5):
|
|
376
|
+
... print(vpool.id('v{0}'.format(i + 1)))
|
|
377
|
+
1
|
|
378
|
+
2
|
|
379
|
+
11
|
|
380
|
+
19
|
|
381
|
+
20
|
|
382
|
+
|
|
383
|
+
In some cases, it makes sense to create an external function for
|
|
384
|
+
accessing IDPool, e.g.:
|
|
385
|
+
|
|
386
|
+
.. code-block:: python
|
|
387
|
+
|
|
388
|
+
>>> # continuing the previous example
|
|
389
|
+
>>> var = lambda i: vpool.id('var{0}'.format(i))
|
|
390
|
+
>>> var(5)
|
|
391
|
+
20
|
|
392
|
+
>>> var('hello_world!')
|
|
393
|
+
21
|
|
394
|
+
"""
|
|
395
|
+
|
|
396
|
+
if obj is not None:
|
|
397
|
+
vid = self.obj2id[obj]
|
|
398
|
+
|
|
399
|
+
if vid not in self.id2obj:
|
|
400
|
+
self.id2obj[vid] = obj
|
|
401
|
+
|
|
402
|
+
# adding the object's negation, if required and supported
|
|
403
|
+
if self.with_neg and hasattr(obj, '__neg__'):
|
|
404
|
+
self.obj2id[-obj] = -vid
|
|
405
|
+
self.id2obj[-vid] = -obj
|
|
406
|
+
else:
|
|
407
|
+
# no object is provided => simply return a new ID
|
|
408
|
+
vid = self._next()
|
|
409
|
+
|
|
410
|
+
return vid
|
|
411
|
+
|
|
412
|
+
def obj(self, vid):
|
|
413
|
+
"""
|
|
414
|
+
The method can be used to map back a given variable identifier to
|
|
415
|
+
the original object labeled by the identifier.
|
|
416
|
+
|
|
417
|
+
:param vid: variable identifier.
|
|
418
|
+
:type vid: int
|
|
419
|
+
|
|
420
|
+
:return: an object corresponding to the given identifier.
|
|
421
|
+
|
|
422
|
+
Example:
|
|
423
|
+
|
|
424
|
+
.. code-block:: python
|
|
425
|
+
|
|
426
|
+
>>> vpool.obj(21)
|
|
427
|
+
'hello_world!'
|
|
428
|
+
"""
|
|
429
|
+
|
|
430
|
+
if vid in self.id2obj:
|
|
431
|
+
return self.id2obj[vid]
|
|
432
|
+
|
|
433
|
+
return None
|
|
434
|
+
|
|
435
|
+
def occupy(self, start, stop):
|
|
436
|
+
"""
|
|
437
|
+
Mark a given interval as occupied so that the manager could skip
|
|
438
|
+
the values from ``start`` to ``stop`` (**inclusive**).
|
|
439
|
+
|
|
440
|
+
:param start: beginning of the interval.
|
|
441
|
+
:param stop: end of the interval.
|
|
442
|
+
|
|
443
|
+
:type start: int
|
|
444
|
+
:type stop: int
|
|
445
|
+
"""
|
|
446
|
+
|
|
447
|
+
if stop >= start:
|
|
448
|
+
# the following check serves to remove unnecessary interval
|
|
449
|
+
# spawning; since the intervals are sorted, we are checking
|
|
450
|
+
# if the previous interval is a (non-strict) subset of the new one
|
|
451
|
+
if len(self._occupied) and self._occupied[-1][0] >= start and self._occupied[-1][1] <= stop:
|
|
452
|
+
self._occupied.pop()
|
|
453
|
+
|
|
454
|
+
self._occupied.append([start, stop])
|
|
455
|
+
self._occupied.sort(key=lambda x: x[0])
|
|
456
|
+
|
|
457
|
+
def _next(self):
|
|
458
|
+
"""
|
|
459
|
+
Get next variable ID. Skip occupied intervals if any.
|
|
460
|
+
"""
|
|
461
|
+
|
|
462
|
+
self.top += 1
|
|
463
|
+
|
|
464
|
+
while self._occupied and self.top >= self._occupied[0][0]:
|
|
465
|
+
if self.top <= self._occupied[0][1]:
|
|
466
|
+
self.top = self._occupied[0][1] + 1
|
|
467
|
+
|
|
468
|
+
self._occupied.pop(0)
|
|
469
|
+
|
|
470
|
+
return self.top
|
|
471
|
+
|
|
472
|
+
|
|
473
|
+
#
|
|
474
|
+
#==============================================================================
|
|
475
|
+
class FormulaError(Exception):
|
|
476
|
+
"""
|
|
477
|
+
This exception is raised when an formula-related issue occurs.
|
|
478
|
+
"""
|
|
479
|
+
|
|
480
|
+
pass
|
|
481
|
+
|
|
482
|
+
|
|
483
|
+
#
|
|
484
|
+
#==============================================================================
|
|
485
|
+
class FormulaType(Enum):
|
|
486
|
+
"""
|
|
487
|
+
This class represents a C-like ``enum`` type for choosing the formula
|
|
488
|
+
type to use. The values denoting all the formula types are as follows:
|
|
489
|
+
|
|
490
|
+
::
|
|
491
|
+
|
|
492
|
+
ATOM = 0
|
|
493
|
+
AND = 1
|
|
494
|
+
OR = 2
|
|
495
|
+
NEG = 3
|
|
496
|
+
IMPL = 4
|
|
497
|
+
EQ = 5
|
|
498
|
+
XOR = 6
|
|
499
|
+
ITE = 7
|
|
500
|
+
"""
|
|
501
|
+
|
|
502
|
+
ATOM = 0
|
|
503
|
+
AND = 1
|
|
504
|
+
OR = 2
|
|
505
|
+
NEG = 3
|
|
506
|
+
IMPL = 4
|
|
507
|
+
EQ = 5
|
|
508
|
+
XOR = 6
|
|
509
|
+
ITE = 7
|
|
510
|
+
CNF = 8 # not in the description intentionally - it should not be used directly
|
|
511
|
+
|
|
512
|
+
|
|
513
|
+
#
|
|
514
|
+
#==============================================================================
|
|
515
|
+
class Formula(object):
|
|
516
|
+
"""
|
|
517
|
+
Abstract formula class. At the same time, the class is a factory for
|
|
518
|
+
its children and can be used this way although it is recommended to
|
|
519
|
+
create objects of the children classes directly. In particular, its
|
|
520
|
+
children classes include :class:`Atom` (atomic formulas - variables
|
|
521
|
+
and constants), :class:`Neg` (negations), :class:`And` (conjunctions),
|
|
522
|
+
:class:`Or` (disjunctions), :class:`Implies` (implications),
|
|
523
|
+
:class:`Equals` (equalities), :class:`XOr` (exclusive disjunctions),
|
|
524
|
+
and :class:`ITE` (if-then-else operations).
|
|
525
|
+
|
|
526
|
+
Due to the need to clausify formulas, an object of any subclass of
|
|
527
|
+
:class:`Formula` is meant to be represented in memory by a single
|
|
528
|
+
copy. This is achieved by storing a dictionary of all the known
|
|
529
|
+
formulas attached to a given *context*. Thus, once a particular
|
|
530
|
+
context is activated, its dictionary will make sure each formula
|
|
531
|
+
variable refers to a single representation of the formula object it
|
|
532
|
+
aims to refer. When it comes to clausifying this formula, the formula
|
|
533
|
+
is encoded exactly once, despite it may be potentially used multiple
|
|
534
|
+
times as part of one of more complex formulas.
|
|
535
|
+
|
|
536
|
+
Example:
|
|
537
|
+
|
|
538
|
+
.. code-block:: python
|
|
539
|
+
|
|
540
|
+
>>> from pysat.formula import *
|
|
541
|
+
>>>
|
|
542
|
+
>>> x1, x2 = Atom('x'), Atom('x')
|
|
543
|
+
>>> id(x1) == id(x2)
|
|
544
|
+
True # x1 and x2 refer to the same atom
|
|
545
|
+
>>> id(x1 & Atom('y')) == id(Atom('y') & x2)
|
|
546
|
+
True # it holds if constructing complex formulas with them as subformulas
|
|
547
|
+
|
|
548
|
+
The class supports multi-context operation. A user may have formulas
|
|
549
|
+
created and clausified in different context. They can also switch from
|
|
550
|
+
one context to another and/or cleanup the instances known in some or
|
|
551
|
+
all contexts.
|
|
552
|
+
|
|
553
|
+
Example:
|
|
554
|
+
|
|
555
|
+
.. code-block:: python
|
|
556
|
+
|
|
557
|
+
>>> from pysat.formula import *
|
|
558
|
+
>>> f1 = Or(And(...)) # arbitrary formula
|
|
559
|
+
>>> # by default, the context is set to 'default'
|
|
560
|
+
>>> # another context can be created like this:
|
|
561
|
+
>>> Formula.set_context(context='some-other-context')
|
|
562
|
+
>>> # the new context knows nothing about formula f1
|
|
563
|
+
>>> # ...
|
|
564
|
+
>>> # cleaning up context 'some-other-context'
|
|
565
|
+
>>> # this deletes all the formulas known in this context
|
|
566
|
+
>>> Formula.cleanup(context='some-other-context')
|
|
567
|
+
>>> # switching back to 'default'
|
|
568
|
+
>>> Formula.set_context(context='default')
|
|
569
|
+
|
|
570
|
+
A user may also want to disable duplicate blocking, which can be
|
|
571
|
+
achieved by setting the context to ``None``.
|
|
572
|
+
|
|
573
|
+
Boolean constants False and True are represented by the atomic
|
|
574
|
+
"formulas" ``Atom(False)`` and ``Atom(True)``, respectively. There are
|
|
575
|
+
two constants storing these values: ``PYSAT_FALSE`` and
|
|
576
|
+
``PYSAT_TRUE``.
|
|
577
|
+
|
|
578
|
+
.. code-block:: python
|
|
579
|
+
|
|
580
|
+
>>> PYSAT_FALSE, PYSAT_TRUE
|
|
581
|
+
(Atom(False), Atom(True))
|
|
582
|
+
"""
|
|
583
|
+
|
|
584
|
+
# we don't want duplicated formulas => let's keep one copy of each
|
|
585
|
+
_instances = collections.defaultdict(lambda: {})
|
|
586
|
+
|
|
587
|
+
# similarly, we create a variable pool in case we need the formulas encoded
|
|
588
|
+
_vpool = collections.defaultdict(lambda: IDPool())
|
|
589
|
+
|
|
590
|
+
# formulas can duplicate when they appear in different contexts
|
|
591
|
+
_context = 'default'
|
|
592
|
+
|
|
593
|
+
@staticmethod
|
|
594
|
+
def set_context(context='default'):
|
|
595
|
+
"""
|
|
596
|
+
Set the current context of interest. If set to ``None``, no
|
|
597
|
+
context will be assumed and duplicate checking will be disabled as
|
|
598
|
+
a result.
|
|
599
|
+
|
|
600
|
+
:param context: new active context
|
|
601
|
+
:type context: hashable
|
|
602
|
+
"""
|
|
603
|
+
|
|
604
|
+
Formula._context = context
|
|
605
|
+
|
|
606
|
+
@staticmethod
|
|
607
|
+
def attach_vpool(vpool, context='default'):
|
|
608
|
+
"""
|
|
609
|
+
Attach an external :class:`IDPool` to be associated with a given
|
|
610
|
+
context. This is useful when a user has an already created
|
|
611
|
+
:class:`IDPool` object and wants to reuse it when clausifying
|
|
612
|
+
their :class:`Formula` objects.
|
|
613
|
+
|
|
614
|
+
:param vpool: an external variable manager
|
|
615
|
+
:param context: target context to be the user of the vpool
|
|
616
|
+
|
|
617
|
+
:type vpool: :class:`IDPool`
|
|
618
|
+
:type context: hashable
|
|
619
|
+
"""
|
|
620
|
+
|
|
621
|
+
Formula._vpool[context] = vpool
|
|
622
|
+
|
|
623
|
+
@staticmethod
|
|
624
|
+
def export_vpool(active=True, context='default'):
|
|
625
|
+
"""
|
|
626
|
+
Opposite to :meth:`attach_vpool`, this method returns a variable
|
|
627
|
+
managed attached to a given context, which may be useful for
|
|
628
|
+
external use.
|
|
629
|
+
|
|
630
|
+
:param active: export the currently active context
|
|
631
|
+
:param context: context using the vpool we are interested in (if ``active`` is ``False``)
|
|
632
|
+
|
|
633
|
+
:type active: bool
|
|
634
|
+
:type context: hashable
|
|
635
|
+
|
|
636
|
+
:rtype: :class:`IDPool`
|
|
637
|
+
"""
|
|
638
|
+
|
|
639
|
+
if active:
|
|
640
|
+
return Formula._vpool[Formula._context]
|
|
641
|
+
else:
|
|
642
|
+
return Formula._vpool[context]
|
|
643
|
+
|
|
644
|
+
@staticmethod
|
|
645
|
+
def cleanup(context=None):
|
|
646
|
+
"""
|
|
647
|
+
Clean up either a given context (if specified as different from
|
|
648
|
+
``None``) or all contexts (otherwise); afterwards, start the
|
|
649
|
+
"default" context from scratch.
|
|
650
|
+
|
|
651
|
+
A context is cleaned by destroying all the associated
|
|
652
|
+
:class:`Formula` objects and all the corresponding variable
|
|
653
|
+
managers. This may be useful if a user wants to start encoding
|
|
654
|
+
their problem from scratch.
|
|
655
|
+
|
|
656
|
+
.. note::
|
|
657
|
+
|
|
658
|
+
Once cleaning of a context is done, the objects referring to
|
|
659
|
+
the context's formulas must not be used. At this point, they
|
|
660
|
+
are orphaned and can't get re-clausified.
|
|
661
|
+
|
|
662
|
+
:param context: target context
|
|
663
|
+
:type context: ``None`` or hashable
|
|
664
|
+
"""
|
|
665
|
+
|
|
666
|
+
# preparing what needs to be cleaned
|
|
667
|
+
if context is not None:
|
|
668
|
+
# only a given context
|
|
669
|
+
to_clean = [context] if context in Formula._instances else []
|
|
670
|
+
else:
|
|
671
|
+
# everything
|
|
672
|
+
to_clean = list(set(Formula._vpool).union(set(Formula._instances)))
|
|
673
|
+
|
|
674
|
+
# actual cleaning
|
|
675
|
+
for ctx in to_clean:
|
|
676
|
+
# we never clean the '_global' context
|
|
677
|
+
if ctx == '_global':
|
|
678
|
+
continue
|
|
679
|
+
|
|
680
|
+
# deleting the content of all the formulas' in the context
|
|
681
|
+
for key in list(Formula._instances[ctx].keys()):
|
|
682
|
+
Formula._instances[ctx][key].__del__()
|
|
683
|
+
|
|
684
|
+
# deleting the corresponding instances and variable manager
|
|
685
|
+
del Formula._instances[ctx]
|
|
686
|
+
if ctx in Formula._vpool:
|
|
687
|
+
del Formula._vpool[ctx]
|
|
688
|
+
|
|
689
|
+
if not Formula._instances:
|
|
690
|
+
# there is no context left; updating the context to 'default'
|
|
691
|
+
Formula.set_context(context='default')
|
|
692
|
+
|
|
693
|
+
@staticmethod
|
|
694
|
+
def formulas(lits, atoms_only=True):
|
|
695
|
+
"""
|
|
696
|
+
Given a list of integer literal identifiers, this method returns a
|
|
697
|
+
list of formulas corresponding to these identifiers. Basically,
|
|
698
|
+
the method can be seen as mapping auxiliary variables naming
|
|
699
|
+
formulas to the corresponding formulas they name.
|
|
700
|
+
|
|
701
|
+
If the argument ``atoms_only`` is set to ``True`` only, the method
|
|
702
|
+
will return a subset of formulas, including only atomic formulas
|
|
703
|
+
(literals). Otherwise, any formula whose name occurs in the input
|
|
704
|
+
list will be included in the result.
|
|
705
|
+
|
|
706
|
+
:param lits: input list of literals
|
|
707
|
+
:param atoms_only: include all known formulas or atomic ones only
|
|
708
|
+
|
|
709
|
+
:type lits: iterable
|
|
710
|
+
:type atoms_only: bool
|
|
711
|
+
|
|
712
|
+
:rtype: list(:class:`Formula`)
|
|
713
|
+
|
|
714
|
+
Example:
|
|
715
|
+
|
|
716
|
+
.. code-block:: python
|
|
717
|
+
|
|
718
|
+
>>> from pysat.formula import *
|
|
719
|
+
>>> from pysat.solvers import Solver
|
|
720
|
+
>>>
|
|
721
|
+
>>> x, y, z = Atom('x'), Atom('y'), Atom('z')
|
|
722
|
+
>>> a = (x @ y) ^ z
|
|
723
|
+
>>>
|
|
724
|
+
>>> with Solver(bootstrap_with=a) as solver:
|
|
725
|
+
... for model in solver.enum_models():
|
|
726
|
+
... # using method formulas to map the model back to atoms
|
|
727
|
+
... print(Formula.formulas(model, atoms_only=True))
|
|
728
|
+
...
|
|
729
|
+
[Neg(Atom('x')), Neg(Atom('y')), Neg(Atom('z'))]
|
|
730
|
+
[Neg(Atom('x')), Atom('y'), Atom('z')]
|
|
731
|
+
[Atom('x'), Atom('y'), Neg(Atom('z'))]
|
|
732
|
+
[Atom('x'), Neg(Atom('y')), Atom('z')]
|
|
733
|
+
"""
|
|
734
|
+
|
|
735
|
+
forms = []
|
|
736
|
+
|
|
737
|
+
for lit in lits:
|
|
738
|
+
if lit in Formula._vpool[Formula._context].id2obj:
|
|
739
|
+
forms.append( Formula._vpool[Formula._context].obj(+lit))
|
|
740
|
+
elif -lit in Formula._vpool[Formula._context].id2obj:
|
|
741
|
+
forms.append(~Formula._vpool[Formula._context].obj(-lit))
|
|
742
|
+
|
|
743
|
+
# if required, we need to filter out everything but atomic forms
|
|
744
|
+
if atoms_only:
|
|
745
|
+
forms = [f for f in forms if type(f) == Atom or type(f) == Neg and type(f.subformula) == Atom]
|
|
746
|
+
|
|
747
|
+
return forms
|
|
748
|
+
|
|
749
|
+
@staticmethod
|
|
750
|
+
def literals(forms):
|
|
751
|
+
"""
|
|
752
|
+
Extract formula names for a given list of formulas and return them
|
|
753
|
+
as a list of integer identifiers. Essentially, the method is the
|
|
754
|
+
opposite to :meth:`formulas`.
|
|
755
|
+
|
|
756
|
+
:param forms: list of formulas to map
|
|
757
|
+
:type forms: iterable
|
|
758
|
+
|
|
759
|
+
:rtype: list(int)
|
|
760
|
+
|
|
761
|
+
Example:
|
|
762
|
+
|
|
763
|
+
.. code-block:: python
|
|
764
|
+
|
|
765
|
+
>>> from pysat.solvers import Solver
|
|
766
|
+
>>> from pysat.formula import *
|
|
767
|
+
>>>
|
|
768
|
+
>>> x, y, z = Atom('x'), Atom('y'), Atom('z')
|
|
769
|
+
>>> a = (x @ y) ^ z
|
|
770
|
+
>>>
|
|
771
|
+
>>> # applying Tseitin transformation to formula a
|
|
772
|
+
>>> a.clausify()
|
|
773
|
+
>>>
|
|
774
|
+
>>> # checking what facts the internal vpool knows
|
|
775
|
+
>>> print(Formula.export_vpool().id2obj)
|
|
776
|
+
{1: Atom('x'), 2: Atom('y'), 3: Equals[Atom('x'), Atom('y')], 4: Atom('z')}
|
|
777
|
+
>>>
|
|
778
|
+
>>> # now, mapping two atoms to their integer id representations
|
|
779
|
+
>>> Formula.literals(forms=[Atom('x'), ~Atom('z')])
|
|
780
|
+
[1, -4]
|
|
781
|
+
"""
|
|
782
|
+
|
|
783
|
+
lits = []
|
|
784
|
+
|
|
785
|
+
for form in forms:
|
|
786
|
+
if form in (PYSAT_TRUE, PYSAT_FALSE):
|
|
787
|
+
continue
|
|
788
|
+
|
|
789
|
+
if not form.name:
|
|
790
|
+
form._clausify(name_required=True)
|
|
791
|
+
|
|
792
|
+
lits.append(Formula._vpool[Formula._context].id(form))
|
|
793
|
+
|
|
794
|
+
return lits
|
|
795
|
+
|
|
796
|
+
def __init__(self, *args, **kwargs):
|
|
797
|
+
"""
|
|
798
|
+
Base Formula initialiser. Expects a sole keyword argument
|
|
799
|
+
``type``, which must be assigned to an enumeration value of
|
|
800
|
+
:class:`FormulaType`.
|
|
801
|
+
|
|
802
|
+
:param type: type of the formula
|
|
803
|
+
:type type: :class:`FormulaType`
|
|
804
|
+
"""
|
|
805
|
+
|
|
806
|
+
assert 'type' in kwargs, 'No \'type\' of the formula is provided'
|
|
807
|
+
self.type = kwargs['type']
|
|
808
|
+
|
|
809
|
+
self.name = None # Tseitin variable encoding the formula clausification
|
|
810
|
+
self.clauses = [] # raw clausal representation of the formula
|
|
811
|
+
self.encoded = [] # tseitin-encoded clauses representing the formula
|
|
812
|
+
|
|
813
|
+
@classmethod
|
|
814
|
+
def __new__(cls, *args, **kwargs):
|
|
815
|
+
"""
|
|
816
|
+
Factory-like formula constructor, which avoids creating duplicated
|
|
817
|
+
formulas if we are currently using a context. Otherwise, simply
|
|
818
|
+
creates a formula object of a given type, without duplicate
|
|
819
|
+
checking.
|
|
820
|
+
"""
|
|
821
|
+
|
|
822
|
+
def _create_object():
|
|
823
|
+
if cls is Formula:
|
|
824
|
+
assert 'type' in kwargs, 'No \'type\' of the formula is provided'
|
|
825
|
+
type = kwargs['type']
|
|
826
|
+
|
|
827
|
+
if type == FormulaType.ATOM:
|
|
828
|
+
return super(Formula, Atom).__new__(Atom)
|
|
829
|
+
if type == FormulaType.AND:
|
|
830
|
+
return super(Formula, And).__new__(And)
|
|
831
|
+
if type == FormulaType.OR:
|
|
832
|
+
return super(Formula, Or).__new__(Or)
|
|
833
|
+
if type == FormulaType.NEG:
|
|
834
|
+
return super(Formula, Neg).__new__(Neg)
|
|
835
|
+
if type == FormulaType.IMPL:
|
|
836
|
+
return super(Formula, Implies).__new__(Implies)
|
|
837
|
+
if type == FormulaType.EQ:
|
|
838
|
+
return super(Formula, Equals).__new__(Equals)
|
|
839
|
+
if type == FormulaType.XOR:
|
|
840
|
+
return super(Formula, XOr).__new__(XOr)
|
|
841
|
+
if type == FormulaType.ITE:
|
|
842
|
+
return super(Formula, ITE).__new__(ITE)
|
|
843
|
+
else:
|
|
844
|
+
raise FormulaError('Unexpected formula type')
|
|
845
|
+
else:
|
|
846
|
+
return super(Formula, cls).__new__(cls)
|
|
847
|
+
|
|
848
|
+
if Formula._context is not None:
|
|
849
|
+
# there is an active context different from None
|
|
850
|
+
# hence, we need to make sure we don't duplicate formulas
|
|
851
|
+
|
|
852
|
+
# getting the key to associate the formula with
|
|
853
|
+
key = Formula._get_key(args, kwargs)
|
|
854
|
+
|
|
855
|
+
# first, checking if the key is known to represent a global constant
|
|
856
|
+
if key in Formula._instances['_global']:
|
|
857
|
+
return Formula._instances['_global'][key]
|
|
858
|
+
|
|
859
|
+
# then, checking if the key is known in the current context
|
|
860
|
+
if key not in Formula._instances[Formula._context]:
|
|
861
|
+
# this key is yet unknown; creating a new formula object
|
|
862
|
+
Formula._instances[Formula._context][key] = _create_object()
|
|
863
|
+
|
|
864
|
+
# returning the object associated with the requested formula
|
|
865
|
+
return Formula._instances[Formula._context][key]
|
|
866
|
+
else:
|
|
867
|
+
return _create_object()
|
|
868
|
+
|
|
869
|
+
def __deepcopy__(self, memo):
|
|
870
|
+
"""
|
|
871
|
+
As no duplicates are allowed, no deep copying is allowed either.
|
|
872
|
+
"""
|
|
873
|
+
|
|
874
|
+
raise FormulaError('Formula class does not allow deep copying')
|
|
875
|
+
|
|
876
|
+
@staticmethod
|
|
877
|
+
def _get_key(*args, **kwargs):
|
|
878
|
+
"""
|
|
879
|
+
The method is used to extract and return a list of attributes to
|
|
880
|
+
serve as a key associated with the formula requested by a user.
|
|
881
|
+
The flattened result will contain string, i.e. ``repr()``,
|
|
882
|
+
representation of all the attributes. The method is meant to be
|
|
883
|
+
internal and not to be used by formula users.
|
|
884
|
+
"""
|
|
885
|
+
|
|
886
|
+
def _hashable(entity):
|
|
887
|
+
try:
|
|
888
|
+
hash(entity)
|
|
889
|
+
return True
|
|
890
|
+
except TypeError:
|
|
891
|
+
return False
|
|
892
|
+
|
|
893
|
+
def _flatten(collection, prefix='', sep='_'):
|
|
894
|
+
items = []
|
|
895
|
+
|
|
896
|
+
# first, assuming it is a flat unhashable collection
|
|
897
|
+
if type(collection) in (tuple, list, set):
|
|
898
|
+
collection = tuple(filter(lambda x: x or x == False, collection))
|
|
899
|
+
|
|
900
|
+
for item in collection:
|
|
901
|
+
if isinstance(item, Iterable):
|
|
902
|
+
items.extend(_flatten(item, prefix=prefix))
|
|
903
|
+
else:
|
|
904
|
+
items.append((prefix, item))
|
|
905
|
+
|
|
906
|
+
# next, checking if it is a dictionary
|
|
907
|
+
elif isinstance(collection, dict):
|
|
908
|
+
for key in collection.keys():
|
|
909
|
+
value = collection[key]
|
|
910
|
+
new_key = prefix + sep + key if prefix else key
|
|
911
|
+
if isinstance(value, Iterable) and value:
|
|
912
|
+
items.extend(_flatten(value, prefix=new_key, sep=sep))
|
|
913
|
+
else:
|
|
914
|
+
|
|
915
|
+
items.append((new_key, value))
|
|
916
|
+
|
|
917
|
+
# finally, it is a simple non-empty object
|
|
918
|
+
elif collection or collection == False:
|
|
919
|
+
items.append((prefix, collection))
|
|
920
|
+
|
|
921
|
+
return items
|
|
922
|
+
|
|
923
|
+
# in case the class of the formula is passed in the arguments
|
|
924
|
+
# we filter it out so that it does not mess up the key computation
|
|
925
|
+
if args:
|
|
926
|
+
args = list(args)
|
|
927
|
+
if type(args[0]) == tuple and type(args[0][0]) == type:
|
|
928
|
+
args[0] = tuple(args[0][1:])
|
|
929
|
+
|
|
930
|
+
# flattening all the inputs
|
|
931
|
+
items = _flatten(args) + _flatten(kwargs)
|
|
932
|
+
ftype, subfs, extra = None, [], []
|
|
933
|
+
|
|
934
|
+
# ignoring everything except subformulas and the type
|
|
935
|
+
for item in items:
|
|
936
|
+
if item[0] == 'type':
|
|
937
|
+
ftype = item
|
|
938
|
+
elif item[0] != 'merge':
|
|
939
|
+
subfs.append(item)
|
|
940
|
+
else:
|
|
941
|
+
extra = [('merge', repr(item[1]))]
|
|
942
|
+
|
|
943
|
+
# the ugly part:
|
|
944
|
+
# reconstructing the key pairs, depending on the type of Formula
|
|
945
|
+
if ftype[1] == FormulaType.ATOM:
|
|
946
|
+
assert len(subfs) == 1, 'A single object is required for an Atom formula'
|
|
947
|
+
subfs[0] = ('object', repr(subfs[0][1]))
|
|
948
|
+
elif ftype[1] == FormulaType.NEG:
|
|
949
|
+
assert len(subfs) == 1, 'A single subformula is required for a Neg formula'
|
|
950
|
+
subfs[0] = ('subformula', id(subfs[0][1]))
|
|
951
|
+
elif ftype[1] == FormulaType.IMPL:
|
|
952
|
+
assert len(subfs) == 2, 'Two subformulas are required for an Implies formula'
|
|
953
|
+
if subfs[0][0] == '' and subfs[1][0] == '':
|
|
954
|
+
subfs[0] = ('left', id(subfs[0][1]))
|
|
955
|
+
subfs[1] = ('right', id(subfs[1][1]))
|
|
956
|
+
elif subfs[1][0] == 'left':
|
|
957
|
+
subfs[0] = ('right', id(subfs[0][1]))
|
|
958
|
+
elif subfs[1][0] == 'right':
|
|
959
|
+
subfs[0] = ('left', id(subfs[0][1]))
|
|
960
|
+
elif ftype[1] == FormulaType.ITE:
|
|
961
|
+
assert len(subfs) == 3, 'Three subformulas are required for an ITE formula'
|
|
962
|
+
if subfs[0][0] == '' and subfs[1][0] == '' and subfs[2][0] == '':
|
|
963
|
+
subfs[0] = ('cond', id(subfs[0][1]))
|
|
964
|
+
subfs[1] = ('cons1', id(subfs[1][1]))
|
|
965
|
+
subfs[2] = ('cons2', id(subfs[2][1]))
|
|
966
|
+
elif subfs[0][0] == subfs[1][0] == '':
|
|
967
|
+
if subfs[2][0] == 'cond':
|
|
968
|
+
subfs[0] = ('cons1', id(subfs[0][1]))
|
|
969
|
+
subfs[1] = ('cons2', id(subfs[1][1]))
|
|
970
|
+
elif subfs[2][0] == 'cons1':
|
|
971
|
+
subfs[0] = ('cond', id(subfs[0][1]))
|
|
972
|
+
subfs[1] = ('cons2', id(subfs[1][1]))
|
|
973
|
+
elif subfs[2][0] == 'cons2':
|
|
974
|
+
subfs[0] = ('cond', id(subfs[0][1]))
|
|
975
|
+
subfs[1] = ('cons1', id(subfs[1][1]))
|
|
976
|
+
elif subfs[0] == '':
|
|
977
|
+
if 'cond' not in (subfs[1][0], subfs[2][0]):
|
|
978
|
+
subfs[0] = ('cond', id(subfs[0][0]))
|
|
979
|
+
elif 'cons1' not in (subfs[1][0], subfs[2][0]):
|
|
980
|
+
subfs[0] = ('cons1', id(subfs[0][0]))
|
|
981
|
+
elif 'cons2' not in (subfs[1][0], subfs[2][0]):
|
|
982
|
+
subfs[0] = ('cons2', id(subfs[0][0]))
|
|
983
|
+
else:
|
|
984
|
+
# these are commutative connectives; we need to sort the arguments
|
|
985
|
+
assert len(subfs) >= 1, 'At least one subformula is required for an And/Equals/Or/XOr formula'
|
|
986
|
+
subfs = sorted(map(lambda p: (p[0], repr(id(p[1]))), subfs))
|
|
987
|
+
|
|
988
|
+
if not extra:
|
|
989
|
+
extra = [('merge', repr(False))]
|
|
990
|
+
|
|
991
|
+
# the key is a string combining all the parts
|
|
992
|
+
return ' '.join([f'{repr(p[0])}:{repr(p[1])}' for p in [ftype] + subfs + extra])
|
|
993
|
+
|
|
994
|
+
def __hash__(self):
|
|
995
|
+
"""
|
|
996
|
+
This method provides us with a trivial way to make
|
|
997
|
+
:class:`Formula` objects hashable. Currently, the implementation
|
|
998
|
+
returns a hash of the string representation of the object.
|
|
999
|
+
"""
|
|
1000
|
+
|
|
1001
|
+
return hash(repr(self))
|
|
1002
|
+
|
|
1003
|
+
def _merge_suboperands(self):
|
|
1004
|
+
"""
|
|
1005
|
+
Auxiliary method used when constructing a new formula to flatten
|
|
1006
|
+
repetitive operations, i.e. if ``self`` equals ``And(left,
|
|
1007
|
+
And(center, right))``, it turns it into formula ``And(left,
|
|
1008
|
+
center, right)`` instead. No arguments are expected. The method is
|
|
1009
|
+
meant for internal use of :class:`Formula` only.
|
|
1010
|
+
"""
|
|
1011
|
+
|
|
1012
|
+
for i, subf in enumerate(self.subformulas):
|
|
1013
|
+
# is there any term of our type among the operands?
|
|
1014
|
+
if subf.type == self.type:
|
|
1015
|
+
operands = subf.subformulas[:]
|
|
1016
|
+
|
|
1017
|
+
# yes => try to put everything under its cap
|
|
1018
|
+
for j in itertools.filterfalse(lambda j: j == i, range(len(self.subformulas))):
|
|
1019
|
+
if self.subformulas[j].type == self.type:
|
|
1020
|
+
operands.extend(self.subformulas[j].subformulas)
|
|
1021
|
+
else:
|
|
1022
|
+
operands.append(self.subformulas[j])
|
|
1023
|
+
|
|
1024
|
+
# we are done with simplification by now
|
|
1025
|
+
self.subformulas = operands
|
|
1026
|
+
break
|
|
1027
|
+
|
|
1028
|
+
def __invert__(self):
|
|
1029
|
+
"""
|
|
1030
|
+
Negation operator overloaded for class :class:`Formula`. Given an
|
|
1031
|
+
object ``f`` of class :class:`Formula`, applying ``~f`` returns a
|
|
1032
|
+
new object ``Neg(f)``.
|
|
1033
|
+
"""
|
|
1034
|
+
|
|
1035
|
+
if self.type != FormulaType.NEG:
|
|
1036
|
+
if self == PYSAT_TRUE:
|
|
1037
|
+
return PYSAT_FALSE
|
|
1038
|
+
if self == PYSAT_FALSE:
|
|
1039
|
+
return PYSAT_TRUE
|
|
1040
|
+
return Neg(self)
|
|
1041
|
+
return self.subformula
|
|
1042
|
+
|
|
1043
|
+
def __neg__(self):
|
|
1044
|
+
"""
|
|
1045
|
+
Negation operator. Takes the same effect as ``__invert__()``.
|
|
1046
|
+
"""
|
|
1047
|
+
|
|
1048
|
+
return self.__invert__()
|
|
1049
|
+
|
|
1050
|
+
def __and__(self, other):
|
|
1051
|
+
"""
|
|
1052
|
+
Logical conjunction operator overloaded for class
|
|
1053
|
+
:class:`Formula`. Given two objects ``a`` and ``b`` of class
|
|
1054
|
+
:class:`Formula`, doing ``a & b`` returns a new object ``And(a,
|
|
1055
|
+
b)``. Applies merging sub-operands.
|
|
1056
|
+
"""
|
|
1057
|
+
|
|
1058
|
+
return And(self, other, merge=True)
|
|
1059
|
+
|
|
1060
|
+
def __iand__(self, other):
|
|
1061
|
+
"""
|
|
1062
|
+
An in-place equivalent of :meth:`__and__`. Given two objects ``a``
|
|
1063
|
+
and ``b`` of class :class:`Formula`, doing ``a &= b`` replaces
|
|
1064
|
+
``a`` with a new object ``And(a, b)``. Applies merging
|
|
1065
|
+
sub-operands.
|
|
1066
|
+
"""
|
|
1067
|
+
|
|
1068
|
+
return And(self, other, merge=True)
|
|
1069
|
+
|
|
1070
|
+
def __or__(self, other):
|
|
1071
|
+
"""
|
|
1072
|
+
Logical disjunction operator overloaded for class
|
|
1073
|
+
:class:`Formula`. Given two objects ``a`` and ``b`` of class
|
|
1074
|
+
:class:`Formula`, doing ``a | b`` returns a new object ``Or(a,
|
|
1075
|
+
b)``. Applies merging sub-operands.
|
|
1076
|
+
"""
|
|
1077
|
+
|
|
1078
|
+
return Or(self, other, merge=True)
|
|
1079
|
+
|
|
1080
|
+
def __ior__(self, other):
|
|
1081
|
+
"""
|
|
1082
|
+
An in-place equivalent of :meth:`__or__`. Given two objects ``a``
|
|
1083
|
+
and ``b`` of class :class:`Formula`, doing ``a |= b`` replaces
|
|
1084
|
+
``a`` with a new object ``Or(a, b)``. Applies merging
|
|
1085
|
+
sub-operands.
|
|
1086
|
+
"""
|
|
1087
|
+
|
|
1088
|
+
return Or(self, other, merge=True)
|
|
1089
|
+
|
|
1090
|
+
def __rshift__(self, other):
|
|
1091
|
+
"""
|
|
1092
|
+
Bitwise right shift operator overloaded for class :class:`Formula`
|
|
1093
|
+
with the semantics of logical implication. Given two objects ``a``
|
|
1094
|
+
and ``b`` of class :class:`Formula`, doing ``a >> b`` returns a
|
|
1095
|
+
new object ``Implies(a, b)``.
|
|
1096
|
+
"""
|
|
1097
|
+
|
|
1098
|
+
return Implies(self, other)
|
|
1099
|
+
|
|
1100
|
+
def __irshift__(self, other):
|
|
1101
|
+
"""
|
|
1102
|
+
An in-place equivalent of :meth:`__rshift__`. Given two objects
|
|
1103
|
+
``a`` and ``b`` of class :class:`Formula`, doing ``a >>= b``
|
|
1104
|
+
assigns ``a`` to a new object ``Implies(a, b)``.
|
|
1105
|
+
"""
|
|
1106
|
+
|
|
1107
|
+
return Implies(self, other)
|
|
1108
|
+
|
|
1109
|
+
def __lshift__(self, other):
|
|
1110
|
+
"""
|
|
1111
|
+
Bitwise left shift operator overloaded for class :class:`Formula`
|
|
1112
|
+
with the semantics of logical implication. Given two objects ``a``
|
|
1113
|
+
and ``b`` of class :class:`Formula`, doing ``a << b`` returns a
|
|
1114
|
+
new object ``Implies(b, a)``.
|
|
1115
|
+
"""
|
|
1116
|
+
|
|
1117
|
+
return Implies(other, self)
|
|
1118
|
+
|
|
1119
|
+
def __ilshift__(self, other):
|
|
1120
|
+
"""
|
|
1121
|
+
An in-place equivalent of :meth:`__lshift__`. Given two objects
|
|
1122
|
+
``a`` and ``b`` of class :class:`Formula`, doing ``a <<= b``
|
|
1123
|
+
assigns ``a`` to a new object ``Implies(b, a)``.
|
|
1124
|
+
"""
|
|
1125
|
+
|
|
1126
|
+
return Implies(other, self)
|
|
1127
|
+
|
|
1128
|
+
def __matmul__(self, other):
|
|
1129
|
+
"""
|
|
1130
|
+
Vector multiplication operator overloaded for class
|
|
1131
|
+
:class:`Formula` with the semantics of logical equivalence. Given
|
|
1132
|
+
two objects ``a`` and ``b`` of class :class:`Formula`, doing ``a @
|
|
1133
|
+
b`` returns a new object ``Equals(a, b)``. Applies merging
|
|
1134
|
+
sub-operands.
|
|
1135
|
+
"""
|
|
1136
|
+
|
|
1137
|
+
return Equals(self, other, merge=True)
|
|
1138
|
+
|
|
1139
|
+
def __imatmul__(self, other):
|
|
1140
|
+
"""
|
|
1141
|
+
An in-place variant of :meth:`__matmul__`. Given two objects ``a``
|
|
1142
|
+
and ``b`` of class :class:`Formula`, doing ``a @= b`` assigns
|
|
1143
|
+
``a`` to a new object ``Equals(a, b)``. Applies merging
|
|
1144
|
+
sub-operands.
|
|
1145
|
+
"""
|
|
1146
|
+
|
|
1147
|
+
return Equals(self, other, merge=True)
|
|
1148
|
+
|
|
1149
|
+
def __xor__(self, other):
|
|
1150
|
+
"""
|
|
1151
|
+
Bitwise exclusive disjunction overloaded for class
|
|
1152
|
+
:class:`Formula`. Given two objects ``a`` and ``b`` of class
|
|
1153
|
+
:class:`Formula`, doing ``a ^ b`` returns a new object ``XOr(a,
|
|
1154
|
+
b)``.
|
|
1155
|
+
"""
|
|
1156
|
+
|
|
1157
|
+
return XOr(self, other, merge=True)
|
|
1158
|
+
|
|
1159
|
+
def __ixor__(self, other):
|
|
1160
|
+
"""
|
|
1161
|
+
An in-place variant of :meth:`__xor__`. Given two objects ``a``
|
|
1162
|
+
and ``b`` of class :class:`Formula`, doing ``a ^= b`` assigns
|
|
1163
|
+
``a`` to a new object ``XOr(a, b)``.
|
|
1164
|
+
"""
|
|
1165
|
+
|
|
1166
|
+
return XOr(self, other, merge=True)
|
|
1167
|
+
|
|
1168
|
+
def __iter__(self):
|
|
1169
|
+
"""
|
|
1170
|
+
Implements an iterator over all clauses of the formula. As the
|
|
1171
|
+
clauses are stored not only in ``self`` but also in its
|
|
1172
|
+
subformulas, the iterator runs recursively by means of calling
|
|
1173
|
+
recursive method :meth:`_iter`.
|
|
1174
|
+
|
|
1175
|
+
Before iterating through the clauses, applies Tseitin
|
|
1176
|
+
transformation (see :meth:`clausify`).
|
|
1177
|
+
|
|
1178
|
+
Example:
|
|
1179
|
+
|
|
1180
|
+
.. code-block:: python
|
|
1181
|
+
|
|
1182
|
+
>>> from pysat.formula import *
|
|
1183
|
+
>>> x, y, z = Atom('x'), Atom('y'), Atom('z')
|
|
1184
|
+
>>> a = (x @ y) | z
|
|
1185
|
+
>>> # iterating through the clauses and printing them as a list
|
|
1186
|
+
>>> [cl for cl in a]
|
|
1187
|
+
[[-1, 2, -3], [1, -2, -3], [3, -1, -2], [3, 1, 2], [3, 4]]
|
|
1188
|
+
>>>
|
|
1189
|
+
>>> # let's see what meaning the identifiers have
|
|
1190
|
+
>>> Formula.export_vpool().id2obj
|
|
1191
|
+
{1: Atom('x'), 2: Atom('y'), 3: Equals[Atom('x'), Atom('y')], 4: Atom('z')}
|
|
1192
|
+
"""
|
|
1193
|
+
|
|
1194
|
+
# first, make sure there is a clausal representation
|
|
1195
|
+
self.clausify()
|
|
1196
|
+
|
|
1197
|
+
# then recursively iterate through the clauses
|
|
1198
|
+
for cl in self._iter({}, outermost=True):
|
|
1199
|
+
if PYSAT_TRUE.name not in cl:
|
|
1200
|
+
yield [l for l in cl if l != PYSAT_FALSE.name]
|
|
1201
|
+
|
|
1202
|
+
def clausify(self):
|
|
1203
|
+
"""
|
|
1204
|
+
This method applies Tseitin transformation to the formula.
|
|
1205
|
+
Recursively gives all the formulas Boolean names accordingly and
|
|
1206
|
+
uses them in the current logic connective following its semantics.
|
|
1207
|
+
As a result, each subformula stores its clausal representation
|
|
1208
|
+
independently of other subformulas (and independently of the root
|
|
1209
|
+
formula).
|
|
1210
|
+
|
|
1211
|
+
Example:
|
|
1212
|
+
|
|
1213
|
+
.. code-block:: python
|
|
1214
|
+
|
|
1215
|
+
>>> from pysat.formula import *
|
|
1216
|
+
>>> x, y, z = Atom('x'), Atom('y'), Atom('z')
|
|
1217
|
+
>>> a = (x @ y) | z
|
|
1218
|
+
>>>
|
|
1219
|
+
>>> a.clausify() # clausifying formula a
|
|
1220
|
+
>>>
|
|
1221
|
+
>>> # let's what clauses represent the root logic connective
|
|
1222
|
+
>>> a.clauses
|
|
1223
|
+
[[3, 4]] # 4 corresponds to z while 3 represents the equality x @ y
|
|
1224
|
+
"""
|
|
1225
|
+
|
|
1226
|
+
self._clausify(name_required=False)
|
|
1227
|
+
|
|
1228
|
+
def simplified(self, assumptions=[]):
|
|
1229
|
+
"""
|
|
1230
|
+
Given a list of assumption atomic formula literals, this method
|
|
1231
|
+
recursively assigns these atoms to the corresponding values
|
|
1232
|
+
followed by formula simplification. As a result, a new formula
|
|
1233
|
+
object is returned.
|
|
1234
|
+
|
|
1235
|
+
:param assumptions: atomic formula objects
|
|
1236
|
+
:type assumptions: list
|
|
1237
|
+
|
|
1238
|
+
:rtype: :class:`Formula`
|
|
1239
|
+
|
|
1240
|
+
Example:
|
|
1241
|
+
|
|
1242
|
+
.. code-block:: python
|
|
1243
|
+
|
|
1244
|
+
>>> from pysat.formula import *
|
|
1245
|
+
>>>
|
|
1246
|
+
>>> x, y, z = Atom('x'), Atom('y'), Atom('z')
|
|
1247
|
+
>>> a = (x @ y) | z # a formula over 3 variables: x, y, and z
|
|
1248
|
+
>>>
|
|
1249
|
+
>>> a.simplified(assumptions=[z])
|
|
1250
|
+
Atom(True)
|
|
1251
|
+
>>>
|
|
1252
|
+
>>> a.simplified(assumptions=[~z])
|
|
1253
|
+
Equals[Atom('x'), Atom('y')]
|
|
1254
|
+
>>>
|
|
1255
|
+
>>> b = a ^ Atom('p') # a more complex formula
|
|
1256
|
+
>>>
|
|
1257
|
+
>>> b.simplified(assumptions=[x, ~Atom('p')])
|
|
1258
|
+
Or[Atom('y'), Atom('z')]
|
|
1259
|
+
"""
|
|
1260
|
+
|
|
1261
|
+
raise FormulaError('No simplification method found for this formula type')
|
|
1262
|
+
|
|
1263
|
+
def satisfied(self, model):
|
|
1264
|
+
"""
|
|
1265
|
+
Given a list of atomic formulas, this method checks whether the
|
|
1266
|
+
current formula is satisfied by assigning these atoms. The method
|
|
1267
|
+
returns ``True`` if the formula gets satisfied, ``False`` if it is
|
|
1268
|
+
falsified, and ``None`` if the answer is unknown.
|
|
1269
|
+
|
|
1270
|
+
:param model: list of atomic formulas
|
|
1271
|
+
:type model: list(:class:`Formula`)
|
|
1272
|
+
|
|
1273
|
+
:rtype: bool or ``None``
|
|
1274
|
+
|
|
1275
|
+
Example:
|
|
1276
|
+
|
|
1277
|
+
.. code-block:: python
|
|
1278
|
+
|
|
1279
|
+
>>> from pysat.formula import *
|
|
1280
|
+
>>>
|
|
1281
|
+
>>> x, y, z = Atom('x'), Atom('y'), Atom('z')
|
|
1282
|
+
>>> a = (x @ y) | z
|
|
1283
|
+
>>>
|
|
1284
|
+
>>> a.satisfied(model=[z])
|
|
1285
|
+
True
|
|
1286
|
+
>>> a.satisfied(model=[~z])
|
|
1287
|
+
>>> # None, as it is not enough to set ~z to determine satisfiability of a
|
|
1288
|
+
"""
|
|
1289
|
+
|
|
1290
|
+
simp = self.simplified(assumptions=model)
|
|
1291
|
+
|
|
1292
|
+
if simp == PYSAT_TRUE:
|
|
1293
|
+
return True
|
|
1294
|
+
elif simp == PYSAT_FALSE:
|
|
1295
|
+
return False
|
|
1296
|
+
|
|
1297
|
+
def atoms(self, constants=False):
|
|
1298
|
+
"""
|
|
1299
|
+
Returns a list of all the atomic formulas (variables and, if
|
|
1300
|
+
required, constants) that this formula is built from. The method
|
|
1301
|
+
recursively traverses the formula tree and collects all the atoms
|
|
1302
|
+
it finds.
|
|
1303
|
+
|
|
1304
|
+
:param constants: include Boolean constants in the list
|
|
1305
|
+
:type constants: bool
|
|
1306
|
+
|
|
1307
|
+
:rtype: list(:class:`Atom`)
|
|
1308
|
+
|
|
1309
|
+
Example:
|
|
1310
|
+
|
|
1311
|
+
.. code-block:: python
|
|
1312
|
+
|
|
1313
|
+
>>> from pysat.formula import *
|
|
1314
|
+
>>>
|
|
1315
|
+
>>> x, y, z = Atom('x'), Atom('y'), Atom('z')
|
|
1316
|
+
>>> a = (x @ y) | z
|
|
1317
|
+
>>>
|
|
1318
|
+
>>> a.atoms()
|
|
1319
|
+
[Atom('x'), Atom('y'), Atom('z')]
|
|
1320
|
+
"""
|
|
1321
|
+
|
|
1322
|
+
dest = set()
|
|
1323
|
+
self._atoms(dest)
|
|
1324
|
+
|
|
1325
|
+
if constants is False:
|
|
1326
|
+
if PYSAT_TRUE in dest:
|
|
1327
|
+
dest.remove(PYSAT_TRUE)
|
|
1328
|
+
|
|
1329
|
+
if PYSAT_FALSE in dest:
|
|
1330
|
+
dest.remove(PYSAT_FALSE)
|
|
1331
|
+
|
|
1332
|
+
return list(dest)
|
|
1333
|
+
|
|
1334
|
+
|
|
1335
|
+
#
|
|
1336
|
+
#==============================================================================
|
|
1337
|
+
class Atom(Formula):
|
|
1338
|
+
"""
|
|
1339
|
+
Atomic formula, i.e. a variable or constant. Although we often refer
|
|
1340
|
+
to negated literals as atomic formulas too, they are techically
|
|
1341
|
+
implemented as ``Neg(Atom)``.
|
|
1342
|
+
|
|
1343
|
+
To create an atomic formula, a user needs to specify an ``object``
|
|
1344
|
+
this formula should signify. When it comes to clausifying the formulas
|
|
1345
|
+
this atom is involved in, the atom receives an auxiliary variable
|
|
1346
|
+
assigned to it as a ``name``.
|
|
1347
|
+
|
|
1348
|
+
Example:
|
|
1349
|
+
|
|
1350
|
+
.. code-block:: python
|
|
1351
|
+
|
|
1352
|
+
>>> from pysat.formula import *
|
|
1353
|
+
>>> x = Atom('x')
|
|
1354
|
+
>>> y = Atom(object='y')
|
|
1355
|
+
>>> # checking x's name
|
|
1356
|
+
>>> x.name
|
|
1357
|
+
>>> # None
|
|
1358
|
+
>>> # right, that's because the atom is not yet clausified
|
|
1359
|
+
>>> x.clausify()
|
|
1360
|
+
>>> x.name
|
|
1361
|
+
1
|
|
1362
|
+
|
|
1363
|
+
If a given object is a positive integer (negative integers aren't
|
|
1364
|
+
allowed), the integer itself serves as the atom's name, which is
|
|
1365
|
+
assigned in the constructor, i.e. no call to :meth:`clausify` is
|
|
1366
|
+
required.
|
|
1367
|
+
|
|
1368
|
+
Example:
|
|
1369
|
+
|
|
1370
|
+
.. code-block:: python
|
|
1371
|
+
|
|
1372
|
+
>>> from pysat.formula import Atom
|
|
1373
|
+
>>> x, y = Atom(1), Atom(2)
|
|
1374
|
+
>>> x.name
|
|
1375
|
+
1
|
|
1376
|
+
>>> y.name
|
|
1377
|
+
2
|
|
1378
|
+
|
|
1379
|
+
Special atoms are reserved for the Boolean constants ``True`` and
|
|
1380
|
+
``False``. Namely, ``Atom(False)`` and ``Atom(True)`` can be accessed
|
|
1381
|
+
through the constants ``PYSAT_FALSE`` and ``PYSAT_TRUE``,
|
|
1382
|
+
respectively.
|
|
1383
|
+
|
|
1384
|
+
Example:
|
|
1385
|
+
|
|
1386
|
+
.. code-block:: python
|
|
1387
|
+
|
|
1388
|
+
>>> from pysat.formula import *
|
|
1389
|
+
>>>
|
|
1390
|
+
>>> print(PYSAT_TRUE, repr(PYSAT_TRUE))
|
|
1391
|
+
T Atom(True)
|
|
1392
|
+
>>>
|
|
1393
|
+
>>> print(PYSAT_FALSE, repr(PYSAT_FALSE))
|
|
1394
|
+
F Atom(False)
|
|
1395
|
+
|
|
1396
|
+
.. note::
|
|
1397
|
+
|
|
1398
|
+
Constant ``Atom(True)`` is distinguished from variable ``Atom(1)``
|
|
1399
|
+
by checking the type of the object (bool vs int).
|
|
1400
|
+
"""
|
|
1401
|
+
|
|
1402
|
+
def __new__(cls, *args, **kwargs):
|
|
1403
|
+
"""
|
|
1404
|
+
Atom constructor.
|
|
1405
|
+
"""
|
|
1406
|
+
|
|
1407
|
+
return Formula.__new__(cls, args, kwargs, type=FormulaType.ATOM)
|
|
1408
|
+
|
|
1409
|
+
def __init__(self, *args, **kwargs):
|
|
1410
|
+
"""
|
|
1411
|
+
Initialiser. The method should receive either an argument or a
|
|
1412
|
+
keyword argument ``object`` signifying the object the new atom is
|
|
1413
|
+
meant to represent.
|
|
1414
|
+
|
|
1415
|
+
:param object: object of interest
|
|
1416
|
+
:type object: any
|
|
1417
|
+
"""
|
|
1418
|
+
|
|
1419
|
+
super(Atom, self).__init__(type=FormulaType.ATOM)
|
|
1420
|
+
|
|
1421
|
+
if args:
|
|
1422
|
+
self.object = args[0]
|
|
1423
|
+
elif 'object' in kwargs:
|
|
1424
|
+
self.object = kwargs['object']
|
|
1425
|
+
else:
|
|
1426
|
+
assert 0, 'No object is given for this atom'
|
|
1427
|
+
|
|
1428
|
+
if type(self.object) == int:
|
|
1429
|
+
# user seems to want to use integer variable directly
|
|
1430
|
+
assert self.object > 0, 'Variables should be represented as positive integers'
|
|
1431
|
+
|
|
1432
|
+
self.name = self.object # using the integer id as the name
|
|
1433
|
+
self.clauses = [[self.name]]
|
|
1434
|
+
|
|
1435
|
+
Formula._vpool[Formula._context].obj2id[self] = self.name
|
|
1436
|
+
Formula._vpool[Formula._context].id2obj[self.name] = self
|
|
1437
|
+
Formula._vpool[Formula._context].occupy(1, self.name)
|
|
1438
|
+
|
|
1439
|
+
def __del__(self):
|
|
1440
|
+
"""
|
|
1441
|
+
Atom destructor.
|
|
1442
|
+
"""
|
|
1443
|
+
|
|
1444
|
+
self.name = None
|
|
1445
|
+
self.clauses = []
|
|
1446
|
+
self.encoded = [] # always empty
|
|
1447
|
+
self.object = None
|
|
1448
|
+
|
|
1449
|
+
def _iter(self, seen, outermost=False):
|
|
1450
|
+
"""
|
|
1451
|
+
Internal iterator over the clauses. Does nothing as there are no
|
|
1452
|
+
clauses to iterate through.
|
|
1453
|
+
"""
|
|
1454
|
+
|
|
1455
|
+
if not self in seen:
|
|
1456
|
+
seen[self] = True
|
|
1457
|
+
|
|
1458
|
+
if outermost:
|
|
1459
|
+
yield from self.clauses
|
|
1460
|
+
else:
|
|
1461
|
+
yield from self.encoded
|
|
1462
|
+
|
|
1463
|
+
def _clausify(self, name_required=True):
|
|
1464
|
+
"""
|
|
1465
|
+
Atom clausification. Basically, the method just assigns an
|
|
1466
|
+
auxiliary variable to serve as a ``name`` of the atom. It does not
|
|
1467
|
+
produce any clauses (the name is left for consistency with the
|
|
1468
|
+
rest of formula types).
|
|
1469
|
+
"""
|
|
1470
|
+
|
|
1471
|
+
# true and false constants shouldn't be encoded
|
|
1472
|
+
if not self.name and self.object not in (False, True):
|
|
1473
|
+
self.name = Formula._vpool[Formula._context].id(self)
|
|
1474
|
+
self.clauses = [[self.name]]
|
|
1475
|
+
|
|
1476
|
+
def simplified(self, assumptions=[]):
|
|
1477
|
+
"""
|
|
1478
|
+
Checks if the current literal appears in the list of assumptions
|
|
1479
|
+
provided in argument ``assumptions``. If it is, the method returns
|
|
1480
|
+
``PYSAT_TRUE``. If the opposite atom is present in
|
|
1481
|
+
``assumptions``, the method returns ``PYSAT_FALSE``. Otherwise, it
|
|
1482
|
+
return ``self``.
|
|
1483
|
+
|
|
1484
|
+
:param assumptions: atomic assumptions
|
|
1485
|
+
:type assumptions: list(:class:`Formula`)
|
|
1486
|
+
|
|
1487
|
+
:rtype: PYSAT_TRUE, PYSAT_FALSE, or self
|
|
1488
|
+
"""
|
|
1489
|
+
|
|
1490
|
+
if self in assumptions:
|
|
1491
|
+
return PYSAT_TRUE
|
|
1492
|
+
elif ~self in assumptions:
|
|
1493
|
+
return PYSAT_FALSE
|
|
1494
|
+
return self
|
|
1495
|
+
|
|
1496
|
+
def _atoms(self, dest):
|
|
1497
|
+
"""
|
|
1498
|
+
The base case of recursive atom collection. Updates the collection
|
|
1499
|
+
with ``self`` atom.
|
|
1500
|
+
|
|
1501
|
+
:param dest: the set of atoms to collect
|
|
1502
|
+
:type dest: set(:class:`Atom`)
|
|
1503
|
+
"""
|
|
1504
|
+
|
|
1505
|
+
dest.add(self)
|
|
1506
|
+
|
|
1507
|
+
def __repr__(self):
|
|
1508
|
+
"""
|
|
1509
|
+
State reproducible string representaion of object.
|
|
1510
|
+
"""
|
|
1511
|
+
|
|
1512
|
+
return f'{self.__class__.__name__}({repr(self.object)})'
|
|
1513
|
+
|
|
1514
|
+
def __str__(self):
|
|
1515
|
+
"""
|
|
1516
|
+
Informal representation of the Atom. Returns a string
|
|
1517
|
+
representation of the underlying object. For constants
|
|
1518
|
+
``PYSAT_FALSE`` and ``PYSAT_TRUE``, the method returns ``'F'`` and
|
|
1519
|
+
``'T'``, respectively.
|
|
1520
|
+
"""
|
|
1521
|
+
|
|
1522
|
+
if not isinstance(self.object, bool):
|
|
1523
|
+
return str(self.object)
|
|
1524
|
+
else:
|
|
1525
|
+
return 'F' if self.object == False else 'T'
|
|
1526
|
+
|
|
1527
|
+
|
|
1528
|
+
# true and false constants (stored in the '_global' context)
|
|
1529
|
+
# in fact, this is where the '_global' context is first created
|
|
1530
|
+
#==============================================================================
|
|
1531
|
+
Formula.set_context('_global')
|
|
1532
|
+
PYSAT_FALSE, PYSAT_TRUE = Atom(False), Atom(True)
|
|
1533
|
+
PYSAT_FALSE.name, PYSAT_TRUE.name = -0.5, 0.5 # special (floating-point) values for the constants
|
|
1534
|
+
# different from all variable names
|
|
1535
|
+
PYSAT_FALSE.clauses, PYSAT_TRUE.clauses = [[-0.5]], [[0.5]] # unit clauses for the constants
|
|
1536
|
+
# FALSE should turn into an empty clause;
|
|
1537
|
+
# while the clause for TRUE should be removed
|
|
1538
|
+
Formula.set_context('default')
|
|
1539
|
+
|
|
1540
|
+
|
|
1541
|
+
#
|
|
1542
|
+
#==============================================================================
|
|
1543
|
+
class And(Formula):
|
|
1544
|
+
"""
|
|
1545
|
+
Conjunction. Given a list of operands (subformulas) :math:`f_i`,
|
|
1546
|
+
:math:`i \\in \\{1,\\ldots,n\\}, n \\in \\mathbb{N}`, it creates a
|
|
1547
|
+
formula :math:`\\bigwedge_{i=1}^{n}{f_i}`. The list of operands *of
|
|
1548
|
+
size at least 1* should be passed as arguments to the constructor.
|
|
1549
|
+
|
|
1550
|
+
Example:
|
|
1551
|
+
|
|
1552
|
+
.. code-block:: python
|
|
1553
|
+
|
|
1554
|
+
>>> from pysat.formula import *
|
|
1555
|
+
>>> x, y, z = Atom('x'), Atom('y'), Atom('z')
|
|
1556
|
+
>>> conj = And(x, y, z)
|
|
1557
|
+
|
|
1558
|
+
If an additional Boolean keyword argument ``merge`` is provided set to
|
|
1559
|
+
``True``, the toolkit will try to flatten the current :class:`And`
|
|
1560
|
+
formula merging its *conjuctive* sub-operands into the list of
|
|
1561
|
+
operands. For example, if ``And(And(x, y), z, merge=True)`` is called,
|
|
1562
|
+
a new Formula object will be created with two operands: ``And(x, y)``
|
|
1563
|
+
and ``z``, followed by merging ``x`` and ``y`` into the list of
|
|
1564
|
+
root-level ``And``. This will result in a formula ``And(x, y, z)``.
|
|
1565
|
+
Merging sub-operands is enabled by default if bitwise operations are
|
|
1566
|
+
used to create ``And`` formulas.
|
|
1567
|
+
|
|
1568
|
+
Example:
|
|
1569
|
+
|
|
1570
|
+
.. code-block:: python
|
|
1571
|
+
|
|
1572
|
+
>>> from pysat.formula import *
|
|
1573
|
+
>>> a1 = And(And(Atom('x'), Atom('y')), Atom('z'))
|
|
1574
|
+
>>> a2 = And(And(Atom('x'), Atom('y')), Atom('z'), merge=True)
|
|
1575
|
+
>>> a3 = Atom('x') & Atom('y') & Atom('z')
|
|
1576
|
+
>>>
|
|
1577
|
+
>>> repr(a1)
|
|
1578
|
+
"And[And[Atom('x'), Atom('y')], Atom('z')]"
|
|
1579
|
+
>>> repr(a2)
|
|
1580
|
+
"And[Atom('x'), Atom('y'), Atom('z')]"
|
|
1581
|
+
>>> repr(a3)
|
|
1582
|
+
"And[Atom('x'), Atom('y'), Atom('z')]"
|
|
1583
|
+
>>>
|
|
1584
|
+
>>> id(a1) == id(a2)
|
|
1585
|
+
False
|
|
1586
|
+
>>>
|
|
1587
|
+
>>> id(a2) == id(a3)
|
|
1588
|
+
True # formulas a2 and a3 refer to the same object
|
|
1589
|
+
|
|
1590
|
+
.. note::
|
|
1591
|
+
|
|
1592
|
+
If there are two formulas representing the same fact with and
|
|
1593
|
+
without merging enabled, they technically sit in two distinct
|
|
1594
|
+
objects. Although PySAT tries to avoid it, clausification of these
|
|
1595
|
+
two formulas may result in unique (different) auxiliary variables
|
|
1596
|
+
assigned to such two formulas.
|
|
1597
|
+
"""
|
|
1598
|
+
|
|
1599
|
+
def __new__(cls, *args, **kwargs):
|
|
1600
|
+
"""
|
|
1601
|
+
Conjunction constructor.
|
|
1602
|
+
"""
|
|
1603
|
+
|
|
1604
|
+
return Formula.__new__(cls, args, kwargs, type=FormulaType.AND)
|
|
1605
|
+
|
|
1606
|
+
def __init__(self, *args, **kwargs):
|
|
1607
|
+
"""
|
|
1608
|
+
Initialiser. Expects a list of arguments signifying the operands
|
|
1609
|
+
of the conjunction. Additionally, a user may set a keyword
|
|
1610
|
+
argument ``merge=True``, which will enable merging sub-operands.
|
|
1611
|
+
"""
|
|
1612
|
+
|
|
1613
|
+
super(And, self).__init__(type=FormulaType.AND)
|
|
1614
|
+
|
|
1615
|
+
self.subformulas = list(args)
|
|
1616
|
+
|
|
1617
|
+
if 'merge' in kwargs and kwargs['merge'] == True:
|
|
1618
|
+
self._merge_suboperands()
|
|
1619
|
+
self.merged = True
|
|
1620
|
+
else:
|
|
1621
|
+
self.merged = False
|
|
1622
|
+
|
|
1623
|
+
def __del__(self):
|
|
1624
|
+
"""
|
|
1625
|
+
And destructor.
|
|
1626
|
+
"""
|
|
1627
|
+
|
|
1628
|
+
self.name = None
|
|
1629
|
+
self.clauses = []
|
|
1630
|
+
self.encoded = []
|
|
1631
|
+
self.subformulas = []
|
|
1632
|
+
|
|
1633
|
+
def _iter(self, seen, outermost=False):
|
|
1634
|
+
"""
|
|
1635
|
+
Internal iterator over the clauses. First, iterates through the
|
|
1636
|
+
clauses of the subformulas followed by the formula's own clauses.
|
|
1637
|
+
"""
|
|
1638
|
+
|
|
1639
|
+
if not self in seen:
|
|
1640
|
+
seen[self] = True
|
|
1641
|
+
|
|
1642
|
+
for sub in self.subformulas:
|
|
1643
|
+
for cl in sub._iter(seen):
|
|
1644
|
+
yield cl
|
|
1645
|
+
|
|
1646
|
+
if outermost:
|
|
1647
|
+
yield from self.clauses
|
|
1648
|
+
else:
|
|
1649
|
+
yield from self.encoded
|
|
1650
|
+
|
|
1651
|
+
def simplified(self, assumptions=[]):
|
|
1652
|
+
"""
|
|
1653
|
+
Given a list of assumption literals, recursively simplifies the
|
|
1654
|
+
subformulas and creates a new formula.
|
|
1655
|
+
|
|
1656
|
+
:param assumptions: atomic assumptions
|
|
1657
|
+
:type assumptions: list(:class:`Formula`)
|
|
1658
|
+
|
|
1659
|
+
:rtype: :class:`Formula`
|
|
1660
|
+
|
|
1661
|
+
Example:
|
|
1662
|
+
|
|
1663
|
+
.. code-block:: python
|
|
1664
|
+
|
|
1665
|
+
>>> from pysat.formula import *
|
|
1666
|
+
>>> x, y, z = Atom('x'), Atom('y'), Atom('z')
|
|
1667
|
+
>>> a = x & y & z
|
|
1668
|
+
>>>
|
|
1669
|
+
>>> print(a.simplified(assumptions=[y]))
|
|
1670
|
+
x & z
|
|
1671
|
+
>>> print(a.simplified(assumptions=[~y]))
|
|
1672
|
+
F # False
|
|
1673
|
+
"""
|
|
1674
|
+
|
|
1675
|
+
oset = set()
|
|
1676
|
+
operands = []
|
|
1677
|
+
|
|
1678
|
+
for sub in self.subformulas:
|
|
1679
|
+
# execute simplification recursively
|
|
1680
|
+
sub = sub.simplified(assumptions=set(assumptions))
|
|
1681
|
+
|
|
1682
|
+
# simplify the current term
|
|
1683
|
+
if sub is PYSAT_FALSE or ~sub in oset:
|
|
1684
|
+
return PYSAT_FALSE
|
|
1685
|
+
|
|
1686
|
+
# a new yet unsimplified term
|
|
1687
|
+
elif sub != PYSAT_TRUE and sub not in oset:
|
|
1688
|
+
oset.add(sub)
|
|
1689
|
+
operands.append(sub)
|
|
1690
|
+
|
|
1691
|
+
# there is nothing left and we did not come across False
|
|
1692
|
+
# we should return True
|
|
1693
|
+
if not operands:
|
|
1694
|
+
return PYSAT_TRUE
|
|
1695
|
+
|
|
1696
|
+
# there is one operand left; we need to return it instead
|
|
1697
|
+
if len(operands) == 1:
|
|
1698
|
+
return operands[0]
|
|
1699
|
+
|
|
1700
|
+
return And(*operands, merge=True) if self.merged else And(*operands)
|
|
1701
|
+
|
|
1702
|
+
def _clausify(self, name_required=True):
|
|
1703
|
+
"""
|
|
1704
|
+
Conjuction clausification.
|
|
1705
|
+
|
|
1706
|
+
If ``name_required`` is ``False``, the method recursively encodes
|
|
1707
|
+
the subformulas and populates self's clauses with unit clauses,
|
|
1708
|
+
each containing the auxiliary "name" of the corresponding
|
|
1709
|
+
subformula, thus representing their conjunction.
|
|
1710
|
+
|
|
1711
|
+
If ``name_required`` is set to ``True``, the method encodes the
|
|
1712
|
+
conjunction using the standard logic: :math:`x \\equiv
|
|
1713
|
+
\\bigwedge{y_i}`, if :math:`x` is the new auxiliary variable
|
|
1714
|
+
encoding ``self`` and :math:`y_i` is the auxiliary variable
|
|
1715
|
+
representing :math:`i`'s subformula.
|
|
1716
|
+
|
|
1717
|
+
:param name_required: whether or not a Tseitin variable is needed
|
|
1718
|
+
:param name_required: bool
|
|
1719
|
+
"""
|
|
1720
|
+
|
|
1721
|
+
save_clauses = bool(self.clauses)
|
|
1722
|
+
|
|
1723
|
+
if not self.clauses:
|
|
1724
|
+
# first, recursively encoding subformulas
|
|
1725
|
+
for sub in self.subformulas:
|
|
1726
|
+
sub._clausify(name_required=True)
|
|
1727
|
+
|
|
1728
|
+
# adding unit clauses
|
|
1729
|
+
self.clauses.append([sub.name])
|
|
1730
|
+
|
|
1731
|
+
# introducing a new name for this formula if required
|
|
1732
|
+
if name_required and not self.name:
|
|
1733
|
+
if save_clauses:
|
|
1734
|
+
self.encoded = [clause.copy() for clause in self.clauses]
|
|
1735
|
+
else:
|
|
1736
|
+
self.encoded = self.clauses
|
|
1737
|
+
self.clauses = []
|
|
1738
|
+
|
|
1739
|
+
self.name = Formula._vpool[Formula._context].id(self)
|
|
1740
|
+
|
|
1741
|
+
cl = [self.name] # final clause (converse implication)
|
|
1742
|
+
for i in range(len(self.encoded)):
|
|
1743
|
+
cl.append(-self.encoded[i][0]) # updating final clause
|
|
1744
|
+
self.encoded[i].append(-self.name) # updating direct implication
|
|
1745
|
+
|
|
1746
|
+
# adding final clause
|
|
1747
|
+
self.encoded.append(cl)
|
|
1748
|
+
|
|
1749
|
+
def _atoms(self, dest):
|
|
1750
|
+
"""
|
|
1751
|
+
Recursive atom collection.
|
|
1752
|
+
|
|
1753
|
+
:param dest: where the atoms are collected
|
|
1754
|
+
:type dest: set(:class:`Atom`)
|
|
1755
|
+
"""
|
|
1756
|
+
|
|
1757
|
+
for sub in self.subformulas:
|
|
1758
|
+
sub._atoms(dest)
|
|
1759
|
+
|
|
1760
|
+
def __repr__(self):
|
|
1761
|
+
"""
|
|
1762
|
+
State reproducible string representaion of object.
|
|
1763
|
+
"""
|
|
1764
|
+
|
|
1765
|
+
return f'{self.__class__.__name__}{repr(self.subformulas)}'
|
|
1766
|
+
|
|
1767
|
+
def __str__(self):
|
|
1768
|
+
"""
|
|
1769
|
+
Informal representation of the ``And`` formula. Returns a string
|
|
1770
|
+
representations of the underlying subformulas joined with ``' &
|
|
1771
|
+
'``.
|
|
1772
|
+
"""
|
|
1773
|
+
|
|
1774
|
+
return ' & '.join([f'{str(sub)}' if sub.type in (FormulaType.ATOM, FormulaType.NEG) else f'({str(sub)})' for sub in self.subformulas])
|
|
1775
|
+
|
|
1776
|
+
|
|
1777
|
+
#
|
|
1778
|
+
#==============================================================================
|
|
1779
|
+
class Or(Formula):
|
|
1780
|
+
"""
|
|
1781
|
+
Disjunction. Given a list of operands (subformulas) :math:`f_i`,
|
|
1782
|
+
:math:`i \\in \\{1,\\ldots,n\\}, n \\in \\mathbb{N}`, it creates a
|
|
1783
|
+
formula :math:`\\bigvee_{i=1}^{n}{f_i}`. The list of operands *of size
|
|
1784
|
+
at least 1* should be passed as arguments to the constructor.
|
|
1785
|
+
|
|
1786
|
+
Example:
|
|
1787
|
+
|
|
1788
|
+
.. code-block:: python
|
|
1789
|
+
|
|
1790
|
+
>>> from pysat.formula import *
|
|
1791
|
+
>>> x, y, z = Atom('x'), Atom('y'), Atom('z')
|
|
1792
|
+
>>> conj = Or(x, y, z)
|
|
1793
|
+
|
|
1794
|
+
If an additional Boolean keyword argument ``merge`` is provided set to
|
|
1795
|
+
``True``, the toolkit will try to flatten the current :class:`Or`
|
|
1796
|
+
formula merging its *conjuctive* sub-operands into the list of
|
|
1797
|
+
operands. For example, if ``Or(Or(x, y), z, merge=True)`` is called, a
|
|
1798
|
+
new Formula object will be created with two operands: ``Or(x, y)`` and
|
|
1799
|
+
``z``, followed by merging ``x`` and ``y`` into the list of root-level
|
|
1800
|
+
``Or``. This will result in a formula ``Or(x, y, z)``. Merging
|
|
1801
|
+
sub-operands is enabled by default if bitwise operations are used to
|
|
1802
|
+
create ``Or`` formulas.
|
|
1803
|
+
|
|
1804
|
+
Example:
|
|
1805
|
+
|
|
1806
|
+
.. code-block:: python
|
|
1807
|
+
|
|
1808
|
+
>>> from pysat.formula import *
|
|
1809
|
+
>>> a1 = Or(Or(Atom('x'), Atom('y')), Atom('z'))
|
|
1810
|
+
>>> a2 = Or(Or(Atom('x'), Atom('y')), Atom('z'), merge=True)
|
|
1811
|
+
>>> a3 = Atom('x') | Atom('y') | Atom('z')
|
|
1812
|
+
>>>
|
|
1813
|
+
>>> repr(a1)
|
|
1814
|
+
"Or[Or[Atom('x'), Atom('y')], Atom('z')]"
|
|
1815
|
+
>>> repr(a2)
|
|
1816
|
+
"Or[Atom('x'), Atom('y'), Atom('z')]"
|
|
1817
|
+
>>> repr(a2)
|
|
1818
|
+
"Or[Atom('x'), Atom('y'), Atom('z')]"
|
|
1819
|
+
>>>
|
|
1820
|
+
>>> id(a1) == id(a2)
|
|
1821
|
+
False
|
|
1822
|
+
>>>
|
|
1823
|
+
>>> id(a2) == id(a3)
|
|
1824
|
+
True # formulas a2 and a3 refer to the same object
|
|
1825
|
+
|
|
1826
|
+
.. note::
|
|
1827
|
+
|
|
1828
|
+
If there are two formulas representing the same fact with and
|
|
1829
|
+
without merging enabled, they technically sit in two distinct
|
|
1830
|
+
objects. Although PySAT tries to avoid it, clausification of these
|
|
1831
|
+
two formulas may result in unique (different) auxiliary variables
|
|
1832
|
+
assigned to such two formulas.
|
|
1833
|
+
"""
|
|
1834
|
+
|
|
1835
|
+
def __new__(cls, *args, **kwargs):
|
|
1836
|
+
"""
|
|
1837
|
+
Disjunction constructor.
|
|
1838
|
+
"""
|
|
1839
|
+
|
|
1840
|
+
return Formula.__new__(cls, args, kwargs, type=FormulaType.OR)
|
|
1841
|
+
|
|
1842
|
+
def __init__(self, *args, **kwargs):
|
|
1843
|
+
"""
|
|
1844
|
+
Initialiser. Expects a list of arguments signifying the operands
|
|
1845
|
+
of the disjunction. Additionally, a user may set a keyword
|
|
1846
|
+
argument ``merge=True``, which will enable merging sub-operands.
|
|
1847
|
+
"""
|
|
1848
|
+
|
|
1849
|
+
super(Or, self).__init__(type=FormulaType.OR)
|
|
1850
|
+
|
|
1851
|
+
self.subformulas = list(args)
|
|
1852
|
+
|
|
1853
|
+
if 'merge' in kwargs and kwargs['merge'] == True:
|
|
1854
|
+
self._merge_suboperands()
|
|
1855
|
+
self.merged = True
|
|
1856
|
+
else:
|
|
1857
|
+
self.merged = False
|
|
1858
|
+
|
|
1859
|
+
def __del__(self):
|
|
1860
|
+
"""
|
|
1861
|
+
Destructor.
|
|
1862
|
+
"""
|
|
1863
|
+
|
|
1864
|
+
self.name = None
|
|
1865
|
+
self.clauses = []
|
|
1866
|
+
self.encoded = []
|
|
1867
|
+
self.subformulas = []
|
|
1868
|
+
|
|
1869
|
+
def _iter(self, seen, outermost=False):
|
|
1870
|
+
"""
|
|
1871
|
+
Internal iterator over the clauses. First, iterates through the
|
|
1872
|
+
clauses of the subformulas followed by the formula's own clauses.
|
|
1873
|
+
"""
|
|
1874
|
+
|
|
1875
|
+
if not self in seen:
|
|
1876
|
+
seen[self] = True
|
|
1877
|
+
|
|
1878
|
+
for sub in self.subformulas:
|
|
1879
|
+
for cl in sub._iter(seen):
|
|
1880
|
+
yield cl
|
|
1881
|
+
|
|
1882
|
+
if outermost:
|
|
1883
|
+
yield from self.clauses
|
|
1884
|
+
else:
|
|
1885
|
+
yield from self.encoded
|
|
1886
|
+
|
|
1887
|
+
def simplified(self, assumptions=[]):
|
|
1888
|
+
"""
|
|
1889
|
+
Given a list of assumption literals, recursively simplifies the
|
|
1890
|
+
subformulas and creates a new formula.
|
|
1891
|
+
|
|
1892
|
+
:param assumptions: atomic assumptions
|
|
1893
|
+
:type assumptions: list(:class:`Formula`)
|
|
1894
|
+
|
|
1895
|
+
:rtype: :class:`Formula`
|
|
1896
|
+
|
|
1897
|
+
Example:
|
|
1898
|
+
|
|
1899
|
+
.. code-block:: python
|
|
1900
|
+
|
|
1901
|
+
>>> from pysat.formula import *
|
|
1902
|
+
>>> x, y, z = Atom('x'), Atom('y'), Atom('z')
|
|
1903
|
+
>>> a = x | y | z
|
|
1904
|
+
>>>
|
|
1905
|
+
>>> print(a.simplified(assumptions=[y]))
|
|
1906
|
+
T # True
|
|
1907
|
+
>>> print(a.simplified(assumptions=[~y]))
|
|
1908
|
+
x | z
|
|
1909
|
+
"""
|
|
1910
|
+
|
|
1911
|
+
oset = set()
|
|
1912
|
+
operands = []
|
|
1913
|
+
|
|
1914
|
+
for sub in self.subformulas:
|
|
1915
|
+
# execute simplification recursively
|
|
1916
|
+
sub = sub.simplified(assumptions=set(assumptions))
|
|
1917
|
+
|
|
1918
|
+
# simplify the current term
|
|
1919
|
+
if sub is PYSAT_TRUE or ~sub in oset:
|
|
1920
|
+
return PYSAT_TRUE
|
|
1921
|
+
|
|
1922
|
+
# a new yet unsimplified term
|
|
1923
|
+
elif sub != PYSAT_FALSE and sub not in oset:
|
|
1924
|
+
oset.add(sub)
|
|
1925
|
+
operands.append(sub)
|
|
1926
|
+
|
|
1927
|
+
# there is nothing left and we did not come across True
|
|
1928
|
+
# we should return False
|
|
1929
|
+
if not operands:
|
|
1930
|
+
return PYSAT_FALSE
|
|
1931
|
+
|
|
1932
|
+
# there is one operand left; we need to return it instead
|
|
1933
|
+
if len(operands) == 1:
|
|
1934
|
+
return operands[0]
|
|
1935
|
+
|
|
1936
|
+
return Or(*operands, merge=True) if self.merged else Or(*operands)
|
|
1937
|
+
|
|
1938
|
+
def _clausify(self, name_required=True):
|
|
1939
|
+
"""
|
|
1940
|
+
Disjunction clausification.
|
|
1941
|
+
|
|
1942
|
+
If ``name_required`` is ``False``, the method recursively encodes
|
|
1943
|
+
the subformulas and populates self's clauses with unit clauses,
|
|
1944
|
+
each containing the auxiliary "name" of the corresponding
|
|
1945
|
+
subformula, thus representing their conjunction.
|
|
1946
|
+
|
|
1947
|
+
If ``name_required`` is set to ``True``, the method encodes the
|
|
1948
|
+
conjunction using the standard logic: :math:`x \\equiv
|
|
1949
|
+
\\bigvee{y_i}`, if :math:`x` is the new auxiliary variable
|
|
1950
|
+
encoding ``self`` and :math:`y_i` is the auxiliary variable
|
|
1951
|
+
representing :math:`i`'s subformula.
|
|
1952
|
+
|
|
1953
|
+
:param name_required: whether or not a Tseitin variable is needed
|
|
1954
|
+
:param name_required: bool
|
|
1955
|
+
"""
|
|
1956
|
+
|
|
1957
|
+
save_clauses = bool(self.clauses)
|
|
1958
|
+
|
|
1959
|
+
if not self.clauses:
|
|
1960
|
+
# first, recursively encoding subformulas
|
|
1961
|
+
self.clauses.append([]) # empty clause, to be filled out
|
|
1962
|
+
for sub in self.subformulas:
|
|
1963
|
+
sub._clausify(name_required=True)
|
|
1964
|
+
|
|
1965
|
+
# adding operand names to the clause
|
|
1966
|
+
self.clauses[0].append(sub.name)
|
|
1967
|
+
|
|
1968
|
+
# introducing a new name for this formula if required
|
|
1969
|
+
if name_required and not self.name:
|
|
1970
|
+
if save_clauses:
|
|
1971
|
+
self.encoded = [clause.copy() for clause in self.clauses]
|
|
1972
|
+
else:
|
|
1973
|
+
self.encoded = self.clauses
|
|
1974
|
+
self.clauses = []
|
|
1975
|
+
|
|
1976
|
+
self.name = Formula._vpool[Formula._context].id(self)
|
|
1977
|
+
|
|
1978
|
+
# direct implication
|
|
1979
|
+
self.encoded[0].append(-self.name)
|
|
1980
|
+
|
|
1981
|
+
# clauses representing converse implication
|
|
1982
|
+
for i in range(len(self.encoded[0]) - 1):
|
|
1983
|
+
self.encoded.append([self.name, -self.encoded[0][i]])
|
|
1984
|
+
|
|
1985
|
+
def _atoms(self, dest):
|
|
1986
|
+
"""
|
|
1987
|
+
Recursive atom collection.
|
|
1988
|
+
|
|
1989
|
+
:param dest: where the atoms are collected
|
|
1990
|
+
:type dest: set(:class:`Atom`)
|
|
1991
|
+
"""
|
|
1992
|
+
|
|
1993
|
+
for sub in self.subformulas:
|
|
1994
|
+
sub._atoms(dest)
|
|
1995
|
+
|
|
1996
|
+
def __repr__(self):
|
|
1997
|
+
"""
|
|
1998
|
+
State reproducible string representaion of object.
|
|
1999
|
+
"""
|
|
2000
|
+
|
|
2001
|
+
return f'{self.__class__.__name__}{repr(self.subformulas)}'
|
|
2002
|
+
|
|
2003
|
+
def __str__(self):
|
|
2004
|
+
"""
|
|
2005
|
+
Informal representation of the ``Or`` formula. Returns a string
|
|
2006
|
+
representations of the underlying subformulas joined with ``' |
|
|
2007
|
+
'``.
|
|
2008
|
+
"""
|
|
2009
|
+
|
|
2010
|
+
return ' | '.join([f'{str(sub)}' if sub.type in (FormulaType.ATOM, FormulaType.NEG) else f'({str(sub)})' for sub in self.subformulas])
|
|
2011
|
+
|
|
2012
|
+
|
|
2013
|
+
#
|
|
2014
|
+
#==============================================================================
|
|
2015
|
+
class Neg(Formula):
|
|
2016
|
+
"""
|
|
2017
|
+
Negation. Given a single operand (subformula) :math:`f`, it creates a
|
|
2018
|
+
formula :math:`\\neg{f}`. The operand must be passed as an argument to
|
|
2019
|
+
the constructor.
|
|
2020
|
+
|
|
2021
|
+
Example:
|
|
2022
|
+
|
|
2023
|
+
.. code-block:: python
|
|
2024
|
+
|
|
2025
|
+
>>> from pysat.formula import *
|
|
2026
|
+
>>> x = Atom('x')
|
|
2027
|
+
>>> n1 = Neg(x)
|
|
2028
|
+
>>> n2 = Neg(subformula=x)
|
|
2029
|
+
>>> print(n1, n2)
|
|
2030
|
+
~x, ~x
|
|
2031
|
+
>>> n3 = ~n1
|
|
2032
|
+
>>> print(n3)
|
|
2033
|
+
x
|
|
2034
|
+
"""
|
|
2035
|
+
|
|
2036
|
+
def __new__(cls, *args, **kwargs):
|
|
2037
|
+
"""
|
|
2038
|
+
Negation constructor.
|
|
2039
|
+
"""
|
|
2040
|
+
|
|
2041
|
+
return Formula.__new__(cls, args, kwargs, type=FormulaType.NEG)
|
|
2042
|
+
|
|
2043
|
+
def __init__(self, *args, **kwargs):
|
|
2044
|
+
"""
|
|
2045
|
+
Initialiser. Expects either a single argument or a single keyword
|
|
2046
|
+
argument ``subformula`` to specify the operand of the negation.
|
|
2047
|
+
"""
|
|
2048
|
+
|
|
2049
|
+
super(Neg, self).__init__(type=FormulaType.NEG)
|
|
2050
|
+
|
|
2051
|
+
if args:
|
|
2052
|
+
self.subformula = args[0]
|
|
2053
|
+
elif 'subformula' in kwargs:
|
|
2054
|
+
self.subformula = kwargs['subformula']
|
|
2055
|
+
else:
|
|
2056
|
+
assert 0, 'No subformula is given for this atom'
|
|
2057
|
+
|
|
2058
|
+
def __del__(self):
|
|
2059
|
+
"""
|
|
2060
|
+
Destructor.
|
|
2061
|
+
"""
|
|
2062
|
+
|
|
2063
|
+
self.name = None
|
|
2064
|
+
self.clauses = []
|
|
2065
|
+
self.encoded = []
|
|
2066
|
+
self.subformula = None
|
|
2067
|
+
|
|
2068
|
+
def _iter(self, seen, outermost=False):
|
|
2069
|
+
"""
|
|
2070
|
+
Recursive iterator through the clauses.
|
|
2071
|
+
"""
|
|
2072
|
+
|
|
2073
|
+
if not self in seen:
|
|
2074
|
+
seen[self] = True
|
|
2075
|
+
|
|
2076
|
+
for cl in self.subformula._iter(seen):
|
|
2077
|
+
yield cl
|
|
2078
|
+
|
|
2079
|
+
if outermost:
|
|
2080
|
+
yield from self.clauses
|
|
2081
|
+
else:
|
|
2082
|
+
yield from self.encoded
|
|
2083
|
+
|
|
2084
|
+
def simplified(self, assumptions=[]):
|
|
2085
|
+
"""
|
|
2086
|
+
Given a list of assumption literals, recursively simplifies the
|
|
2087
|
+
subformula and then creates and returns a new formula with this
|
|
2088
|
+
simplified subformula.
|
|
2089
|
+
|
|
2090
|
+
:param assumptions: atomic assumptions
|
|
2091
|
+
:type assumptions: list(:class:`Formula`)
|
|
2092
|
+
|
|
2093
|
+
:rtype: :class:`Formula`
|
|
2094
|
+
|
|
2095
|
+
Example:
|
|
2096
|
+
|
|
2097
|
+
.. code-block:: python
|
|
2098
|
+
|
|
2099
|
+
>>> from pysat.formula import *
|
|
2100
|
+
>>> x, y, z = Atom('x'), Atom('y'), Atom('z')
|
|
2101
|
+
>>> a = x & y | z
|
|
2102
|
+
>>> b = ~a
|
|
2103
|
+
>>>
|
|
2104
|
+
>>> print(b.simplified(assumptions=[y]))
|
|
2105
|
+
~(x | z)
|
|
2106
|
+
>>> print(b.simplified(assumptions=[~y]))
|
|
2107
|
+
~z
|
|
2108
|
+
"""
|
|
2109
|
+
|
|
2110
|
+
subformula = self.subformula.simplified(assumptions=set(assumptions))
|
|
2111
|
+
|
|
2112
|
+
if subformula is PYSAT_FALSE:
|
|
2113
|
+
return PYSAT_TRUE
|
|
2114
|
+
elif subformula is PYSAT_TRUE:
|
|
2115
|
+
return PYSAT_FALSE
|
|
2116
|
+
else:
|
|
2117
|
+
return Neg(subformula)
|
|
2118
|
+
|
|
2119
|
+
def _clausify(self, name_required=True):
|
|
2120
|
+
"""
|
|
2121
|
+
Negation clausification.
|
|
2122
|
+
|
|
2123
|
+
If ``name_required`` is ``False``, the method recursively encodes
|
|
2124
|
+
the subformula and populates self's clauses with a single unit clause
|
|
2125
|
+
containing the negation of the subformula's "name".
|
|
2126
|
+
|
|
2127
|
+
If ``name_required`` is set to ``True``, the method removes this
|
|
2128
|
+
single unit clause and instead gives name to the negation, which
|
|
2129
|
+
is equal to the negation of subformula's name.
|
|
2130
|
+
|
|
2131
|
+
:param name_required: whether or not a Tseitin variable is needed
|
|
2132
|
+
:param name_required: bool
|
|
2133
|
+
"""
|
|
2134
|
+
|
|
2135
|
+
if not self.clauses:
|
|
2136
|
+
# first, recursively encoding subformula
|
|
2137
|
+
self.subformula._clausify(name_required=True)
|
|
2138
|
+
self.clauses.append([-self.subformula.name])
|
|
2139
|
+
|
|
2140
|
+
# introducing a new name for this formula if required
|
|
2141
|
+
if name_required and not self.name:
|
|
2142
|
+
self.name = self.clauses[0][0]
|
|
2143
|
+
|
|
2144
|
+
Formula._vpool[Formula._context].obj2id[self] = self.name
|
|
2145
|
+
Formula._vpool[Formula._context].id2obj[self.name] = self
|
|
2146
|
+
|
|
2147
|
+
def _atoms(self, dest):
|
|
2148
|
+
"""
|
|
2149
|
+
Recursive atom collection.
|
|
2150
|
+
|
|
2151
|
+
:param dest: where the atoms are collected
|
|
2152
|
+
:type dest: set(:class:`Atom`)
|
|
2153
|
+
"""
|
|
2154
|
+
|
|
2155
|
+
self.subformula._atoms(dest)
|
|
2156
|
+
|
|
2157
|
+
def __repr__(self):
|
|
2158
|
+
"""
|
|
2159
|
+
State reproducible string representaion of object.
|
|
2160
|
+
"""
|
|
2161
|
+
|
|
2162
|
+
return f'{self.__class__.__name__}({repr(self.subformula)})'
|
|
2163
|
+
|
|
2164
|
+
def __str__(self):
|
|
2165
|
+
"""
|
|
2166
|
+
Informal representation of the ``Neg`` formula. Returns a string
|
|
2167
|
+
representation of the underlying subformula prefixed with
|
|
2168
|
+
``'~'``.
|
|
2169
|
+
"""
|
|
2170
|
+
|
|
2171
|
+
return f'~{str(self.subformula)}' if self.subformula.type == FormulaType.ATOM else f'~({str(self.subformula)})'
|
|
2172
|
+
|
|
2173
|
+
|
|
2174
|
+
#
|
|
2175
|
+
#==============================================================================
|
|
2176
|
+
class Implies(Formula):
|
|
2177
|
+
"""
|
|
2178
|
+
Implication. Given two operands :math:`f_1` and :math:`f_2`, it
|
|
2179
|
+
creates a formula :math:`f_1 \\rightarrow f_2`. The operands must be
|
|
2180
|
+
passed to the constructors either as two arguments or two keyword
|
|
2181
|
+
arguments ``left`` and ``right``.
|
|
2182
|
+
|
|
2183
|
+
Example:
|
|
2184
|
+
|
|
2185
|
+
.. code-block:: python
|
|
2186
|
+
|
|
2187
|
+
>>> from pysat.formula import *
|
|
2188
|
+
>>> x, y = Atom('x'), Atom('y')
|
|
2189
|
+
>>> a = Implies(x, y)
|
|
2190
|
+
>>> print(a)
|
|
2191
|
+
x >> y
|
|
2192
|
+
"""
|
|
2193
|
+
|
|
2194
|
+
def __new__(cls, *args, **kwargs):
|
|
2195
|
+
"""
|
|
2196
|
+
Implication constructor.
|
|
2197
|
+
"""
|
|
2198
|
+
|
|
2199
|
+
return Formula.__new__(cls, args, kwargs, type=FormulaType.IMPL)
|
|
2200
|
+
|
|
2201
|
+
def __init__(self, *args, **kwargs):
|
|
2202
|
+
"""
|
|
2203
|
+
Initialiser. Expects two arguments (either unnamed or keyword
|
|
2204
|
+
arguments) with the input formulas: left and right.
|
|
2205
|
+
"""
|
|
2206
|
+
|
|
2207
|
+
super(Implies, self).__init__(type=FormulaType.IMPL)
|
|
2208
|
+
|
|
2209
|
+
# initially, there are no operands
|
|
2210
|
+
self.left = self.right = None
|
|
2211
|
+
|
|
2212
|
+
pos = 0
|
|
2213
|
+
if 'left' in kwargs:
|
|
2214
|
+
self.left = kwargs['left']
|
|
2215
|
+
else:
|
|
2216
|
+
assert args, '\'left\' argument for Implies is not found'
|
|
2217
|
+
self.left = args[0]
|
|
2218
|
+
pos += 1
|
|
2219
|
+
|
|
2220
|
+
if 'right' in kwargs:
|
|
2221
|
+
self.right = kwargs['right']
|
|
2222
|
+
else:
|
|
2223
|
+
assert len(args) > pos, '\'right\' argument for Implies is not found'
|
|
2224
|
+
self.right = args[pos]
|
|
2225
|
+
|
|
2226
|
+
assert self.left and self.right, 'Implications accept two (left and right) operands'
|
|
2227
|
+
|
|
2228
|
+
def __del__(self):
|
|
2229
|
+
"""
|
|
2230
|
+
Implication destructor.
|
|
2231
|
+
"""
|
|
2232
|
+
|
|
2233
|
+
self.name = None
|
|
2234
|
+
self.clauses = []
|
|
2235
|
+
self.encoded = []
|
|
2236
|
+
self.left = self.right = None
|
|
2237
|
+
|
|
2238
|
+
def _iter(self, seen, outermost=False):
|
|
2239
|
+
"""
|
|
2240
|
+
Clause iterator. Recursively iterates through the clauses of
|
|
2241
|
+
``left`` and ``right`` subformulas followed by own clause
|
|
2242
|
+
traversal.
|
|
2243
|
+
"""
|
|
2244
|
+
|
|
2245
|
+
if not self in seen:
|
|
2246
|
+
seen[self] = True
|
|
2247
|
+
|
|
2248
|
+
for sub in [self.left, self.right]:
|
|
2249
|
+
for cl in sub._iter(seen):
|
|
2250
|
+
yield cl
|
|
2251
|
+
|
|
2252
|
+
if outermost:
|
|
2253
|
+
yield from self.clauses
|
|
2254
|
+
else:
|
|
2255
|
+
yield from self.encoded
|
|
2256
|
+
|
|
2257
|
+
def simplified(self, assumptions=[]):
|
|
2258
|
+
"""
|
|
2259
|
+
|
|
2260
|
+
Given a list of assumption literals, recursively simplifies the
|
|
2261
|
+
left and right subformulas and then creates and returns a new
|
|
2262
|
+
formula with these simplified subformulas.
|
|
2263
|
+
|
|
2264
|
+
:param assumptions: atomic assumptions
|
|
2265
|
+
:type assumptions: list(:class:`Formula`)
|
|
2266
|
+
|
|
2267
|
+
:rtype: :class:`Formula`
|
|
2268
|
+
|
|
2269
|
+
Example:
|
|
2270
|
+
|
|
2271
|
+
.. code-block:: python
|
|
2272
|
+
|
|
2273
|
+
>>> from pysat.formula import *
|
|
2274
|
+
>>> x, y, z = Atom('x'), Atom('y')
|
|
2275
|
+
>>> a = x >> y
|
|
2276
|
+
>>>
|
|
2277
|
+
>>> print(a.simplified(assumptions=[y]))
|
|
2278
|
+
T
|
|
2279
|
+
>>> print(a.simplified(assumptions=[~y]))
|
|
2280
|
+
~x
|
|
2281
|
+
"""
|
|
2282
|
+
|
|
2283
|
+
left = self.left.simplified (assumptions=set(assumptions))
|
|
2284
|
+
right = self.right.simplified(assumptions=set(assumptions))
|
|
2285
|
+
|
|
2286
|
+
if left is PYSAT_FALSE or right is PYSAT_TRUE or left == right:
|
|
2287
|
+
return PYSAT_TRUE
|
|
2288
|
+
elif left is PYSAT_TRUE:
|
|
2289
|
+
return right
|
|
2290
|
+
elif right is PYSAT_FALSE:
|
|
2291
|
+
return ~left
|
|
2292
|
+
elif self.left == ~right:
|
|
2293
|
+
return right
|
|
2294
|
+
|
|
2295
|
+
return Implies(left, right)
|
|
2296
|
+
|
|
2297
|
+
def _clausify(self, name_required=True):
|
|
2298
|
+
"""
|
|
2299
|
+
Implication clausification.
|
|
2300
|
+
|
|
2301
|
+
If ``name_required`` is ``False``, the method recursively encodes
|
|
2302
|
+
the left and right subformulas giving them names, say, :math:`x`
|
|
2303
|
+
and :math:`y` respectively and the populates self's clauses with a
|
|
2304
|
+
single binary clause :math:`(\\neg{x}\\lor y)`.
|
|
2305
|
+
|
|
2306
|
+
If ``name_required`` is set to ``True``, the method removes this
|
|
2307
|
+
single clause and instead gives a name to the implication by
|
|
2308
|
+
Tseitin-encoding it, i.e. :math:`n \\equiv (\\neg{x}\\lor y)`.
|
|
2309
|
+
|
|
2310
|
+
:param name_required: whether or not a Tseitin variable is needed
|
|
2311
|
+
:param name_required: bool
|
|
2312
|
+
"""
|
|
2313
|
+
|
|
2314
|
+
save_clauses = bool(self.clauses)
|
|
2315
|
+
|
|
2316
|
+
if not self.clauses:
|
|
2317
|
+
# first, recursively encoding subformula
|
|
2318
|
+
self.left._clausify(name_required=True)
|
|
2319
|
+
self.right._clausify(name_required=True)
|
|
2320
|
+
self.clauses.append([-self.left.name, self.right.name])
|
|
2321
|
+
|
|
2322
|
+
# introducing a new name for this formula if required
|
|
2323
|
+
if name_required and not self.name:
|
|
2324
|
+
if save_clauses:
|
|
2325
|
+
self.encoded = [clause.copy() for clause in self.clauses]
|
|
2326
|
+
else:
|
|
2327
|
+
self.encoded = self.clauses
|
|
2328
|
+
self.clauses = []
|
|
2329
|
+
|
|
2330
|
+
self.name = Formula._vpool[Formula._context].id(self)
|
|
2331
|
+
|
|
2332
|
+
# direct implication
|
|
2333
|
+
self.encoded[0].append(-self.name)
|
|
2334
|
+
|
|
2335
|
+
# clauses representing converse implication
|
|
2336
|
+
for i in range(len(self.encoded[0]) - 1):
|
|
2337
|
+
self.encoded.append([self.name, -self.encoded[0][i]])
|
|
2338
|
+
|
|
2339
|
+
def _atoms(self, dest):
|
|
2340
|
+
"""
|
|
2341
|
+
Recursive atom collection.
|
|
2342
|
+
|
|
2343
|
+
:param dest: where the atoms are collected
|
|
2344
|
+
:type dest: set(:class:`Atom`)
|
|
2345
|
+
"""
|
|
2346
|
+
|
|
2347
|
+
self.left._atoms(dest)
|
|
2348
|
+
self.right._atoms(dest)
|
|
2349
|
+
|
|
2350
|
+
def __repr__(self):
|
|
2351
|
+
"""
|
|
2352
|
+
State reproducible string representaion of object.
|
|
2353
|
+
"""
|
|
2354
|
+
|
|
2355
|
+
return f'{self.__class__.__name__}({repr(self.left)}, {repr(self.right)})'
|
|
2356
|
+
|
|
2357
|
+
def __str__(self):
|
|
2358
|
+
"""
|
|
2359
|
+
Informal representation of the ``Implies`` formula. Returns a
|
|
2360
|
+
string representation of the left and right subformulas with with
|
|
2361
|
+
a ``'>>'`` in the middle.
|
|
2362
|
+
"""
|
|
2363
|
+
|
|
2364
|
+
return '{0} >> {1}'.format(f'{str(self.left)}' if self.left.type in (FormulaType.ATOM, FormulaType.NEG) else f'({str(self.left)})',
|
|
2365
|
+
f'{str(self.right)}' if self.right.type in (FormulaType.ATOM, FormulaType.NEG) else f'({str(self.right)})')
|
|
2366
|
+
|
|
2367
|
+
|
|
2368
|
+
#
|
|
2369
|
+
#==============================================================================
|
|
2370
|
+
class Equals(Formula):
|
|
2371
|
+
"""
|
|
2372
|
+
Equivalence. Given a list of operands (subformulas) :math:`f_i`,
|
|
2373
|
+
:math:`i \\in \\{1,\\ldots,n\\}, n \\in \\mathbb{N}`, it creates a
|
|
2374
|
+
formula :math:`f_1 \\leftrightarrow f_2
|
|
2375
|
+
\\leftrightarrow\\ldots\\leftrightarrow f_n`. The list of operands *of
|
|
2376
|
+
size at least 2* should be passed as arguments to the constructor.
|
|
2377
|
+
|
|
2378
|
+
Example:
|
|
2379
|
+
|
|
2380
|
+
.. code-block:: python
|
|
2381
|
+
|
|
2382
|
+
>>> from pysat.formula import *
|
|
2383
|
+
>>> x, y, z = Atom('x'), Atom('y'), Atom('z')
|
|
2384
|
+
>>> equiv = Equals(x, y, z)
|
|
2385
|
+
|
|
2386
|
+
If an additional Boolean keyword argument ``merge`` is provided set to
|
|
2387
|
+
``True``, the toolkit will try to flatten the current :class:`Equals`
|
|
2388
|
+
formula merging its *equivalence* sub-operands into the list of
|
|
2389
|
+
operands. For example, if ``Equals(Equals(x, y), z, merge=True)`` is
|
|
2390
|
+
called, a new Formula object will be created with two operands:
|
|
2391
|
+
``Equals(x, y)`` and ``z``, followed by merging ``x`` and ``y`` into
|
|
2392
|
+
the list of root-level ``Equals``. This will result in a formula
|
|
2393
|
+
``Equals(x, y, z)``. Merging sub-operands is enabled by default if
|
|
2394
|
+
bitwise operations are used to create ``Equals`` formulas.
|
|
2395
|
+
|
|
2396
|
+
Example:
|
|
2397
|
+
|
|
2398
|
+
.. code-block:: python
|
|
2399
|
+
|
|
2400
|
+
>>> from pysat.formula import *
|
|
2401
|
+
>>> a1 = Equals(Equals(Atom('x'), Atom('y')), Atom('z'))
|
|
2402
|
+
>>> a2 = Equals(Equals(Atom('x'), Atom('y')), Atom('z'), merge=True)
|
|
2403
|
+
>>> a3 = Atom('x') == Atom('y') == Atom('z')
|
|
2404
|
+
>>>
|
|
2405
|
+
>>> print(a1)
|
|
2406
|
+
(x @ y) @ z
|
|
2407
|
+
>>> print(a2)
|
|
2408
|
+
x @ y @ z
|
|
2409
|
+
>>> print(a3)
|
|
2410
|
+
x @ y @ z
|
|
2411
|
+
>>>
|
|
2412
|
+
>>> id(a1) == id(a2)
|
|
2413
|
+
False
|
|
2414
|
+
>>>
|
|
2415
|
+
>>> id(a2) == id(a3)
|
|
2416
|
+
True # formulas a2 and a3 refer to the same object
|
|
2417
|
+
|
|
2418
|
+
.. note::
|
|
2419
|
+
|
|
2420
|
+
If there are two formulas representing the same fact with and
|
|
2421
|
+
without merging enabled, they technically sit in two distinct
|
|
2422
|
+
objects. Although PySAT tries to avoid it, clausification of these
|
|
2423
|
+
two formulas may result in unique (different) auxiliary variables
|
|
2424
|
+
assigned to such two formulas.
|
|
2425
|
+
"""
|
|
2426
|
+
|
|
2427
|
+
def __new__(cls, *args, **kwargs):
|
|
2428
|
+
"""
|
|
2429
|
+
Equivalence constructor.
|
|
2430
|
+
"""
|
|
2431
|
+
|
|
2432
|
+
return Formula.__new__(cls, args, kwargs, type=FormulaType.EQ)
|
|
2433
|
+
|
|
2434
|
+
def __init__(self, *args, **kwargs):
|
|
2435
|
+
"""
|
|
2436
|
+
Initialiser. Expects a list of arguments signifying the operands
|
|
2437
|
+
of the equivalence. Additionally, a user may set a keyword
|
|
2438
|
+
argument ``merge=True``, which will enable merging sub-operands.
|
|
2439
|
+
"""
|
|
2440
|
+
|
|
2441
|
+
super(Equals, self).__init__(type=FormulaType.EQ)
|
|
2442
|
+
|
|
2443
|
+
self.subformulas = list(args)
|
|
2444
|
+
|
|
2445
|
+
if 'merge' in kwargs and kwargs['merge'] == True:
|
|
2446
|
+
self._merge_suboperands()
|
|
2447
|
+
self.merged = True
|
|
2448
|
+
else:
|
|
2449
|
+
self.merged = False
|
|
2450
|
+
|
|
2451
|
+
if len(self.subformulas) < 2:
|
|
2452
|
+
raise FormulaError('Equivalence requires at least 2 arguments')
|
|
2453
|
+
|
|
2454
|
+
def __del__(self):
|
|
2455
|
+
"""
|
|
2456
|
+
Destructor.
|
|
2457
|
+
"""
|
|
2458
|
+
|
|
2459
|
+
self.name = None
|
|
2460
|
+
self.clauses = []
|
|
2461
|
+
self.encoded = []
|
|
2462
|
+
self.subformulas = []
|
|
2463
|
+
|
|
2464
|
+
def _iter(self, seen, outermost=False):
|
|
2465
|
+
"""
|
|
2466
|
+
Internal iterator over the clauses. First, iterates through the
|
|
2467
|
+
clauses of the subformulas followed by the formula's own clauses.
|
|
2468
|
+
"""
|
|
2469
|
+
|
|
2470
|
+
if not self in seen:
|
|
2471
|
+
seen[self] = True
|
|
2472
|
+
|
|
2473
|
+
for sub in self.subformulas:
|
|
2474
|
+
for cl in sub._iter(seen):
|
|
2475
|
+
yield cl
|
|
2476
|
+
|
|
2477
|
+
if outermost:
|
|
2478
|
+
yield from self.clauses
|
|
2479
|
+
else:
|
|
2480
|
+
yield from self.encoded
|
|
2481
|
+
|
|
2482
|
+
def simplified(self, assumptions=[]):
|
|
2483
|
+
"""
|
|
2484
|
+
Given a list of assumption literals, recursively simplifies the
|
|
2485
|
+
subformulas and creates a new formula.
|
|
2486
|
+
|
|
2487
|
+
:param assumptions: atomic assumptions
|
|
2488
|
+
:type assumptions: list(:class:`Formula`)
|
|
2489
|
+
|
|
2490
|
+
:rtype: :class:`Formula`
|
|
2491
|
+
|
|
2492
|
+
Example:
|
|
2493
|
+
|
|
2494
|
+
.. code-block:: python
|
|
2495
|
+
|
|
2496
|
+
>>> from pysat.formula import *
|
|
2497
|
+
>>> x, y, z = Atom('x'), Atom('y'), Atom('z')
|
|
2498
|
+
>>> a = x @ y @ z
|
|
2499
|
+
>>>
|
|
2500
|
+
>>> print(a.simplified(assumptions=[y]))
|
|
2501
|
+
x & z # x and z must also be True
|
|
2502
|
+
>>> print(a.simplified(assumptions=[~y]))
|
|
2503
|
+
~x & ~z # x and z must also be False
|
|
2504
|
+
"""
|
|
2505
|
+
|
|
2506
|
+
oset = set()
|
|
2507
|
+
operands = []
|
|
2508
|
+
t_present, f_present = False, False
|
|
2509
|
+
|
|
2510
|
+
for sub in self.subformulas:
|
|
2511
|
+
# execute simplification recursively
|
|
2512
|
+
sub = sub.simplified(assumptions=set(assumptions))
|
|
2513
|
+
|
|
2514
|
+
# record if the subterms degenerated to a True constant
|
|
2515
|
+
if sub is PYSAT_TRUE:
|
|
2516
|
+
if f_present:
|
|
2517
|
+
return PYSAT_FALSE
|
|
2518
|
+
t_present = True
|
|
2519
|
+
# same for False
|
|
2520
|
+
elif sub is PYSAT_FALSE:
|
|
2521
|
+
if t_present:
|
|
2522
|
+
return PYSAT_FALSE
|
|
2523
|
+
f_present = True
|
|
2524
|
+
# there is an opposite subformula too; equality is unsatisfiable
|
|
2525
|
+
elif ~sub in oset:
|
|
2526
|
+
return PYSAT_FALSE
|
|
2527
|
+
|
|
2528
|
+
# a new yet unsimplified term
|
|
2529
|
+
elif sub not in oset:
|
|
2530
|
+
oset.add(sub)
|
|
2531
|
+
operands.append(sub)
|
|
2532
|
+
|
|
2533
|
+
if not operands:
|
|
2534
|
+
return PYSAT_FALSE if f_present and t_present else PYSAT_TRUE
|
|
2535
|
+
|
|
2536
|
+
if len(operands) == 1:
|
|
2537
|
+
if not (t_present or f_present):
|
|
2538
|
+
# we got a single subformula but nothing is simplified above;
|
|
2539
|
+
# hence, the single subformula results from duplicate removal
|
|
2540
|
+
# i.e. we have a tautological equality
|
|
2541
|
+
return PYSAT_TRUE
|
|
2542
|
+
|
|
2543
|
+
return operands[0] if not f_present else Neg(operands[0])
|
|
2544
|
+
|
|
2545
|
+
if f_present or t_present:
|
|
2546
|
+
if f_present:
|
|
2547
|
+
operands = [Neg(sub) for sub in operands]
|
|
2548
|
+
return And(*operands, merge=True) if self.merged else And(*operands)
|
|
2549
|
+
|
|
2550
|
+
return Equals(*operands, merge=True) if self.merged else Equals(*operands)
|
|
2551
|
+
|
|
2552
|
+
def _clausify(self, name_required=True):
|
|
2553
|
+
"""
|
|
2554
|
+
Equivalence clausification.
|
|
2555
|
+
|
|
2556
|
+
If ``name_required`` is ``False``, the method recursively encodes
|
|
2557
|
+
the subformulas and populates self's clauses with binary clauses
|
|
2558
|
+
connecting two consecutive subformulas :math:`f_i` and
|
|
2559
|
+
:math:`f_{i+1}` by introducing two clauses :math:`(\\neg{f_i}\\lor
|
|
2560
|
+
f_{i+1})` and :math:`(f_i\\lor\\neg{f_i+1})`.
|
|
2561
|
+
|
|
2562
|
+
If ``name_required`` is set to ``True``, the method introduces an
|
|
2563
|
+
new auxiliary name for the equivalence term and clausifies it by
|
|
2564
|
+
relating it with the names of subformulas.
|
|
2565
|
+
|
|
2566
|
+
:param name_required: whether or not a Tseitin variable is needed
|
|
2567
|
+
:param name_required: bool
|
|
2568
|
+
"""
|
|
2569
|
+
|
|
2570
|
+
save_clauses = bool(self.clauses)
|
|
2571
|
+
|
|
2572
|
+
if not self.clauses:
|
|
2573
|
+
# first, recursively encoding subformulas
|
|
2574
|
+
for sub in self.subformulas:
|
|
2575
|
+
sub._clausify(name_required=True)
|
|
2576
|
+
|
|
2577
|
+
for i in range(len(self.subformulas) - 1):
|
|
2578
|
+
# current subformula is equivalent to the next one
|
|
2579
|
+
self.clauses.append([-self.subformulas[i].name, +self.subformulas[i + 1].name])
|
|
2580
|
+
self.clauses.append([+self.subformulas[i].name, -self.subformulas[i + 1].name])
|
|
2581
|
+
|
|
2582
|
+
# introducing a new name for this formula if required
|
|
2583
|
+
if name_required and not self.name:
|
|
2584
|
+
if save_clauses:
|
|
2585
|
+
self.encoded = [clause.copy() for clause in self.clauses]
|
|
2586
|
+
else:
|
|
2587
|
+
self.encoded = self.clauses
|
|
2588
|
+
self.clauses = []
|
|
2589
|
+
|
|
2590
|
+
self.name = Formula._vpool[Formula._context].id(self)
|
|
2591
|
+
|
|
2592
|
+
# direct implication (just adding the selector)
|
|
2593
|
+
for cl in self.encoded:
|
|
2594
|
+
cl.append(-self.name)
|
|
2595
|
+
|
|
2596
|
+
# clauses representing converse implication
|
|
2597
|
+
self.encoded.append([self.name] + [-s.name for s in self.subformulas])
|
|
2598
|
+
self.encoded.append([self.name] + [+s.name for s in self.subformulas])
|
|
2599
|
+
|
|
2600
|
+
def _atoms(self, dest):
|
|
2601
|
+
"""
|
|
2602
|
+
Recursive atom collection.
|
|
2603
|
+
|
|
2604
|
+
:param dest: where the atoms are collected
|
|
2605
|
+
:type dest: set(:class:`Atom`)
|
|
2606
|
+
"""
|
|
2607
|
+
|
|
2608
|
+
for sub in self.subformulas:
|
|
2609
|
+
sub._atoms(dest)
|
|
2610
|
+
|
|
2611
|
+
def __repr__(self):
|
|
2612
|
+
"""
|
|
2613
|
+
State reproducible string representaion of object.
|
|
2614
|
+
"""
|
|
2615
|
+
|
|
2616
|
+
return f'{self.__class__.__name__}{repr(self.subformulas)}'
|
|
2617
|
+
|
|
2618
|
+
def __str__(self):
|
|
2619
|
+
"""
|
|
2620
|
+
Informal representation of the ``Equals`` formula. Returns a
|
|
2621
|
+
string representation of the subformulas with with joined by
|
|
2622
|
+
``' @ '``.
|
|
2623
|
+
"""
|
|
2624
|
+
|
|
2625
|
+
return ' @ '.join([f'{str(sub)}' if sub.type in (FormulaType.ATOM, FormulaType.NEG) else f'({str(sub)})' for sub in self.subformulas])
|
|
2626
|
+
|
|
2627
|
+
|
|
2628
|
+
#
|
|
2629
|
+
#==============================================================================
|
|
2630
|
+
class XOr(Formula):
|
|
2631
|
+
"""
|
|
2632
|
+
Exclusive disjunction. Given a list of operands (subformulas)
|
|
2633
|
+
:math:`f_i`, :math:`i \\in \\{1,\\ldots,n\\}, n \\in \\mathbb{N}`, it
|
|
2634
|
+
creates a formula :math:`f_1 \\oplus f_2 \\oplus\\ldots\\oplus f_n`.
|
|
2635
|
+
The list of operands *of size at least 2* should be passed as
|
|
2636
|
+
arguments to the constructor.
|
|
2637
|
+
|
|
2638
|
+
Example:
|
|
2639
|
+
|
|
2640
|
+
.. code-block:: python
|
|
2641
|
+
|
|
2642
|
+
>>> from pysat.formula import *
|
|
2643
|
+
>>> x, y, z = Atom('x'), Atom('y'), Atom('z')
|
|
2644
|
+
>>> xor = XOr(x, y, z)
|
|
2645
|
+
|
|
2646
|
+
If an additional Boolean keyword argument ``merge`` is provided set to
|
|
2647
|
+
``True``, the toolkit will try to flatten the current :class:`XOr`
|
|
2648
|
+
formula merging its *equivalence* sub-operands into the list of
|
|
2649
|
+
operands. For example, if ``XOr(XOr(x, y), z, merge=True)`` is called,
|
|
2650
|
+
a new Formula object will be created with two operands: ``XOr(x, y)``
|
|
2651
|
+
and ``z``, followed by merging ``x`` and ``y`` into the list of
|
|
2652
|
+
root-level ``XOr``. This will result in a formula ``XOr(x, y, z)``.
|
|
2653
|
+
Merging sub-operands is disabled by default if bitwise operations are
|
|
2654
|
+
used to create ``XOr`` formulas.
|
|
2655
|
+
|
|
2656
|
+
Example:
|
|
2657
|
+
|
|
2658
|
+
.. code-block:: python
|
|
2659
|
+
|
|
2660
|
+
>>> from pysat.formula import *
|
|
2661
|
+
>>> a1 = XOr(XOr(Atom('x'), Atom('y')), Atom('z'))
|
|
2662
|
+
>>> a2 = XOr(XOr(Atom('x'), Atom('y')), Atom('z'), merge=True)
|
|
2663
|
+
>>> a3 = Atom('x') ^ Atom('y') ^ Atom('z')
|
|
2664
|
+
>>>
|
|
2665
|
+
>>> print(a1)
|
|
2666
|
+
(x ^ y) ^ z
|
|
2667
|
+
>>> print(a2)
|
|
2668
|
+
x ^ y ^ z
|
|
2669
|
+
>>> print(a3)
|
|
2670
|
+
(x ^ y) ^ z
|
|
2671
|
+
>>>
|
|
2672
|
+
>>> id(a1) == id(a2)
|
|
2673
|
+
False
|
|
2674
|
+
>>>
|
|
2675
|
+
>>> id(a1) == id(a3)
|
|
2676
|
+
True # formulas a1 and a3 refer to the same object
|
|
2677
|
+
|
|
2678
|
+
.. note::
|
|
2679
|
+
|
|
2680
|
+
If there are two formulas representing the same fact with and
|
|
2681
|
+
without merging enabled, they technically sit in two distinct
|
|
2682
|
+
objects. Although PySAT tries to avoid it, clausification of these
|
|
2683
|
+
two formulas may result in unique (different) auxiliary variables
|
|
2684
|
+
assigned to such two formulas.
|
|
2685
|
+
"""
|
|
2686
|
+
|
|
2687
|
+
def __new__(cls, *args, **kwargs):
|
|
2688
|
+
"""
|
|
2689
|
+
Equivalence constructor.
|
|
2690
|
+
"""
|
|
2691
|
+
|
|
2692
|
+
return Formula.__new__(cls, args, kwargs, type=FormulaType.XOR)
|
|
2693
|
+
|
|
2694
|
+
def __init__(self, *args, **kwargs):
|
|
2695
|
+
"""
|
|
2696
|
+
Initialiser. Expects a list of arguments signifying the operands
|
|
2697
|
+
of the exclusive disjunction. Additionally, a user may set a
|
|
2698
|
+
keyword argument ``merge=True``, which will enable merging
|
|
2699
|
+
sub-operands.
|
|
2700
|
+
"""
|
|
2701
|
+
|
|
2702
|
+
super(XOr, self).__init__(type=FormulaType.XOR)
|
|
2703
|
+
|
|
2704
|
+
self.subformulas = list(args)
|
|
2705
|
+
|
|
2706
|
+
if 'merge' in kwargs and kwargs['merge'] == True:
|
|
2707
|
+
self._merge_suboperands()
|
|
2708
|
+
self.merged = True
|
|
2709
|
+
else:
|
|
2710
|
+
self.merged = False
|
|
2711
|
+
|
|
2712
|
+
if len(self.subformulas) < 2:
|
|
2713
|
+
raise FormulaError('XOr requires at least 2 arguments')
|
|
2714
|
+
|
|
2715
|
+
def __del__(self):
|
|
2716
|
+
"""
|
|
2717
|
+
Destructor.
|
|
2718
|
+
"""
|
|
2719
|
+
|
|
2720
|
+
self.name = None
|
|
2721
|
+
self.clauses = []
|
|
2722
|
+
self.encoded = []
|
|
2723
|
+
self.subformulas = []
|
|
2724
|
+
|
|
2725
|
+
def _iter(self, seen, outermost=False):
|
|
2726
|
+
"""
|
|
2727
|
+
Internal iterator over the clauses. First, iterates through the
|
|
2728
|
+
clauses of the subformulas followed by the formula's own clauses.
|
|
2729
|
+
"""
|
|
2730
|
+
|
|
2731
|
+
if not self in seen:
|
|
2732
|
+
seen[self] = True
|
|
2733
|
+
|
|
2734
|
+
for sub in self.subformulas:
|
|
2735
|
+
for cl in sub._iter(seen):
|
|
2736
|
+
yield cl
|
|
2737
|
+
|
|
2738
|
+
if outermost:
|
|
2739
|
+
yield from self.clauses
|
|
2740
|
+
else:
|
|
2741
|
+
yield from self.encoded
|
|
2742
|
+
|
|
2743
|
+
def simplified(self, assumptions=[]):
|
|
2744
|
+
"""
|
|
2745
|
+
Given a list of assumption literals, recursively simplifies the
|
|
2746
|
+
subformulas and creates a new formula.
|
|
2747
|
+
|
|
2748
|
+
:param assumptions: atomic assumptions
|
|
2749
|
+
:type assumptions: list(:class:`Formula`)
|
|
2750
|
+
|
|
2751
|
+
:rtype: :class:`Formula`
|
|
2752
|
+
|
|
2753
|
+
Example:
|
|
2754
|
+
|
|
2755
|
+
.. code-block:: python
|
|
2756
|
+
|
|
2757
|
+
>>> from pysat.formula import *
|
|
2758
|
+
>>> x, y, z = Atom('x'), Atom('y'), Atom('z')
|
|
2759
|
+
>>> a = x ^ y ^ z
|
|
2760
|
+
>>>
|
|
2761
|
+
>>> print(a.simplified(assumptions=[y]))
|
|
2762
|
+
~x ^ z
|
|
2763
|
+
>>> print(a.simplified(assumptions=[~y]))
|
|
2764
|
+
x ^ z
|
|
2765
|
+
"""
|
|
2766
|
+
|
|
2767
|
+
oset = {}
|
|
2768
|
+
nof_trues = 0
|
|
2769
|
+
|
|
2770
|
+
for sub in self.subformulas:
|
|
2771
|
+
# execute simplification recursively
|
|
2772
|
+
sub = sub.simplified(assumptions=set(assumptions))
|
|
2773
|
+
|
|
2774
|
+
# record if the subterms degenerated to a True constant
|
|
2775
|
+
if sub is PYSAT_TRUE:
|
|
2776
|
+
nof_trues += 1
|
|
2777
|
+
|
|
2778
|
+
# we've got x ^ ~x, which results in a True constant
|
|
2779
|
+
elif ~sub in oset:
|
|
2780
|
+
nof_trues += 1
|
|
2781
|
+
oset.pop(~sub)
|
|
2782
|
+
|
|
2783
|
+
# this is a duplicate of an already seen sub-term
|
|
2784
|
+
# as a result, they cancel each other out
|
|
2785
|
+
elif sub in oset:
|
|
2786
|
+
oset.pop(sub)
|
|
2787
|
+
|
|
2788
|
+
# a new yet unsimplified term
|
|
2789
|
+
elif sub is not PYSAT_FALSE:
|
|
2790
|
+
oset[sub] = None
|
|
2791
|
+
|
|
2792
|
+
# getting the actual list of operands
|
|
2793
|
+
operands = list(oset)
|
|
2794
|
+
|
|
2795
|
+
if not operands:
|
|
2796
|
+
return PYSAT_TRUE if nof_trues % 2 else PYSAT_FALSE
|
|
2797
|
+
|
|
2798
|
+
if len(operands) == 1:
|
|
2799
|
+
return operands[0] if nof_trues % 2 == 0 else Neg(operands[0])
|
|
2800
|
+
|
|
2801
|
+
if nof_trues % 2 == 1:
|
|
2802
|
+
return Neg(XOr(*operands, merge=True)) if self.merged else Neg(XOr(*operands))
|
|
2803
|
+
|
|
2804
|
+
return XOr(*operands, merge=True) if self.merged else XOr(*operands)
|
|
2805
|
+
|
|
2806
|
+
def _clausify(self, name_required=True):
|
|
2807
|
+
"""
|
|
2808
|
+
XOr clausification. If the number of subformulas is more than 2,
|
|
2809
|
+
encodes the XOr sequentially by gradually introducing an auxiliary
|
|
2810
|
+
variable for each pair of operands.
|
|
2811
|
+
|
|
2812
|
+
:param name_required: whether or not a Tseitin variable is needed
|
|
2813
|
+
:param name_required: bool
|
|
2814
|
+
"""
|
|
2815
|
+
|
|
2816
|
+
save_clauses = bool(self.clauses)
|
|
2817
|
+
|
|
2818
|
+
if not self.clauses:
|
|
2819
|
+
# first, recursively encoding subformulas
|
|
2820
|
+
inputs = []
|
|
2821
|
+
for sub in self.subformulas:
|
|
2822
|
+
sub._clausify(name_required=True)
|
|
2823
|
+
inputs.append(sub.name)
|
|
2824
|
+
|
|
2825
|
+
if len(self.subformulas) > 2:
|
|
2826
|
+
# build a hierarchy of binary XOR constraints
|
|
2827
|
+
# until there are exactly two inputs left
|
|
2828
|
+
while len(inputs) > 2:
|
|
2829
|
+
n2 = inputs.pop()
|
|
2830
|
+
n1 = inputs.pop()
|
|
2831
|
+
|
|
2832
|
+
f1 = Formula._vpool[Formula._context].obj(n1)
|
|
2833
|
+
f2 = Formula._vpool[Formula._context].obj(n2)
|
|
2834
|
+
|
|
2835
|
+
# creating auxiliary XOr formulas and encoding them
|
|
2836
|
+
ao = XOr(f1, f2)
|
|
2837
|
+
ao._clausify(name_required=True)
|
|
2838
|
+
|
|
2839
|
+
# collecting all the corresponding clauses
|
|
2840
|
+
self.clauses += ao.encoded
|
|
2841
|
+
|
|
2842
|
+
inputs.append(ao.name)
|
|
2843
|
+
|
|
2844
|
+
assert len(inputs) == 2, 'Wrong number of operands for XOr when encoding'
|
|
2845
|
+
|
|
2846
|
+
# final XOr, without a name (for now)
|
|
2847
|
+
f1 = Formula._vpool[Formula._context].obj(inputs[0])
|
|
2848
|
+
f2 = Formula._vpool[Formula._context].obj(inputs[1])
|
|
2849
|
+
|
|
2850
|
+
final = XOr(f1, f2)
|
|
2851
|
+
final.clauses.append([-inputs[0], -inputs[1]])
|
|
2852
|
+
final.clauses.append([+inputs[0], +inputs[1]])
|
|
2853
|
+
|
|
2854
|
+
self.clauses += final.clauses
|
|
2855
|
+
else:
|
|
2856
|
+
self.clauses.append([-inputs[0], -inputs[1]])
|
|
2857
|
+
self.clauses.append([+inputs[0], +inputs[1]])
|
|
2858
|
+
|
|
2859
|
+
# introducing a new name for this formula if required
|
|
2860
|
+
if name_required and not self.name:
|
|
2861
|
+
if save_clauses:
|
|
2862
|
+
self.encoded = [clause.copy() for clause in self.clauses]
|
|
2863
|
+
else:
|
|
2864
|
+
self.encoded = self.clauses
|
|
2865
|
+
self.clauses = []
|
|
2866
|
+
|
|
2867
|
+
n1, n2 = self.encoded[-1]
|
|
2868
|
+
if len(self.subformulas) > 2:
|
|
2869
|
+
# reconstructing the final subterm
|
|
2870
|
+
f1 = Formula._vpool[Formula._context].obj(n1)
|
|
2871
|
+
f2 = Formula._vpool[Formula._context].obj(n2)
|
|
2872
|
+
final = XOr(f1, f2)
|
|
2873
|
+
|
|
2874
|
+
final.name = Formula._vpool[Formula._context].id(final)
|
|
2875
|
+
self.name = final.name
|
|
2876
|
+
else:
|
|
2877
|
+
self.name = Formula._vpool[Formula._context].id(self)
|
|
2878
|
+
final = None
|
|
2879
|
+
|
|
2880
|
+
# direct implication (just adding the selector)
|
|
2881
|
+
self.encoded[-2].append(-self.name)
|
|
2882
|
+
self.encoded[-1].append(-self.name)
|
|
247
2883
|
|
|
248
|
-
|
|
249
|
-
|
|
2884
|
+
# clauses representing converse implication
|
|
2885
|
+
self.encoded.append([self.name, -n1, +n2])
|
|
2886
|
+
self.encoded.append([self.name, +n1, -n2])
|
|
250
2887
|
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
2888
|
+
if final:
|
|
2889
|
+
# sharing converse implication with final subterm (if any)
|
|
2890
|
+
final.encoded.append(self.encoded[-2])
|
|
2891
|
+
final.encoded.append(self.encoded[-1])
|
|
254
2892
|
|
|
255
|
-
def
|
|
2893
|
+
def _atoms(self, dest):
|
|
256
2894
|
"""
|
|
257
|
-
|
|
2895
|
+
Recursive atom collection.
|
|
2896
|
+
|
|
2897
|
+
:param dest: where the atoms are collected
|
|
2898
|
+
:type dest: set(:class:`Atom`)
|
|
258
2899
|
"""
|
|
259
2900
|
|
|
260
|
-
self.
|
|
2901
|
+
for sub in self.subformulas:
|
|
2902
|
+
sub._atoms(dest)
|
|
261
2903
|
|
|
262
2904
|
def __repr__(self):
|
|
263
2905
|
"""
|
|
264
2906
|
State reproducible string representaion of object.
|
|
265
2907
|
"""
|
|
266
|
-
return f"IDPool(start_from={self.top+1}, occupied={self._occupied})"
|
|
267
2908
|
|
|
268
|
-
|
|
2909
|
+
return f'{self.__class__.__name__}{repr(self.subformulas)}'
|
|
2910
|
+
|
|
2911
|
+
def __str__(self):
|
|
269
2912
|
"""
|
|
270
|
-
|
|
271
|
-
|
|
2913
|
+
Informal representation of the ``XOr`` formula. Returns a
|
|
2914
|
+
string representation of the subformulas with with joined by
|
|
2915
|
+
``' ^ '``.
|
|
272
2916
|
"""
|
|
273
2917
|
|
|
274
|
-
|
|
275
|
-
self.top = start_from - 1
|
|
2918
|
+
return ' ^ '.join([f'{str(sub)}' if sub.type in (FormulaType.ATOM, FormulaType.NEG) else f'({str(sub)})' for sub in self.subformulas])
|
|
276
2919
|
|
|
277
|
-
# occupied IDs
|
|
278
|
-
self._occupied = sorted(occupied, key=lambda x: x[0])
|
|
279
2920
|
|
|
280
|
-
|
|
281
|
-
|
|
2921
|
+
#
|
|
2922
|
+
#==============================================================================
|
|
2923
|
+
class ITE(Formula):
|
|
2924
|
+
"""
|
|
2925
|
+
If-then-else operator. Given three operands (subformulas) :math:`x`,
|
|
2926
|
+
:math:`y`, and :math:`z`, it creates a formula :math:`(x \\rightarrow
|
|
2927
|
+
y) \\land (\\neg{x} \\rightarrow z)`. The operands should be passed as
|
|
2928
|
+
arguments to the constructor.
|
|
282
2929
|
|
|
283
|
-
|
|
284
|
-
# (if for whatever reason necessary)
|
|
285
|
-
self.id2obj = {}
|
|
2930
|
+
Example:
|
|
286
2931
|
|
|
287
|
-
|
|
2932
|
+
.. code-block:: python
|
|
2933
|
+
|
|
2934
|
+
>>> from pysat.formula import *
|
|
2935
|
+
>>> x, y, z = Atom('x'), Atom('y'), Atom('z')
|
|
2936
|
+
>>> ite = ITE(x, y, z)
|
|
2937
|
+
>>>
|
|
2938
|
+
>>> print(ite)
|
|
2939
|
+
>>> (x >> y) & (~x >> z)
|
|
2940
|
+
"""
|
|
2941
|
+
|
|
2942
|
+
def __new__(cls, *args, **kwargs):
|
|
2943
|
+
"""
|
|
2944
|
+
ITE constructor.
|
|
288
2945
|
"""
|
|
289
|
-
The method is to be used to assign an integer variable ID for a
|
|
290
|
-
given new object. If the object already has an ID, no new ID is
|
|
291
|
-
created and the old one is returned instead.
|
|
292
2946
|
|
|
293
|
-
|
|
294
|
-
string variable names. Note that if the object is not provided,
|
|
295
|
-
the method will return a new id unassigned to any object.
|
|
2947
|
+
return Formula.__new__(cls, args, kwargs, type=FormulaType.ITE)
|
|
296
2948
|
|
|
297
|
-
|
|
2949
|
+
def __init__(self, *args, **kwargs):
|
|
2950
|
+
"""
|
|
2951
|
+
Initialiser. Expects three arguments (either unnamed or keyword
|
|
2952
|
+
arguments) with the input formulas: ``cond`` (condition),
|
|
2953
|
+
``cons1`` (consequence1) and ``cons2`` (consequence2).
|
|
2954
|
+
"""
|
|
298
2955
|
|
|
299
|
-
|
|
2956
|
+
super(ITE, self).__init__(type=FormulaType.ITE)
|
|
300
2957
|
|
|
301
|
-
|
|
2958
|
+
# initially, there are no operands
|
|
2959
|
+
self.cond = self.cons1 = self.cons2 = None
|
|
302
2960
|
|
|
303
|
-
|
|
2961
|
+
pos = 0
|
|
2962
|
+
if 'cond' in kwargs:
|
|
2963
|
+
self.cond = kwargs['cond']
|
|
2964
|
+
else:
|
|
2965
|
+
assert args, '\'cond\' argument for ITE is not found'
|
|
2966
|
+
self.cond = args[0]
|
|
2967
|
+
pos += 1
|
|
304
2968
|
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
1
|
|
312
|
-
2
|
|
313
|
-
11
|
|
314
|
-
19
|
|
315
|
-
20
|
|
2969
|
+
if 'cons1' in kwargs:
|
|
2970
|
+
self.cons1 = kwargs['cons1']
|
|
2971
|
+
else:
|
|
2972
|
+
assert len(args) > pos, '\'cons1\' argument for ITE is not found'
|
|
2973
|
+
self.cons1 = args[pos]
|
|
2974
|
+
pos += 1
|
|
316
2975
|
|
|
317
|
-
|
|
318
|
-
|
|
2976
|
+
if 'cons2' in kwargs:
|
|
2977
|
+
self.cons2 = kwargs['cons2']
|
|
2978
|
+
else:
|
|
2979
|
+
assert len(args) > pos, '\'cons2\' argument for ITE is not found'
|
|
2980
|
+
self.cons2 = args[pos]
|
|
319
2981
|
|
|
320
|
-
|
|
2982
|
+
assert self.cond and self.cons1 and self.cons2, 'ITE formulas accept three (cond, cons1, and cons2) operands'
|
|
321
2983
|
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
20
|
|
326
|
-
>>> var('hello_world!')
|
|
327
|
-
21
|
|
2984
|
+
def __del__(self):
|
|
2985
|
+
"""
|
|
2986
|
+
Destructor.
|
|
328
2987
|
"""
|
|
329
2988
|
|
|
330
|
-
|
|
331
|
-
|
|
2989
|
+
self.name = None
|
|
2990
|
+
self.clauses = []
|
|
2991
|
+
self.encoded = []
|
|
2992
|
+
self.cond = self.cons1 = self.cons2 = None
|
|
332
2993
|
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
2994
|
+
def _iter(self, seen, outermost=False):
|
|
2995
|
+
"""
|
|
2996
|
+
Internal iterator over the clauses. First, iterates through the
|
|
2997
|
+
clauses of the subformulas followed by the formula's own clauses.
|
|
2998
|
+
"""
|
|
338
2999
|
|
|
339
|
-
|
|
3000
|
+
if not self in seen:
|
|
3001
|
+
seen[self] = True
|
|
340
3002
|
|
|
341
|
-
|
|
3003
|
+
for sub in [self.cond, self.cons1, self.cons2]:
|
|
3004
|
+
for cl in sub._iter(seen):
|
|
3005
|
+
yield cl
|
|
3006
|
+
|
|
3007
|
+
if outermost:
|
|
3008
|
+
yield from self.clauses
|
|
3009
|
+
else:
|
|
3010
|
+
yield from self.encoded
|
|
3011
|
+
|
|
3012
|
+
def simplified(self, assumptions=[]):
|
|
342
3013
|
"""
|
|
343
|
-
|
|
344
|
-
|
|
3014
|
+
Given a list of assumption literals, recursively simplifies the
|
|
3015
|
+
subformulas and creates a new formula.
|
|
345
3016
|
|
|
346
|
-
:param
|
|
347
|
-
:type
|
|
3017
|
+
:param assumptions: atomic assumptions
|
|
3018
|
+
:type assumptions: list(:class:`Formula`)
|
|
348
3019
|
|
|
349
|
-
:
|
|
3020
|
+
:rtype: :class:`Formula`
|
|
350
3021
|
|
|
351
3022
|
Example:
|
|
352
3023
|
|
|
353
3024
|
.. code-block:: python
|
|
354
3025
|
|
|
355
|
-
>>>
|
|
356
|
-
'
|
|
3026
|
+
>>> from pysat.formula import *
|
|
3027
|
+
>>> x, y, z = Atom('x'), Atom('y'), Atom('z')
|
|
3028
|
+
>>> ite = ITE(x, y, z)
|
|
3029
|
+
>>>
|
|
3030
|
+
>>> print(ite.simplified(assumptions=[y]))
|
|
3031
|
+
x | z
|
|
3032
|
+
>>> print(ite.simplified(assumptions=[~y]))
|
|
3033
|
+
~x & z
|
|
357
3034
|
"""
|
|
358
3035
|
|
|
359
|
-
|
|
360
|
-
|
|
3036
|
+
assumptions = set(assumptions)
|
|
3037
|
+
cond = self.cond .simplified(assumptions)
|
|
3038
|
+
cons1 = self.cons1.simplified(assumptions)
|
|
3039
|
+
cons2 = self.cons2.simplified(assumptions)
|
|
3040
|
+
|
|
3041
|
+
# a heavy list of conditions, each of which can simplify the expression
|
|
3042
|
+
if cond is PYSAT_TRUE:
|
|
3043
|
+
return cons1
|
|
3044
|
+
elif cond is PYSAT_FALSE:
|
|
3045
|
+
return cons2
|
|
3046
|
+
elif cons1 is PYSAT_TRUE or cons1 == cond:
|
|
3047
|
+
return Or(cond, cons2).simplified(assumptions)
|
|
3048
|
+
elif cons2 is PYSAT_TRUE or cons2 == Neg(cond) or cond == Neg(cons2):
|
|
3049
|
+
return Implies(cond, cons1).simplified(assumptions)
|
|
3050
|
+
elif cond == Neg(cons1) or Neg(cond) == cons1 or cond == cons2:
|
|
3051
|
+
return And(cons1, cons2).simplified(assumptions)
|
|
3052
|
+
elif cons1 is PYSAT_FALSE:
|
|
3053
|
+
return And(Neg(cond), cons2).simplified(assumptions)
|
|
3054
|
+
elif cons2 is PYSAT_FALSE:
|
|
3055
|
+
return And(cond, cons1).simplified(assumptions)
|
|
3056
|
+
elif cons1 == Neg(cons2) or Neg(cons1) == cons2:
|
|
3057
|
+
return Equals(cond, cons1).simplified(assumptions)
|
|
3058
|
+
|
|
3059
|
+
return ITE(cond, cons1, cons2)
|
|
3060
|
+
|
|
3061
|
+
def _clausify(self, name_required=True):
|
|
3062
|
+
"""
|
|
3063
|
+
ITE clausification.
|
|
361
3064
|
|
|
362
|
-
|
|
3065
|
+
If ``name_required`` is ``False``, the method recursively encodes
|
|
3066
|
+
the ``cond``, ``cons1``, and ``cons2`` subformulas giving them
|
|
3067
|
+
names, say, :math:`x`, :math:`y`, and :math:`z`, respectivela, and
|
|
3068
|
+
the populates self's clauses with two binary clauses
|
|
3069
|
+
:math:`(\\neg{x}\\lor y)` and :math:`(x \\lor y)`.
|
|
363
3070
|
|
|
364
|
-
|
|
3071
|
+
If ``name_required`` is set to ``True``, the method removes these
|
|
3072
|
+
clauses and instead gives a name to the ITE by Tseitin-encoding
|
|
3073
|
+
it, i.e. encoding :math:`n \\equiv \\left[(\\neg{x}\\lor
|
|
3074
|
+
y)\\land(x\\lor y)\\right]`.
|
|
3075
|
+
|
|
3076
|
+
:param name_required: whether or not a Tseitin variable is needed
|
|
3077
|
+
:param name_required: bool
|
|
365
3078
|
"""
|
|
366
|
-
Mark a given interval as occupied so that the manager could skip
|
|
367
|
-
the values from ``start`` to ``stop`` (**inclusive**).
|
|
368
3079
|
|
|
369
|
-
|
|
370
|
-
:param stop: end of the interval.
|
|
3080
|
+
save_clauses = bool(self.clauses)
|
|
371
3081
|
|
|
372
|
-
|
|
373
|
-
|
|
3082
|
+
if not self.clauses:
|
|
3083
|
+
# first, recursively encoding subformula
|
|
3084
|
+
self.cond._clausify(name_required=True)
|
|
3085
|
+
self.cons1._clausify(name_required=True)
|
|
3086
|
+
self.cons2._clausify(name_required=True)
|
|
3087
|
+
self.clauses.append([-self.cond.name, self.cons1.name])
|
|
3088
|
+
self.clauses.append([+self.cond.name, self.cons2.name])
|
|
3089
|
+
|
|
3090
|
+
# introducing a new name for this formula if required
|
|
3091
|
+
if name_required and not self.name:
|
|
3092
|
+
if save_clauses:
|
|
3093
|
+
self.encoded = [clause.copy() for clause in self.clauses]
|
|
3094
|
+
else:
|
|
3095
|
+
self.encoded = self.clauses
|
|
3096
|
+
self.clauses = []
|
|
3097
|
+
|
|
3098
|
+
self.name = Formula._vpool[Formula._context].id(self)
|
|
3099
|
+
|
|
3100
|
+
# direct implication
|
|
3101
|
+
self.encoded[0].append(-self.name)
|
|
3102
|
+
self.encoded[1].append(-self.name)
|
|
3103
|
+
|
|
3104
|
+
# converse implication
|
|
3105
|
+
self.encoded.append([self.name, -self.cond.name, -self.cons1.name])
|
|
3106
|
+
self.encoded.append([self.name, +self.cond.name, -self.cons2.name])
|
|
3107
|
+
self.encoded.append([self.name, -self.cons1.name, -self.cons2.name])
|
|
3108
|
+
|
|
3109
|
+
def _atoms(self, dest):
|
|
374
3110
|
"""
|
|
3111
|
+
Recursive atom collection.
|
|
375
3112
|
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
3113
|
+
:param dest: where the atoms are collected
|
|
3114
|
+
:type dest: set(:class:`Atom`)
|
|
3115
|
+
"""
|
|
379
3116
|
|
|
380
|
-
|
|
3117
|
+
self.cond._atoms(dest)
|
|
3118
|
+
self.cons1._atoms(dest)
|
|
3119
|
+
self.cons2._atoms(dest)
|
|
3120
|
+
|
|
3121
|
+
def __repr__(self):
|
|
381
3122
|
"""
|
|
382
|
-
|
|
3123
|
+
State reproducible string representaion of object.
|
|
383
3124
|
"""
|
|
384
3125
|
|
|
385
|
-
self.
|
|
3126
|
+
return f'{self.__class__.__name__}({repr(self.cond)}, {repr(self.cons1)}, {repr(self.cons2)})'
|
|
386
3127
|
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
3128
|
+
def __str__(self):
|
|
3129
|
+
"""
|
|
3130
|
+
Informal representation of the ``ITE`` formula. Returns a string
|
|
3131
|
+
representation of the subformulas as two implications connected
|
|
3132
|
+
with the conjuction symbol ``' & '``
|
|
3133
|
+
"""
|
|
390
3134
|
|
|
391
|
-
|
|
3135
|
+
cons1 = '{0} >> {1}'.format(f'{str(self.cond)}' if self.cond.type in (FormulaType.ATOM, FormulaType.NEG) else f'({str(self.cond)})',
|
|
3136
|
+
f'{str(self.cons1)}' if self.cons1.type in (FormulaType.ATOM, FormulaType.NEG) else f'({str(self.cons1)})')
|
|
392
3137
|
|
|
393
|
-
|
|
3138
|
+
cons2 = '{0} >> {1}'.format(f'~{str(self.cond)}' if self.cond.type in (FormulaType.ATOM, FormulaType.NEG) else f'~({str(self.cond)})',
|
|
3139
|
+
f'{str(self.cons2)}' if self.cons2.type in (FormulaType.ATOM, FormulaType.NEG) else f'({str(self.cons2)})')
|
|
3140
|
+
|
|
3141
|
+
return '({0}) & ({1})'.format(cons1, cons2)
|
|
394
3142
|
|
|
395
3143
|
|
|
396
3144
|
#
|
|
397
3145
|
#==============================================================================
|
|
398
|
-
class CNF(object):
|
|
3146
|
+
class CNF(Formula, object):
|
|
399
3147
|
"""
|
|
400
3148
|
Class for manipulating CNF formulas. It can be used for creating
|
|
401
3149
|
formulas, reading them from a file, or writing them to a file. The
|
|
@@ -409,6 +3157,7 @@ class CNF(object):
|
|
|
409
3157
|
:param from_clauses: a list of clauses to bootstrap the formula with
|
|
410
3158
|
:param from_aiger: an AIGER circuit to bootstrap the formula with
|
|
411
3159
|
:param comment_lead: a list of characters leading comment lines
|
|
3160
|
+
:param by_ref: flag to indicate how to copy clauses - by reference or deep-copy
|
|
412
3161
|
|
|
413
3162
|
:type from_file: str
|
|
414
3163
|
:type from_fp: file_pointer
|
|
@@ -416,18 +3165,24 @@ class CNF(object):
|
|
|
416
3165
|
:type from_clauses: list(list(int))
|
|
417
3166
|
:type from_aiger: :class:`aiger.AIG` (see `py-aiger package <https://github.com/mvcisback/py-aiger>`__)
|
|
418
3167
|
:type comment_lead: list(str)
|
|
3168
|
+
:type by_ref: bool
|
|
419
3169
|
"""
|
|
420
3170
|
|
|
421
3171
|
def __init__(self, from_file=None, from_fp=None, from_string=None,
|
|
422
|
-
from_clauses=[], from_aiger=None, comment_lead=['c']):
|
|
3172
|
+
from_clauses=[], from_aiger=None, comment_lead=['c'], by_ref=False):
|
|
423
3173
|
"""
|
|
424
3174
|
Constructor.
|
|
425
3175
|
"""
|
|
426
3176
|
|
|
427
3177
|
self.nv = 0
|
|
428
3178
|
self.clauses = []
|
|
3179
|
+
self.encoded = []
|
|
429
3180
|
self.comments = []
|
|
430
3181
|
|
|
3182
|
+
# this variable is required for integration with Formula
|
|
3183
|
+
self.name = None
|
|
3184
|
+
self.type = FormulaType.CNF
|
|
3185
|
+
|
|
431
3186
|
if from_file:
|
|
432
3187
|
self.from_file(from_file, comment_lead, compressed_with='use_ext')
|
|
433
3188
|
elif from_fp:
|
|
@@ -435,23 +3190,46 @@ class CNF(object):
|
|
|
435
3190
|
elif from_string:
|
|
436
3191
|
self.from_string(from_string, comment_lead)
|
|
437
3192
|
elif from_clauses:
|
|
438
|
-
self.from_clauses(from_clauses)
|
|
3193
|
+
self.from_clauses(from_clauses, by_ref)
|
|
439
3194
|
elif from_aiger:
|
|
440
3195
|
self.from_aiger(from_aiger)
|
|
441
3196
|
|
|
3197
|
+
def __new__(cls, *args, **kwargs):
|
|
3198
|
+
"""
|
|
3199
|
+
While :class:`CNF` inherits from :class:`Formula` (and so do its
|
|
3200
|
+
children), we don't want it to invoke the complex mechanisms of
|
|
3201
|
+
formula construction for CNF formulas. Instead, they should behave
|
|
3202
|
+
as in the previous versions of PySAT. Therefore, we call a simple
|
|
3203
|
+
object constructor here.
|
|
3204
|
+
"""
|
|
3205
|
+
|
|
3206
|
+
return object.__new__(cls)
|
|
3207
|
+
|
|
442
3208
|
def __repr__(self):
|
|
443
3209
|
"""
|
|
444
3210
|
State reproducible string representaion of object.
|
|
445
3211
|
"""
|
|
3212
|
+
|
|
446
3213
|
s = self.to_dimacs().replace('\n', '\\n')
|
|
447
|
-
return f
|
|
3214
|
+
return f'CNF(from_string=\'{s}\')'
|
|
3215
|
+
|
|
3216
|
+
def _compute_nv(self):
|
|
3217
|
+
"""
|
|
3218
|
+
Search and store the highest variable.
|
|
3219
|
+
"""
|
|
3220
|
+
|
|
3221
|
+
self.nv = max(map(abs, itertools.chain(*self.clauses)), default=self.nv)
|
|
3222
|
+
|
|
3223
|
+
# in case we use this CNF as a subformula in the future,
|
|
3224
|
+
# let's put the number of used variables in Formula's IDPool
|
|
3225
|
+
Formula._vpool[Formula._context].occupy(1, self.nv)
|
|
448
3226
|
|
|
449
3227
|
def from_file(self, fname, comment_lead=['c'], compressed_with='use_ext'):
|
|
450
3228
|
"""
|
|
451
3229
|
Read a CNF formula from a file in the DIMACS format. A file name is
|
|
452
3230
|
expected as an argument. A default argument is ``comment_lead`` for
|
|
453
3231
|
parsing comment lines. A given file can be compressed by either
|
|
454
|
-
gzip, bzip2, or
|
|
3232
|
+
gzip, bzip2, lzma, or zstd.
|
|
455
3233
|
|
|
456
3234
|
:param fname: name of a file to parse.
|
|
457
3235
|
:param comment_lead: a list of characters leading comment lines
|
|
@@ -462,11 +3240,12 @@ class CNF(object):
|
|
|
462
3240
|
:type compressed_with: str
|
|
463
3241
|
|
|
464
3242
|
Note that the ``compressed_with`` parameter can be ``None`` (i.e.
|
|
465
|
-
the file is uncompressed), ``'gzip'``, ``'bzip2'``, ``'lzma'``,
|
|
466
|
-
``'use_ext'``. The latter value indicates that
|
|
467
|
-
should be automatically determined based on the
|
|
468
|
-
Using ``'lzma'`` in Python 2 requires the
|
|
469
|
-
package to be additionally installed.
|
|
3243
|
+
the file is uncompressed), ``'gzip'``, ``'bzip2'``, ``'lzma'``,
|
|
3244
|
+
``'zstd'``, or ``'use_ext'``. The latter value indicates that
|
|
3245
|
+
compression type should be automatically determined based on the
|
|
3246
|
+
file extension. Using ``'lzma'`` in Python 2 requires the
|
|
3247
|
+
``backports.lzma`` package to be additionally installed. Using
|
|
3248
|
+
``'zstd'`` requires Python 3.14.
|
|
470
3249
|
|
|
471
3250
|
Usage example:
|
|
472
3251
|
|
|
@@ -520,7 +3299,7 @@ class CNF(object):
|
|
|
520
3299
|
elif not line.startswith('p cnf '):
|
|
521
3300
|
self.comments.append(line)
|
|
522
3301
|
|
|
523
|
-
self.
|
|
3302
|
+
self._compute_nv()
|
|
524
3303
|
|
|
525
3304
|
def from_string(self, string, comment_lead=['c']):
|
|
526
3305
|
"""
|
|
@@ -554,12 +3333,19 @@ class CNF(object):
|
|
|
554
3333
|
|
|
555
3334
|
self.from_fp(StringIO(string), comment_lead)
|
|
556
3335
|
|
|
557
|
-
def from_clauses(self, clauses):
|
|
3336
|
+
def from_clauses(self, clauses, by_ref=False):
|
|
558
3337
|
"""
|
|
559
|
-
This methods copies a list of clauses into a CNF object.
|
|
3338
|
+
This methods copies a list of clauses into a CNF object. The
|
|
3339
|
+
optional keyword argument ``by_ref``, which is by default set to
|
|
3340
|
+
``False``, signifies whether the clauses should be deep-copied or
|
|
3341
|
+
copied by reference (by default, deep-copying is applied although
|
|
3342
|
+
it is slower).
|
|
560
3343
|
|
|
561
3344
|
:param clauses: a list of clauses
|
|
3345
|
+
:param by_ref: a flag to indicate whether to deep-copy the clauses or copy them by reference
|
|
3346
|
+
|
|
562
3347
|
:type clauses: list(list(int))
|
|
3348
|
+
:type by_ref: bool
|
|
563
3349
|
|
|
564
3350
|
Example:
|
|
565
3351
|
|
|
@@ -573,10 +3359,9 @@ class CNF(object):
|
|
|
573
3359
|
5
|
|
574
3360
|
"""
|
|
575
3361
|
|
|
576
|
-
self.clauses = copy.deepcopy(clauses)
|
|
3362
|
+
self.clauses = clauses if by_ref else copy.deepcopy(clauses)
|
|
577
3363
|
|
|
578
|
-
|
|
579
|
-
self.nv = max([abs(l) for l in cl] + [self.nv])
|
|
3364
|
+
self._compute_nv()
|
|
580
3365
|
|
|
581
3366
|
def from_aiger(self, aig, vpool=None):
|
|
582
3367
|
"""
|
|
@@ -638,7 +3423,7 @@ class CNF(object):
|
|
|
638
3423
|
|
|
639
3424
|
self.clauses = [list(cls) for cls in aig_cnf.clauses]
|
|
640
3425
|
self.comments = ['c ' + c.strip() for c in aig_cnf.comments]
|
|
641
|
-
self.
|
|
3426
|
+
self._compute_nv()
|
|
642
3427
|
|
|
643
3428
|
# saving input and output variables
|
|
644
3429
|
self.inps = list(aig_cnf.input2lit.values())
|
|
@@ -667,32 +3452,36 @@ class CNF(object):
|
|
|
667
3452
|
cnf = CNF()
|
|
668
3453
|
cnf.nv = self.nv
|
|
669
3454
|
cnf.clauses = copy.deepcopy(self.clauses)
|
|
3455
|
+
cnf.encoded = copy.deepcopy(self.encoded)
|
|
670
3456
|
cnf.comments = copy.deepcopy(self.comments)
|
|
671
3457
|
|
|
672
3458
|
return cnf
|
|
673
3459
|
|
|
674
|
-
def to_file(self, fname, comments=None, compress_with='use_ext'):
|
|
3460
|
+
def to_file(self, fname, comments=None, as_dnf=False, compress_with='use_ext'):
|
|
675
3461
|
"""
|
|
676
3462
|
The method is for saving a CNF formula into a file in the DIMACS
|
|
677
3463
|
CNF format. A file name is expected as an argument. Additionally,
|
|
678
3464
|
supplementary comment lines can be specified in the ``comments``
|
|
679
3465
|
parameter. Also, a file can be compressed using either gzip, bzip2,
|
|
680
|
-
|
|
3466
|
+
lzma (xz), or zstd.
|
|
681
3467
|
|
|
682
3468
|
:param fname: a file name where to store the formula.
|
|
683
3469
|
:param comments: additional comments to put in the file.
|
|
684
|
-
:param
|
|
3470
|
+
:param as_dnf: a flag to use for specifying "dnf" in the preamble.
|
|
3471
|
+
:param compress_with: file compression algorithm.
|
|
685
3472
|
|
|
686
3473
|
:type fname: str
|
|
687
3474
|
:type comments: list(str)
|
|
3475
|
+
:type as_dnf: bool
|
|
688
3476
|
:type compress_with: str
|
|
689
3477
|
|
|
690
|
-
Note that the ``
|
|
691
|
-
the file is uncompressed), ``'gzip'``, ``'bzip2'``, ``'lzma'``,
|
|
692
|
-
``'use_ext'``. The latter value indicates that
|
|
693
|
-
should be automatically determined based on the
|
|
694
|
-
Using ``'lzma'`` in Python 2 requires the
|
|
695
|
-
package to be additionally installed.
|
|
3478
|
+
Note that the ``compressed_with`` parameter can be ``None`` (i.e.
|
|
3479
|
+
the file is uncompressed), ``'gzip'``, ``'bzip2'``, ``'lzma'``,
|
|
3480
|
+
``'zstd'``, or ``'use_ext'``. The latter value indicates that
|
|
3481
|
+
compression type should be automatically determined based on the
|
|
3482
|
+
file extension. Using ``'lzma'`` in Python 2 requires the
|
|
3483
|
+
``backports.lzma`` package to be additionally installed. Using
|
|
3484
|
+
``'zstd'`` requires Python 3.14.
|
|
696
3485
|
|
|
697
3486
|
Example:
|
|
698
3487
|
|
|
@@ -706,9 +3495,9 @@ class CNF(object):
|
|
|
706
3495
|
"""
|
|
707
3496
|
|
|
708
3497
|
with FileObject(fname, mode='w', compression=compress_with) as fobj:
|
|
709
|
-
self.to_fp(fobj.fp, comments)
|
|
3498
|
+
self.to_fp(fobj.fp, comments, as_dnf)
|
|
710
3499
|
|
|
711
|
-
def to_fp(self, file_pointer, comments=None):
|
|
3500
|
+
def to_fp(self, file_pointer, comments=None, as_dnf=False):
|
|
712
3501
|
"""
|
|
713
3502
|
The method can be used to save a CNF formula into a file pointer.
|
|
714
3503
|
The file pointer is expected as an argument. Additionally,
|
|
@@ -717,9 +3506,11 @@ class CNF(object):
|
|
|
717
3506
|
|
|
718
3507
|
:param file_pointer: a file pointer where to store the formula.
|
|
719
3508
|
:param comments: additional comments to put in the file.
|
|
3509
|
+
:param as_dnf: a flag to use for specifying "dnf" in the preamble.
|
|
720
3510
|
|
|
721
3511
|
:type file_pointer: file pointer
|
|
722
3512
|
:type comments: list(str)
|
|
3513
|
+
:type as_dnf: bool
|
|
723
3514
|
|
|
724
3515
|
Example:
|
|
725
3516
|
|
|
@@ -742,7 +3533,7 @@ class CNF(object):
|
|
|
742
3533
|
for c in comments:
|
|
743
3534
|
print(c, file=file_pointer)
|
|
744
3535
|
|
|
745
|
-
print('p cnf', self.nv, len(self.clauses), file=file_pointer)
|
|
3536
|
+
print('p cnf' if not as_dnf else 'p dnf', self.nv, len(self.clauses), file=file_pointer)
|
|
746
3537
|
|
|
747
3538
|
for cl in self.clauses:
|
|
748
3539
|
print(' '.join(str(l) for l in cl), '0', file=file_pointer)
|
|
@@ -773,12 +3564,13 @@ class CNF(object):
|
|
|
773
3564
|
-1 2 0
|
|
774
3565
|
-2 3 0
|
|
775
3566
|
-3 0
|
|
776
|
-
|
|
777
3567
|
"""
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
3568
|
+
|
|
3569
|
+
header_lines = [f'p cnf {self.nv} {len(self.clauses)}']
|
|
3570
|
+
comment_lines = [f'{comment}' for comment in self.comments]
|
|
3571
|
+
clause_lines = [' '.join(map(str,clause)) + ' 0' for clause in self.clauses]
|
|
3572
|
+
|
|
3573
|
+
lines = '\n'.join(comment_lines + header_lines + clause_lines)
|
|
782
3574
|
return lines
|
|
783
3575
|
|
|
784
3576
|
def to_alien(self, file_pointer, format='opb', comments=None):
|
|
@@ -867,14 +3659,32 @@ class CNF(object):
|
|
|
867
3659
|
print('(check-sat)', file=file_pointer)
|
|
868
3660
|
print('(exit)', file=file_pointer)
|
|
869
3661
|
|
|
870
|
-
def append(self, clause):
|
|
3662
|
+
def append(self, clause, update_vpool=False):
|
|
871
3663
|
"""
|
|
872
3664
|
Add one more clause to CNF formula. This method additionally
|
|
873
3665
|
updates the number of variables, i.e. variable ``self.nv``, used
|
|
874
3666
|
in the formula.
|
|
875
3667
|
|
|
876
|
-
|
|
3668
|
+
The additional keyword argument ``update_vpool`` can be set to
|
|
3669
|
+
``True`` if the user wants to update for default static pool of
|
|
3670
|
+
variable identifiers stored in class :class:`Formula`. In light of
|
|
3671
|
+
the fact that a user may encode their problem manually and add
|
|
3672
|
+
thousands to millions of clauses using this method, the value of
|
|
3673
|
+
``update_vpool`` is set to ``False`` by default.
|
|
3674
|
+
|
|
3675
|
+
.. note::
|
|
3676
|
+
|
|
3677
|
+
Setting ``update_vpool=True`` is required if a user wants to
|
|
3678
|
+
combine this :class:`CNF` formula with other (clausal or
|
|
3679
|
+
non-clausal) formulas followed by the clausification of the
|
|
3680
|
+
result combination. Alternatively, a user may resort to using
|
|
3681
|
+
the method :meth:`extend` instead.
|
|
3682
|
+
|
|
3683
|
+
:param clause: a new clause to add
|
|
3684
|
+
:param update_vpool: update or not the static vpool
|
|
3685
|
+
|
|
877
3686
|
:type clause: list(int)
|
|
3687
|
+
:type update_vpool: bool
|
|
878
3688
|
|
|
879
3689
|
.. code-block:: python
|
|
880
3690
|
|
|
@@ -888,13 +3698,16 @@ class CNF(object):
|
|
|
888
3698
|
self.nv = max([abs(l) for l in clause] + [self.nv])
|
|
889
3699
|
self.clauses.append(list(clause))
|
|
890
3700
|
|
|
3701
|
+
if update_vpool:
|
|
3702
|
+
Formula._vpool[Formula._context].occupy(1, self.nv)
|
|
3703
|
+
|
|
891
3704
|
def extend(self, clauses):
|
|
892
3705
|
"""
|
|
893
3706
|
Add several clauses to CNF formula. The clauses should be given in
|
|
894
3707
|
the form of list. For every clause in the list, method
|
|
895
3708
|
:meth:`append` is invoked.
|
|
896
3709
|
|
|
897
|
-
:param clauses: a list of new clauses to add
|
|
3710
|
+
:param clauses: a list of new clauses to add
|
|
898
3711
|
:type clauses: list(list(int))
|
|
899
3712
|
|
|
900
3713
|
Example:
|
|
@@ -911,6 +3724,9 @@ class CNF(object):
|
|
|
911
3724
|
for cl in clauses:
|
|
912
3725
|
self.append(cl)
|
|
913
3726
|
|
|
3727
|
+
# updating the default vpool here, once all the clauses are appended
|
|
3728
|
+
Formula._vpool[Formula._context].occupy(1, self.nv)
|
|
3729
|
+
|
|
914
3730
|
def __iter__(self):
|
|
915
3731
|
"""
|
|
916
3732
|
Iterator over all clauses of the formula.
|
|
@@ -957,13 +3773,14 @@ class CNF(object):
|
|
|
957
3773
|
|
|
958
3774
|
def negate(self, topv=None):
|
|
959
3775
|
"""
|
|
960
|
-
Given a CNF formula :math
|
|
961
|
-
formula :math:`\\neg{
|
|
962
|
-
encoded to CNF with the use of *auxiliary* Tseitin
|
|
963
|
-
A new CNF formula is returned keeping all the
|
|
964
|
-
variables that can be accessed through the
|
|
965
|
-
All the literals used to encode the negation
|
|
966
|
-
clauses can be accessed through the ``enclits``
|
|
3776
|
+
Given a CNF formula :math:`\\mathcal{F}`, this method creates a
|
|
3777
|
+
CNF formula :math:`\\neg{\\mathcal{F}}`. The negation of the
|
|
3778
|
+
formula is encoded to CNF with the use of *auxiliary* Tseitin
|
|
3779
|
+
variables [1]_. A new CNF formula is returned keeping all the
|
|
3780
|
+
newly introduced variables that can be accessed through the
|
|
3781
|
+
``auxvars`` variable. All the literals used to encode the negation
|
|
3782
|
+
of the original clauses can be accessed through the ``enclits``
|
|
3783
|
+
variable.
|
|
967
3784
|
|
|
968
3785
|
**Note** that the negation of each clause is encoded with one
|
|
969
3786
|
auxiliary variable if it is not unit size. Otherwise, no auxiliary
|
|
@@ -1023,6 +3840,98 @@ class CNF(object):
|
|
|
1023
3840
|
negated.clauses.append(negated.enclits)
|
|
1024
3841
|
return negated
|
|
1025
3842
|
|
|
3843
|
+
def _clausify(self, name_required=False):
|
|
3844
|
+
"""
|
|
3845
|
+
As a means of seamless integration of :class:`CNF` and
|
|
3846
|
+
:class:`Formula` objects, this method Tseitin-encodes a CNF
|
|
3847
|
+
formula, which results in a new Boolean variable "naming" it.
|
|
3848
|
+
|
|
3849
|
+
If ``name_required`` is ``False``, nothing is done as the formula
|
|
3850
|
+
is already clausal.
|
|
3851
|
+
"""
|
|
3852
|
+
|
|
3853
|
+
# no clausification is required as we have a CNF already
|
|
3854
|
+
# but a name may be required; hence, the following Tseitin-encoding
|
|
3855
|
+
|
|
3856
|
+
if name_required and not self.name:
|
|
3857
|
+
# we do not update top variable id, allowing a user to reuse variables
|
|
3858
|
+
clauses, auxvars, enclits = [], [], []
|
|
3859
|
+
|
|
3860
|
+
# encoding the clauses
|
|
3861
|
+
for cl in self.clauses:
|
|
3862
|
+
auxv = cl[0]
|
|
3863
|
+
if len(cl) > 1:
|
|
3864
|
+
auxv = Formula._vpool[Formula._context].id()
|
|
3865
|
+
|
|
3866
|
+
# direct implication
|
|
3867
|
+
for l in cl:
|
|
3868
|
+
clauses.append([-l, auxv])
|
|
3869
|
+
|
|
3870
|
+
# opposite implication
|
|
3871
|
+
clauses.append(cl + [-auxv])
|
|
3872
|
+
|
|
3873
|
+
# keeping all Tseitin variables
|
|
3874
|
+
auxvars.append(auxv)
|
|
3875
|
+
|
|
3876
|
+
# literals representing the clauses
|
|
3877
|
+
enclits.append(auxv)
|
|
3878
|
+
|
|
3879
|
+
# encoding the conjunction
|
|
3880
|
+
if len(enclits) > 1:
|
|
3881
|
+
self.name = Formula._vpool[Formula._context].id(self)
|
|
3882
|
+
|
|
3883
|
+
for lit in enclits:
|
|
3884
|
+
clauses.append([-self.name, lit])
|
|
3885
|
+
clauses.append([self.name] + [-lit for lit in enclits])
|
|
3886
|
+
else: # single clause - nothing left to encode
|
|
3887
|
+
self.name = enclits[0] # existing variable
|
|
3888
|
+
|
|
3889
|
+
# connecting it to the CNF object as its name
|
|
3890
|
+
Formula._vpool[Formula._context].obj2id[self] = self.name
|
|
3891
|
+
Formula._vpool[Formula._context].id2obj[self.name] = self
|
|
3892
|
+
|
|
3893
|
+
# just in case, marking all ids below self.name as occupied
|
|
3894
|
+
Formula._vpool[Formula._context].occupy(1, self.name)
|
|
3895
|
+
|
|
3896
|
+
self.encoded = clauses
|
|
3897
|
+
self.auxvars = auxvars
|
|
3898
|
+
self.enclits = enclits
|
|
3899
|
+
self.nv = self.name
|
|
3900
|
+
|
|
3901
|
+
def _atoms(self, dest):
|
|
3902
|
+
"""
|
|
3903
|
+
The base case of recursive atom collection. Extends the collection
|
|
3904
|
+
with all the variables in the CNF formula.
|
|
3905
|
+
|
|
3906
|
+
:param dest: the set of atoms to collect
|
|
3907
|
+
:type dest: set(:class:`Atom`)
|
|
3908
|
+
"""
|
|
3909
|
+
|
|
3910
|
+
dest |= set(range(1, self.nv + 1))
|
|
3911
|
+
|
|
3912
|
+
def _iter(self, seen, outermost=False):
|
|
3913
|
+
"""
|
|
3914
|
+
This is a copy of :meth:`__iter__`, to be consistent with
|
|
3915
|
+
:class:`Formula`.
|
|
3916
|
+
"""
|
|
3917
|
+
|
|
3918
|
+
if not self in seen:
|
|
3919
|
+
seen[self] = True
|
|
3920
|
+
|
|
3921
|
+
if outermost:
|
|
3922
|
+
yield from self.clauses
|
|
3923
|
+
else:
|
|
3924
|
+
yield from self.encoded
|
|
3925
|
+
|
|
3926
|
+
def simplified(self, assumptions=[]):
|
|
3927
|
+
"""
|
|
3928
|
+
As any other Formula type, CNF formulas have this method, although
|
|
3929
|
+
intentionally left unimplemented. Raises a ``FormulaError``
|
|
3930
|
+
exception.
|
|
3931
|
+
"""
|
|
3932
|
+
|
|
3933
|
+
raise FormulaError('Cannot simplify a CNF formula')
|
|
3934
|
+
|
|
1026
3935
|
|
|
1027
3936
|
#
|
|
1028
3937
|
#==============================================================================
|
|
@@ -1069,15 +3978,16 @@ class WCNF(object):
|
|
|
1069
3978
|
"""
|
|
1070
3979
|
State reproducible string representaion of object.
|
|
1071
3980
|
"""
|
|
3981
|
+
|
|
1072
3982
|
s = self.to_dimacs().replace('\n', '\\n')
|
|
1073
|
-
return f
|
|
3983
|
+
return f'WCNF(from_string=\'{s}\')'
|
|
1074
3984
|
|
|
1075
3985
|
def from_file(self, fname, comment_lead=['c'], compressed_with='use_ext'):
|
|
1076
3986
|
"""
|
|
1077
3987
|
Read a WCNF formula from a file in the DIMACS format. A file name
|
|
1078
3988
|
is expected as an argument. A default argument is ``comment_lead``
|
|
1079
3989
|
for parsing comment lines. A given file can be compressed by either
|
|
1080
|
-
gzip, bzip2, or
|
|
3990
|
+
gzip, bzip2, lzma, or zstd.
|
|
1081
3991
|
|
|
1082
3992
|
:param fname: name of a file to parse.
|
|
1083
3993
|
:param comment_lead: a list of characters leading comment lines
|
|
@@ -1088,11 +3998,12 @@ class WCNF(object):
|
|
|
1088
3998
|
:type compressed_with: str
|
|
1089
3999
|
|
|
1090
4000
|
Note that the ``compressed_with`` parameter can be ``None`` (i.e.
|
|
1091
|
-
the file is uncompressed), ``'gzip'``, ``'bzip2'``, ``'lzma'``,
|
|
1092
|
-
``'use_ext'``. The latter value indicates that
|
|
1093
|
-
should be automatically determined based on the
|
|
1094
|
-
Using ``'lzma'`` in Python 2 requires the
|
|
1095
|
-
package to be additionally installed.
|
|
4001
|
+
the file is uncompressed), ``'gzip'``, ``'bzip2'``, ``'lzma'``,
|
|
4002
|
+
``'zstd'``, or ``'use_ext'``. The latter value indicates that
|
|
4003
|
+
compression type should be automatically determined based on the
|
|
4004
|
+
file extension. Using ``'lzma'`` in Python 2 requires the
|
|
4005
|
+
``backports.lzma`` package to be additionally installed. Using
|
|
4006
|
+
``'zstd'`` requires Python 3.14.
|
|
1096
4007
|
|
|
1097
4008
|
Usage example:
|
|
1098
4009
|
|
|
@@ -1293,7 +4204,7 @@ class WCNF(object):
|
|
|
1293
4204
|
CNF format. A file name is expected as an argument. Additionally,
|
|
1294
4205
|
supplementary comment lines can be specified in the ``comments``
|
|
1295
4206
|
parameter. Also, a file can be compressed using either gzip, bzip2,
|
|
1296
|
-
|
|
4207
|
+
lzma (xz), or zstd.
|
|
1297
4208
|
|
|
1298
4209
|
:param fname: a file name where to store the formula.
|
|
1299
4210
|
:param comments: additional comments to put in the file.
|
|
@@ -1303,12 +4214,13 @@ class WCNF(object):
|
|
|
1303
4214
|
:type comments: list(str)
|
|
1304
4215
|
:type compress_with: str
|
|
1305
4216
|
|
|
1306
|
-
Note that the ``
|
|
1307
|
-
the file is uncompressed), ``'gzip'``, ``'bzip2'``, ``'lzma'``,
|
|
1308
|
-
``'use_ext'``. The latter value indicates that
|
|
1309
|
-
should be automatically determined based on the
|
|
1310
|
-
Using ``'lzma'`` in Python 2 requires the
|
|
1311
|
-
package to be additionally installed.
|
|
4217
|
+
Note that the ``compressed_with`` parameter can be ``None`` (i.e.
|
|
4218
|
+
the file is uncompressed), ``'gzip'``, ``'bzip2'``, ``'lzma'``,
|
|
4219
|
+
``'zstd'``, or ``'use_ext'``. The latter value indicates that
|
|
4220
|
+
compression type should be automatically determined based on the
|
|
4221
|
+
file extension. Using ``'lzma'`` in Python 2 requires the
|
|
4222
|
+
``backports.lzma`` package to be additionally installed. Using
|
|
4223
|
+
``'zstd'`` requires Python 3.14.
|
|
1312
4224
|
|
|
1313
4225
|
Example:
|
|
1314
4226
|
|
|
@@ -1394,13 +4306,14 @@ class WCNF(object):
|
|
|
1394
4306
|
10 1 2 0
|
|
1395
4307
|
1 -1 0
|
|
1396
4308
|
2 -2 0
|
|
1397
|
-
|
|
1398
4309
|
"""
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
4310
|
+
|
|
4311
|
+
header_lines = [f'p wcnf {self.nv} {len(self.hard) + len(self.soft)} {self.topw}']
|
|
4312
|
+
comment_lines = [f'{comment}' for comment in self.comments]
|
|
4313
|
+
hard_lines = [f'{self.topw} ' + ' '.join(map(str, clause)) + ' 0' for clause in self.hard]
|
|
4314
|
+
soft_lines = [f'{weight} ' + ' '.join(map(str, clause)) + ' 0' for clause, weight in zip(self.soft, self.wght)]
|
|
4315
|
+
|
|
4316
|
+
lines = '\n'.join(comment_lines + header_lines + hard_lines + soft_lines)
|
|
1404
4317
|
return lines
|
|
1405
4318
|
|
|
1406
4319
|
def to_alien(self, file_pointer, format='opb', comments=None):
|
|
@@ -1620,7 +4533,7 @@ class WCNF(object):
|
|
|
1620
4533
|
|
|
1621
4534
|
cnf.nv = self.nv
|
|
1622
4535
|
cnf.clauses = copy.deepcopy(self.hard) + copy.deepcopy(self.soft)
|
|
1623
|
-
cnf.
|
|
4536
|
+
cnf.comments = self.comments[:]
|
|
1624
4537
|
|
|
1625
4538
|
return cnf
|
|
1626
4539
|
|
|
@@ -1636,8 +4549,8 @@ class CNFPlus(CNF, object):
|
|
|
1636
4549
|
supports *native* cardinality constraints of `MiniCard
|
|
1637
4550
|
<https://github.com/liffiton/minicard>`__.
|
|
1638
4551
|
|
|
1639
|
-
The parser of input DIMACS files of :class:`CNFPlus` assumes the
|
|
1640
|
-
of AtMostK and AtLeastK constraints defined in the `description
|
|
4552
|
+
The parser of input DIMACS files of :class:`CNFPlus` assumes the
|
|
4553
|
+
syntax of AtMostK and AtLeastK constraints defined in the `description
|
|
1641
4554
|
<https://github.com/liffiton/minicard>`__ of MiniCard:
|
|
1642
4555
|
|
|
1643
4556
|
::
|
|
@@ -1648,12 +4561,26 @@ class CNFPlus(CNF, object):
|
|
|
1648
4561
|
4 5 6 -7 >= 2
|
|
1649
4562
|
3 5 7 0
|
|
1650
4563
|
|
|
4564
|
+
Additionally, :class:`CNFPlus` support pseudo-Boolean constraints,
|
|
4565
|
+
i.e. weighted linear constraints by extending the above format.
|
|
4566
|
+
Basically, a pseudo-Boolean constraint needs to specify all the
|
|
4567
|
+
summands as ``weight*literal`` with the entire constraint being
|
|
4568
|
+
prepended with character ``w`` as follows:
|
|
4569
|
+
|
|
4570
|
+
::
|
|
4571
|
+
|
|
4572
|
+
c Example: One cardinality constraint and one PB constraint followed by a clause
|
|
4573
|
+
p cnf+ 7 3
|
|
4574
|
+
1 -2 3 5 -7 <= 3
|
|
4575
|
+
w 1*4 2*5 1*6 3*-7 >= 2
|
|
4576
|
+
3 5 7 0
|
|
4577
|
+
|
|
1651
4578
|
Each AtLeastK constraint is translated into an AtMostK constraint in
|
|
1652
|
-
the standard way: :math
|
|
1653
|
-
|
|
1654
|
-
are stored in variable ``atmosts``, each being a pair
|
|
1655
|
-
where ``lits`` is a list of literals in the sum and
|
|
1656
|
-
bound.
|
|
4579
|
+
the standard way: :math:`\\sum_{i=1}^{n}{x_i}\\geq k \\leftrightarrow
|
|
4580
|
+
\\sum_{i=1}^{n}{\\neg{x_i}}\\leq (n-k)`. Internally, AtMostK
|
|
4581
|
+
constraints are stored in variable ``atmosts``, each being a pair
|
|
4582
|
+
``(lits, k)``, where ``lits`` is a list of literals in the sum and
|
|
4583
|
+
``k`` is the upper bound.
|
|
1657
4584
|
|
|
1658
4585
|
Example:
|
|
1659
4586
|
|
|
@@ -1688,8 +4615,9 @@ class CNFPlus(CNF, object):
|
|
|
1688
4615
|
"""
|
|
1689
4616
|
State reproducible string representaion of object.
|
|
1690
4617
|
"""
|
|
4618
|
+
|
|
1691
4619
|
s = self.to_dimacs().replace('\n', '\\n')
|
|
1692
|
-
return f
|
|
4620
|
+
return f'CNFPlus(from_string=\'{s}\')'
|
|
1693
4621
|
|
|
1694
4622
|
def from_fp(self, file_pointer, comment_lead=['c']):
|
|
1695
4623
|
"""
|
|
@@ -1729,16 +4657,24 @@ class CNFPlus(CNF, object):
|
|
|
1729
4657
|
if line.endswith(' 0'): # normal case
|
|
1730
4658
|
self.clauses.append(list(map(int, line.split()[:-1])))
|
|
1731
4659
|
else: # atmost/atleast constraint
|
|
1732
|
-
items =
|
|
1733
|
-
|
|
4660
|
+
items = line.split()
|
|
4661
|
+
|
|
4662
|
+
if items[0] == 'w': # literals are weighted here
|
|
4663
|
+
wght, lits = list(map(list, zip(*[map(int, pair.split('*')) for pair in items[1:-2]])))
|
|
4664
|
+
sumw = sum(wght)
|
|
4665
|
+
else:
|
|
4666
|
+
lits = [int(l) for l in items[:-2]]
|
|
4667
|
+
sumw = len(lits)
|
|
4668
|
+
|
|
1734
4669
|
rhs = int(items[-1])
|
|
1735
4670
|
self.nv = max([abs(l) for l in lits] + [self.nv])
|
|
1736
4671
|
|
|
1737
4672
|
if items[-2][0] == '>':
|
|
1738
4673
|
lits = list(map(lambda l: -l, lits))
|
|
1739
|
-
rhs =
|
|
4674
|
+
rhs = sumw - rhs
|
|
4675
|
+
|
|
4676
|
+
self.atmosts.append([lits, rhs, wght] if items[0] == 'w' else [lits, rhs])
|
|
1740
4677
|
|
|
1741
|
-
self.atmosts.append([lits, rhs])
|
|
1742
4678
|
elif not line.startswith('p cnf'): # cnf is allowed here
|
|
1743
4679
|
self.comments.append(line)
|
|
1744
4680
|
|
|
@@ -1786,7 +4722,11 @@ class CNFPlus(CNF, object):
|
|
|
1786
4722
|
print(' '.join(str(l) for l in cl), '0', file=file_pointer)
|
|
1787
4723
|
|
|
1788
4724
|
for am in self.atmosts:
|
|
1789
|
-
|
|
4725
|
+
if len(am) == 2: # cardinality constraint
|
|
4726
|
+
print(' '.join(str(l) for l in am[0]), '<=', am[1], file=file_pointer)
|
|
4727
|
+
else: # len(am) == 3 => PB constraint
|
|
4728
|
+
assert len(am[0]) == len(am[2]), 'Number of literals should be equal to the number of weights'
|
|
4729
|
+
print('w', ' '.join('{0}*{1}'.format(str(w), str(l)) for w, l in zip(am[2], am[0])), '<=', am[1], file=file_pointer)
|
|
1790
4730
|
|
|
1791
4731
|
def to_dimacs(self):
|
|
1792
4732
|
"""
|
|
@@ -1814,13 +4754,21 @@ class CNFPlus(CNF, object):
|
|
|
1814
4754
|
3 5 7 0
|
|
1815
4755
|
1 -2 3 5 -7 <= 3
|
|
1816
4756
|
-4 -5 -6 7 <= 2
|
|
1817
|
-
|
|
1818
4757
|
"""
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
4758
|
+
|
|
4759
|
+
header_lines = [f'p cnf+ {self.nv} {len(self.clauses) + len(self.atmosts)}']
|
|
4760
|
+
comment_lines = [f'{comment}' for comment in self.comments]
|
|
4761
|
+
clause_lines = [' '.join(map(str, clause)) + ' 0' for clause in self.clauses]
|
|
4762
|
+
|
|
4763
|
+
atmost_lines = []
|
|
4764
|
+
for am in self.atmosts:
|
|
4765
|
+
if len(am) == 2: # cardinality constraint
|
|
4766
|
+
atmost_lines.append(' '.join(str(l) for l in am[0]) + ' <= ' + str(am[1]))
|
|
4767
|
+
else: # len(am) == 3 => PB constraint
|
|
4768
|
+
assert len(am[0]) == len(am[2]), 'Number of literals should be equal to the number of weights'
|
|
4769
|
+
atmost_lines.append('w ' + ' '.join('{0}*{1}'.format(str(w), str(l)) for w, l in zip(am[2], am[0])) + ' <= ' + str(am[1]))
|
|
4770
|
+
|
|
4771
|
+
lines = '\n'.join(comment_lines + header_lines + clause_lines + atmost_lines)
|
|
1824
4772
|
return lines
|
|
1825
4773
|
|
|
1826
4774
|
def to_alien(self, file_pointer, format='opb', comments=None):
|
|
@@ -1910,16 +4858,25 @@ class CNFPlus(CNF, object):
|
|
|
1910
4858
|
|
|
1911
4859
|
for i, am in enumerate(self.atmosts, len(self.clauses) + 1):
|
|
1912
4860
|
line, neg = [], 0
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
4861
|
+
|
|
4862
|
+
if len(am) == 2:
|
|
4863
|
+
for l in am[0]:
|
|
4864
|
+
if l > 0:
|
|
4865
|
+
line.append('-{0} x{1}'.format('1' if format == 'opb' else '', l))
|
|
4866
|
+
neg += 1
|
|
4867
|
+
else:
|
|
4868
|
+
line.append('+{0} x{1}'.format('1' if format == 'opb' else '', -l))
|
|
4869
|
+
else:
|
|
4870
|
+
for w, l in zip(am[2], am[0]):
|
|
4871
|
+
if l > 0:
|
|
4872
|
+
line.append('-{0} x{1}'.format(w, l))
|
|
4873
|
+
neg += w
|
|
4874
|
+
else:
|
|
4875
|
+
line.append('+{0} x{1}'.format(w, -l))
|
|
1919
4876
|
|
|
1920
4877
|
print('{0} {1} >= {2} {3}'.format('' if format == 'opb' else 'c{0}:'.format(i),
|
|
1921
4878
|
' '.join(l for l in line),
|
|
1922
|
-
len(am[0]) - am[1] - neg, ';' if format == 'opb' else ''),
|
|
4879
|
+
(len(am[0]) if len(am) == 2 else sum(am[2])) - am[1] - neg, ';' if format == 'opb' else ''),
|
|
1923
4880
|
file=file_pointer)
|
|
1924
4881
|
|
|
1925
4882
|
if format == 'lp':
|
|
@@ -2092,6 +5049,15 @@ class CNFPlus(CNF, object):
|
|
|
2092
5049
|
|
|
2093
5050
|
return cnfplus
|
|
2094
5051
|
|
|
5052
|
+
def _clausify(self, name_required=False):
|
|
5053
|
+
"""
|
|
5054
|
+
This method currently only raises an error as there is no support
|
|
5055
|
+
of ``atmosts`` in :class:`Formula`. This may potentially be fixed
|
|
5056
|
+
in the future.
|
|
5057
|
+
"""
|
|
5058
|
+
|
|
5059
|
+
raise FormulaError('Integration of CNFPlus and Formula is not yet supported')
|
|
5060
|
+
|
|
2095
5061
|
|
|
2096
5062
|
#
|
|
2097
5063
|
#==============================================================================
|
|
@@ -2117,14 +5083,29 @@ class WCNFPlus(WCNF, object):
|
|
|
2117
5083
|
10 4 5 6 -7 >= 2
|
|
2118
5084
|
5 3 5 7 0
|
|
2119
5085
|
|
|
5086
|
+
Additionally, :class:`WCNFPlus` support pseudo-Boolean constraints,
|
|
5087
|
+
i.e. weighted linear constraints by extending the above format.
|
|
5088
|
+
Basically, a pseudo-Boolean constraint needs to specify all the
|
|
5089
|
+
summands as ``weight*literal`` with the entire constraint being
|
|
5090
|
+
prepended with character ``w`` as follows:
|
|
5091
|
+
|
|
5092
|
+
::
|
|
5093
|
+
|
|
5094
|
+
c Example: One cardinality constraint and one PB constraint followed by a soft clause
|
|
5095
|
+
p wcnf+ 7 3 10
|
|
5096
|
+
10 1 -2 3 5 -7 <= 3
|
|
5097
|
+
10 w 1*4 2*5 1*6 3*-7 >= 2
|
|
5098
|
+
5 3 5 7 0
|
|
5099
|
+
|
|
2120
5100
|
**Note** that every cardinality constraint is assumed to be *hard*,
|
|
2121
5101
|
i.e. soft cardinality constraints are currently *not supported*.
|
|
2122
5102
|
|
|
2123
5103
|
Each AtLeastK constraint is translated into an AtMostK constraint in
|
|
2124
|
-
the standard way: :math
|
|
2125
|
-
|
|
2126
|
-
are stored in variable ``atms``, each being a pair
|
|
2127
|
-
``lits`` is a list of literals in the sum and
|
|
5104
|
+
the standard way: :math:`\\sum_{i=1}^{n}{x_i}\\geq k \\leftrightarrow
|
|
5105
|
+
\\sum_{i=1}^{n}{\\neg{x_i}}\\leq (n-k)`. Internally, AtMostK
|
|
5106
|
+
constraints are stored in variable ``atms``, each being a pair
|
|
5107
|
+
``(lits, k)``, where ``lits`` is a list of literals in the sum and
|
|
5108
|
+
``k`` is the upper bound.
|
|
2128
5109
|
|
|
2129
5110
|
Example:
|
|
2130
5111
|
|
|
@@ -2162,8 +5143,9 @@ class WCNFPlus(WCNF, object):
|
|
|
2162
5143
|
"""
|
|
2163
5144
|
State reproducible string representaion of object.
|
|
2164
5145
|
"""
|
|
5146
|
+
|
|
2165
5147
|
s = self.to_dimacs().replace('\n', '\\n')
|
|
2166
|
-
return f
|
|
5148
|
+
return f'WCNFPlus(from_string=\'{s}\')'
|
|
2167
5149
|
|
|
2168
5150
|
def from_fp(self, file_pointer, comment_lead=['c']):
|
|
2169
5151
|
"""
|
|
@@ -2224,16 +5206,24 @@ class WCNFPlus(WCNF, object):
|
|
|
2224
5206
|
# it will be processed later
|
|
2225
5207
|
negs.append(tuple([list(map(int, items.split()[:-1])), -w]))
|
|
2226
5208
|
else: # atmost/atleast constraint
|
|
2227
|
-
items =
|
|
2228
|
-
|
|
5209
|
+
items = line.split()
|
|
5210
|
+
|
|
5211
|
+
if items[1] == 'w': # literals are weighted here
|
|
5212
|
+
wght, lits = list(map(list, zip(*[map(int, pair.split('*')) for pair in items[2:-2]])))
|
|
5213
|
+
sumw = sum(wght)
|
|
5214
|
+
else:
|
|
5215
|
+
lits = [int(l) for l in items[1:-2]]
|
|
5216
|
+
sumw = len(lits)
|
|
5217
|
+
|
|
2229
5218
|
rhs = int(items[-1])
|
|
2230
5219
|
self.nv = max([abs(l) for l in lits] + [self.nv])
|
|
2231
5220
|
|
|
2232
5221
|
if items[-2][0] == '>':
|
|
2233
5222
|
lits = list(map(lambda l: -l, lits))
|
|
2234
|
-
rhs =
|
|
5223
|
+
rhs = sumw - rhs
|
|
5224
|
+
|
|
5225
|
+
self.atms.append([lits, rhs, wght] if items[1] == 'w' else [lits, rhs])
|
|
2235
5226
|
|
|
2236
|
-
self.atms.append([lits, rhs])
|
|
2237
5227
|
elif not line.startswith('p wcnf'): # wcnf is allowed here
|
|
2238
5228
|
self.comments.append(line)
|
|
2239
5229
|
else: # expecting the preamble
|
|
@@ -2304,7 +5294,11 @@ class WCNFPlus(WCNF, object):
|
|
|
2304
5294
|
|
|
2305
5295
|
# atmost constraints are hard
|
|
2306
5296
|
for am in self.atms:
|
|
2307
|
-
|
|
5297
|
+
if len(am) == 2: # cardinality constraint
|
|
5298
|
+
print(self.topw, ' '.join(str(l) for l in am[0]), '<=', am[1], file=file_pointer)
|
|
5299
|
+
else: # len(am) == 3 => PB constraint
|
|
5300
|
+
assert len(am[0]) == len(am[2]), 'Number of literals should be equal to the number of weights'
|
|
5301
|
+
print(self.topw, 'w', ' '.join('{0}*{1}'.format(str(w), str(l)) for w, l in zip(am[2], am[0])), '<=', am[1], file=file_pointer)
|
|
2308
5302
|
|
|
2309
5303
|
def to_dimacs(self):
|
|
2310
5304
|
"""
|
|
@@ -2333,14 +5327,23 @@ class WCNFPlus(WCNF, object):
|
|
|
2333
5327
|
5 3 5 7 0
|
|
2334
5328
|
10 1 -2 3 5 -7 <= 3
|
|
2335
5329
|
10 -4 -5 -6 7 <= 2
|
|
2336
|
-
|
|
2337
5330
|
"""
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
|
|
5331
|
+
|
|
5332
|
+
header_lines = [f'p wcnf+ {self.nv} {len(self.hard) + len(self.soft) + len(self.atms)} {self.topw}']
|
|
5333
|
+
comment_lines = [f'{comment}' for comment in self.comments]
|
|
5334
|
+
hard_lines = [f'{self.topw} ' + ' '.join(map(str,clause)) + ' 0' for clause in self.hard]
|
|
5335
|
+
soft_lines = [f'{weight} ' + ' '.join(map(str,clause)) + ' 0' for clause, weight in zip(self.soft, self.wght)]
|
|
5336
|
+
|
|
5337
|
+
atmost_lines = []
|
|
5338
|
+
for am in self.atms:
|
|
5339
|
+
if len(am) == 2: # cardinality constraint
|
|
5340
|
+
atmost_lines.append(f'{self.topw} ' + ' '.join(str(l) for l in am[0]) + ' <= ' + str(am[1]))
|
|
5341
|
+
else: # len(am) == 3 => PB constraint
|
|
5342
|
+
assert len(am[0]) == len(am[2]), 'Number of literals should be equal to the number of weights'
|
|
5343
|
+
atmost_lines.append(f'{self.topw} ' + 'w ' + ' '.join('{0}*{1}'.format(str(w), str(l)) for w, l in zip(am[2], am[0])) + ' <= ' + str(am[1]))
|
|
5344
|
+
|
|
5345
|
+
lines = '\n'.join(comment_lines + header_lines + hard_lines + soft_lines + atmost_lines) + '\n'
|
|
5346
|
+
|
|
2344
5347
|
return lines
|
|
2345
5348
|
|
|
2346
5349
|
def to_alien(self, file_pointer, format='opb', comments=None):
|
|
@@ -2446,16 +5449,25 @@ class WCNFPlus(WCNF, object):
|
|
|
2446
5449
|
|
|
2447
5450
|
for i, am in enumerate(self.atms, len(self.hard) + len(hard) + 1):
|
|
2448
5451
|
line, neg = [], 0
|
|
2449
|
-
|
|
2450
|
-
|
|
2451
|
-
|
|
2452
|
-
|
|
2453
|
-
|
|
2454
|
-
|
|
5452
|
+
|
|
5453
|
+
if len(am) == 2:
|
|
5454
|
+
for l in am[0]:
|
|
5455
|
+
if l > 0:
|
|
5456
|
+
line.append('-{0} x{1}'.format('1' if format == 'opb' else '', l))
|
|
5457
|
+
neg += 1
|
|
5458
|
+
else:
|
|
5459
|
+
line.append('+{0} x{1}'.format('1' if format == 'opb' else '', -l))
|
|
5460
|
+
else:
|
|
5461
|
+
for w, l in zip(am[2], am[0]):
|
|
5462
|
+
if l > 0:
|
|
5463
|
+
line.append('-{0} x{1}'.format(w, l))
|
|
5464
|
+
neg += w
|
|
5465
|
+
else:
|
|
5466
|
+
line.append('+{0} x{1}'.format(w, -l))
|
|
2455
5467
|
|
|
2456
5468
|
print('{0} {1} >= {2} {3}'.format('' if format == 'opb' else 'c{0}:'.format(i),
|
|
2457
5469
|
' '.join(l for l in line),
|
|
2458
|
-
len(am[0]) - am[1] - neg, ';' if format == 'opb' else ''),
|
|
5470
|
+
(len(am[0]) if len(am) == 2 else sum(am[2])) - am[1] - neg, ';' if format == 'opb' else ''),
|
|
2459
5471
|
file=file_pointer)
|
|
2460
5472
|
|
|
2461
5473
|
if format == 'lp':
|
|
@@ -2565,7 +5577,7 @@ class WCNFPlus(WCNF, object):
|
|
|
2565
5577
|
cnf.nv = self.nv
|
|
2566
5578
|
cnf.clauses = copy.deepcopy(self.hard) + copy.deepcopy(self.soft)
|
|
2567
5579
|
cnf.atmosts = copy.deepcopy(self.atms)
|
|
2568
|
-
cnf.
|
|
5580
|
+
cnf.comments = self.comments[:]
|
|
2569
5581
|
|
|
2570
5582
|
return cnf
|
|
2571
5583
|
|