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.

Files changed (45) hide show
  1. pycard.cp310-win_amd64.pyd +0 -0
  2. pysat/__init__.py +4 -4
  3. pysat/_fileio.py +30 -14
  4. pysat/allies/approxmc.py +22 -22
  5. pysat/allies/unigen.py +435 -0
  6. pysat/card.py +13 -12
  7. pysat/engines.py +1302 -0
  8. pysat/examples/bbscan.py +663 -0
  9. pysat/examples/bica.py +691 -0
  10. pysat/examples/fm.py +12 -8
  11. pysat/examples/genhard.py +24 -23
  12. pysat/examples/hitman.py +53 -37
  13. pysat/examples/lbx.py +56 -15
  14. pysat/examples/lsu.py +28 -14
  15. pysat/examples/mcsls.py +53 -15
  16. pysat/examples/models.py +6 -4
  17. pysat/examples/musx.py +15 -7
  18. pysat/examples/optux.py +71 -32
  19. pysat/examples/primer.py +620 -0
  20. pysat/examples/rc2.py +268 -69
  21. pysat/formula.py +3241 -229
  22. pysat/pb.py +85 -37
  23. pysat/process.py +16 -2
  24. pysat/solvers.py +2119 -724
  25. pysolvers.cp310-win_amd64.pyd +0 -0
  26. {python_sat-0.1.8.dev10.data → python_sat-1.8.dev26.data}/scripts/approxmc.py +22 -22
  27. python_sat-1.8.dev26.data/scripts/bbscan.py +663 -0
  28. python_sat-1.8.dev26.data/scripts/bica.py +691 -0
  29. {python_sat-0.1.8.dev10.data → python_sat-1.8.dev26.data}/scripts/fm.py +12 -8
  30. {python_sat-0.1.8.dev10.data → python_sat-1.8.dev26.data}/scripts/genhard.py +24 -23
  31. {python_sat-0.1.8.dev10.data → python_sat-1.8.dev26.data}/scripts/lbx.py +56 -15
  32. {python_sat-0.1.8.dev10.data → python_sat-1.8.dev26.data}/scripts/lsu.py +28 -14
  33. {python_sat-0.1.8.dev10.data → python_sat-1.8.dev26.data}/scripts/mcsls.py +53 -15
  34. {python_sat-0.1.8.dev10.data → python_sat-1.8.dev26.data}/scripts/models.py +6 -4
  35. {python_sat-0.1.8.dev10.data → python_sat-1.8.dev26.data}/scripts/musx.py +15 -7
  36. {python_sat-0.1.8.dev10.data → python_sat-1.8.dev26.data}/scripts/optux.py +71 -32
  37. python_sat-1.8.dev26.data/scripts/primer.py +620 -0
  38. {python_sat-0.1.8.dev10.data → python_sat-1.8.dev26.data}/scripts/rc2.py +268 -69
  39. python_sat-1.8.dev26.data/scripts/unigen.py +435 -0
  40. {python_sat-0.1.8.dev10.dist-info → python_sat-1.8.dev26.dist-info}/METADATA +19 -5
  41. python_sat-1.8.dev26.dist-info/RECORD +48 -0
  42. {python_sat-0.1.8.dev10.dist-info → python_sat-1.8.dev26.dist-info}/WHEEL +1 -1
  43. python_sat-0.1.8.dev10.dist-info/RECORD +0 -39
  44. {python_sat-0.1.8.dev10.dist-info → python_sat-1.8.dev26.dist-info/licenses}/LICENSE.txt +0 -0
  45. {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. Although only
31
- clausal formulas are supported at this point, future releases of PySAT are
32
- expected to implement data structures and methods to manipulate arbitrary
33
- Boolean formulas. The module implements the :class:`CNF` class, which
34
- represents a formula in `conjunctive normal form (CNF)
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:`\mathcal{F}=\mathcal{H}\wedge\mathcal{S}`. Soft clauses of a WCNF
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:`\mathcal{S}` is a pair :math:`(c_i, w_i)`. In partial (unweighted)
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:`\mathcal{F}=\mathcal{H}\wedge\mathcal{S}`
127
- targets satisfying all its hard clauses :math:`\mathcal{H}` and maximizing
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:`\sum_{c_i\in\mathcal{S}}{w_i\\cdot c_i}`.
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+\sum_{c_i\in\mathcal{S}}{w_i}`. Top weight of a
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 provides
170
- the extended classes :class:`CNFPlus` and :class:`WCNFPlus`. The only
171
- difference between ``?CNF`` and ``?CNFPlus`` is the support for *native*
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
- :param start_from: the smallest ID to assign.
249
- :param occupied: a list of occupied intervals.
2884
+ # clauses representing converse implication
2885
+ self.encoded.append([self.name, -n1, +n2])
2886
+ self.encoded.append([self.name, +n1, -n2])
250
2887
 
251
- :type start_from: int
252
- :type occupied: list(list(int))
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 __init__(self, start_from=1, occupied=[]):
2893
+ def _atoms(self, dest):
256
2894
  """
257
- Constructor.
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.restart(start_from=start_from, occupied=occupied)
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
- def restart(self, start_from=1, occupied=[]):
2909
+ return f'{self.__class__.__name__}{repr(self.subformulas)}'
2910
+
2911
+ def __str__(self):
269
2912
  """
270
- Restart the manager from scratch. The arguments replicate those of
271
- the constructor of :class:`IDPool`.
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
- # initial ID
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
- # main dictionary storing the mapping from objects to variable IDs
281
- self.obj2id = collections.defaultdict(lambda: self._next())
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
- # mapping back from variable IDs to objects
284
- # (if for whatever reason necessary)
285
- self.id2obj = {}
2930
+ Example:
286
2931
 
287
- def id(self, obj=None):
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
- An object can be anything. In some cases it is convenient to use
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
- :param obj: an object to assign an ID to.
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
- :rtype: int.
2956
+ super(ITE, self).__init__(type=FormulaType.ITE)
300
2957
 
301
- Example:
2958
+ # initially, there are no operands
2959
+ self.cond = self.cons1 = self.cons2 = None
302
2960
 
303
- .. code-block:: python
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
- >>> from pysat.formula import IDPool
306
- >>> vpool = IDPool(occupied=[[12, 18], [3, 10]])
307
- >>>
308
- >>> # creating 5 unique variables for the following strings
309
- >>> for i in range(5):
310
- ... print(vpool.id('v{0}'.format(i + 1)))
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
- In some cases, it makes sense to create an external function for
318
- accessing IDPool, e.g.:
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
- .. code-block:: python
2982
+ assert self.cond and self.cons1 and self.cons2, 'ITE formulas accept three (cond, cons1, and cons2) operands'
321
2983
 
322
- >>> # continuing the previous example
323
- >>> var = lambda i: vpool.id('var{0}'.format(i))
324
- >>> var(5)
325
- 20
326
- >>> var('hello_world!')
327
- 21
2984
+ def __del__(self):
2985
+ """
2986
+ Destructor.
328
2987
  """
329
2988
 
330
- if obj is not None:
331
- vid = self.obj2id[obj]
2989
+ self.name = None
2990
+ self.clauses = []
2991
+ self.encoded = []
2992
+ self.cond = self.cons1 = self.cons2 = None
332
2993
 
333
- if vid not in self.id2obj:
334
- self.id2obj[vid] = obj
335
- else:
336
- # no object is provided => simply return a new ID
337
- vid = self._next()
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
- return vid
3000
+ if not self in seen:
3001
+ seen[self] = True
340
3002
 
341
- def obj(self, vid):
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
- The method can be used to map back a given variable identifier to
344
- the original object labeled by the identifier.
3014
+ Given a list of assumption literals, recursively simplifies the
3015
+ subformulas and creates a new formula.
345
3016
 
346
- :param vid: variable identifier.
347
- :type vid: int
3017
+ :param assumptions: atomic assumptions
3018
+ :type assumptions: list(:class:`Formula`)
348
3019
 
349
- :return: an object corresponding to the given identifier.
3020
+ :rtype: :class:`Formula`
350
3021
 
351
3022
  Example:
352
3023
 
353
3024
  .. code-block:: python
354
3025
 
355
- >>> vpool.obj(21)
356
- 'hello_world!'
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
- if vid in self.id2obj:
360
- return self.id2obj[vid]
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
- return None
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
- def occupy(self, start, stop):
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
- :param start: beginning of the interval.
370
- :param stop: end of the interval.
3080
+ save_clauses = bool(self.clauses)
371
3081
 
372
- :type start: int
373
- :type stop: int
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
- if stop >= start:
377
- self._occupied.append([start, stop])
378
- self._occupied.sort(key=lambda x: x[0])
3113
+ :param dest: where the atoms are collected
3114
+ :type dest: set(:class:`Atom`)
3115
+ """
379
3116
 
380
- def _next(self):
3117
+ self.cond._atoms(dest)
3118
+ self.cons1._atoms(dest)
3119
+ self.cons2._atoms(dest)
3120
+
3121
+ def __repr__(self):
381
3122
  """
382
- Get next variable ID. Skip occupied intervals if any.
3123
+ State reproducible string representaion of object.
383
3124
  """
384
3125
 
385
- self.top += 1
3126
+ return f'{self.__class__.__name__}({repr(self.cond)}, {repr(self.cons1)}, {repr(self.cons2)})'
386
3127
 
387
- while self._occupied and self.top >= self._occupied[0][0]:
388
- if self.top <= self._occupied[0][1]:
389
- self.top = self._occupied[0][1] + 1
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
- self._occupied.pop(0)
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
- return self.top
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"CNF(from_string=\"{s}\")"
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 lzma.
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'``, or
466
- ``'use_ext'``. The latter value indicates that compression type
467
- should be automatically determined based on the file extension.
468
- Using ``'lzma'`` in Python 2 requires the ``backports.lzma``
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.nv = max(map(lambda cl: max(map(abs, cl)), itertools.chain.from_iterable([[[self.nv]], self.clauses])))
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
- for cl in self.clauses:
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.nv = max(map(abs, itertools.chain(*self.clauses)))
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
- or lzma (xz).
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 compress_with: file compression algorithm
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 ``compress_with`` parameter can be ``None`` (i.e.
691
- the file is uncompressed), ``'gzip'``, ``'bzip2'``, ``'lzma'``, or
692
- ``'use_ext'``. The latter value indicates that compression type
693
- should be automatically determined based on the file extension.
694
- Using ``'lzma'`` in Python 2 requires the ``backports.lzma``
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
- header_lines = [f"p cnf {self.nv} {len(self.clauses)}"]
779
- comment_lines = [f"{comment}" for comment in self.comments]
780
- clause_lines = [" ".join(map(str,clause)) + " 0" for clause in self.clauses]
781
- lines = "\n".join(comment_lines + header_lines + clause_lines) + "\n"
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
- :param clause: a new clause to add.
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:`\mathcal{F}`, this method creates a CNF
961
- formula :math:`\\neg{\mathcal{F}}`. The negation of the formula is
962
- encoded to CNF with the use of *auxiliary* Tseitin variables [1]_.
963
- A new CNF formula is returned keeping all the newly introduced
964
- variables that can be accessed through the ``auxvars`` variable.
965
- All the literals used to encode the negation of the original
966
- clauses can be accessed through the ``enclits`` variable.
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"WCNF(from_string=\"{s}\")"
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 lzma.
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'``, or
1092
- ``'use_ext'``. The latter value indicates that compression type
1093
- should be automatically determined based on the file extension.
1094
- Using ``'lzma'`` in Python 2 requires the ``backports.lzma``
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
- or lzma (xz).
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 ``compress_with`` parameter can be ``None`` (i.e.
1307
- the file is uncompressed), ``'gzip'``, ``'bzip2'``, ``'lzma'``, or
1308
- ``'use_ext'``. The latter value indicates that compression type
1309
- should be automatically determined based on the file extension.
1310
- Using ``'lzma'`` in Python 2 requires the ``backports.lzma``
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
- header_lines = [f"p wcnf {self.nv} {len(self.hard)+len(self.soft)} {self.topw}"]
1400
- comment_lines = [f"{comment}" for comment in self.comments]
1401
- hard_lines = [f"{self.topw} " + " ".join(map(str,clause)) + " 0" for clause in self.hard]
1402
- soft_lines = [f"{weight} " + " ".join(map(str,clause)) + " 0" for clause, weight in zip(self.soft, self.wght)]
1403
- lines = "\n".join(comment_lines + header_lines + hard_lines + soft_lines) + "\n"
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.commends = self.comments[:]
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 syntax
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:`\sum_{i=1}^{n}{x_i}\geq k \leftrightarrow
1653
- \sum_{i=1}^{n}{\\neg{x_i}}\leq (n-k)`. Internally, AtMostK constraints
1654
- are stored in variable ``atmosts``, each being a pair ``(lits, k)``,
1655
- where ``lits`` is a list of literals in the sum and ``k`` is the upper
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"CNFPlus(from_string=\"{s}\")"
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 = [i for i in line.split()]
1733
- lits = [int(l) for l in items[:-2]]
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 = len(lits) - 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
- print(' '.join(str(l) for l in am[0]), '<=', am[1], file=file_pointer)
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
- header_lines = [f"p cnf+ {self.nv} {len(self.clauses) + len(self.atmosts)}"]
1820
- comment_lines = [f"{comment}" for comment in self.comments]
1821
- clause_lines = [" ".join(map(str,clause)) + " 0" for clause in self.clauses]
1822
- atmost_lines = [" ".join(map(str,clause)) + " <= " + str(most) for clause, most in self.atmosts]
1823
- lines = "\n".join(comment_lines + header_lines + clause_lines + atmost_lines) + "\n"
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
- for l in am[0]:
1914
- if l > 0:
1915
- line.append('-{0} x{1}'.format('1' if format == 'opb' else '', l))
1916
- neg += 1
1917
- else:
1918
- line.append('+{0} x{1}'.format('1' if format == 'opb' else '', -l))
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:`\sum_{i=1}^{n}{x_i}\geq k \leftrightarrow
2125
- \sum_{i=1}^{n}{\\neg{x_i}}\leq (n-k)`. Internally, AtMostK constraints
2126
- are stored in variable ``atms``, each being a pair ``(lits, k)``, where
2127
- ``lits`` is a list of literals in the sum and ``k`` is the upper bound.
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"WCNFPlus(from_string=\"{s}\")"
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 = [i for i in line.split()]
2228
- lits = [int(l) for l in items[1:-2]]
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 = len(lits) - 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
- print(self.topw, ' '.join(str(l) for l in am[0]), '<=', am[1], file=file_pointer)
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
- header_lines = [f"p wcnf+ {self.nv} {len(self.hard)+len(self.soft)+len(self.atms)} {self.topw}"]
2339
- comment_lines = [f"{comment}" for comment in self.comments]
2340
- hard_lines = [f"{self.topw} " + " ".join(map(str,clause)) + " 0" for clause in self.hard]
2341
- soft_lines = [f"{weight} " + " ".join(map(str,clause)) + " 0" for clause, weight in zip(self.soft, self.wght)]
2342
- atmost_lines = [f"{self.topw} " + " ".join(map(str,clause)) + " <= " + str(most) for clause, most in self.atms]
2343
- lines = "\n".join(comment_lines + header_lines + hard_lines + soft_lines + atmost_lines) + "\n"
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
- for l in am[0]:
2450
- if l > 0:
2451
- line.append('-{0} x{1}'.format('1' if format == 'opb' else '', l))
2452
- neg += 1
2453
- else:
2454
- line.append('+{0} x{1}'.format('1' if format == 'opb' else '', -l))
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.commends = self.comments[:]
5580
+ cnf.comments = self.comments[:]
2569
5581
 
2570
5582
  return cnf
2571
5583