python-sat 1.9.dev20__cp314-cp314t-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.
Files changed (52) hide show
  1. pycard.cp314t-win_amd64.pyd +0 -0
  2. pyformula.cp314t-win_amd64.pyd +0 -0
  3. pysat/__init__.py +36 -0
  4. pysat/_fileio.py +224 -0
  5. pysat/_utils.py +58 -0
  6. pysat/allies/__init__.py +0 -0
  7. pysat/allies/approxmc.py +385 -0
  8. pysat/allies/unigen.py +435 -0
  9. pysat/card.py +1024 -0
  10. pysat/engines.py +1328 -0
  11. pysat/examples/__init__.py +0 -0
  12. pysat/examples/bbscan.py +663 -0
  13. pysat/examples/bica.py +701 -0
  14. pysat/examples/fm.py +527 -0
  15. pysat/examples/genhard.py +516 -0
  16. pysat/examples/hitman.py +648 -0
  17. pysat/examples/lbx.py +638 -0
  18. pysat/examples/lsu.py +496 -0
  19. pysat/examples/mcsls.py +610 -0
  20. pysat/examples/models.py +189 -0
  21. pysat/examples/musx.py +344 -0
  22. pysat/examples/optux.py +750 -0
  23. pysat/examples/primer.py +620 -0
  24. pysat/examples/rc2.py +2043 -0
  25. pysat/examples/usage.py +63 -0
  26. pysat/formula.py +5899 -0
  27. pysat/integer.py +2768 -0
  28. pysat/pb.py +454 -0
  29. pysat/process.py +363 -0
  30. pysat/solvers.py +10223 -0
  31. pysolvers.cp314t-win_amd64.pyd +0 -0
  32. python_sat-1.9.dev20.data/platlib/msvcp140-a4c2229bdc2a2a630acdc095b4d86008.dll +0 -0
  33. python_sat-1.9.dev20.data/scripts/approxmc.py +385 -0
  34. python_sat-1.9.dev20.data/scripts/bbscan.py +663 -0
  35. python_sat-1.9.dev20.data/scripts/bica.py +701 -0
  36. python_sat-1.9.dev20.data/scripts/fm.py +527 -0
  37. python_sat-1.9.dev20.data/scripts/genhard.py +516 -0
  38. python_sat-1.9.dev20.data/scripts/lbx.py +638 -0
  39. python_sat-1.9.dev20.data/scripts/lsu.py +496 -0
  40. python_sat-1.9.dev20.data/scripts/mcsls.py +610 -0
  41. python_sat-1.9.dev20.data/scripts/models.py +189 -0
  42. python_sat-1.9.dev20.data/scripts/musx.py +344 -0
  43. python_sat-1.9.dev20.data/scripts/optux.py +750 -0
  44. python_sat-1.9.dev20.data/scripts/primer.py +620 -0
  45. python_sat-1.9.dev20.data/scripts/rc2.py +2043 -0
  46. python_sat-1.9.dev20.data/scripts/unigen.py +435 -0
  47. python_sat-1.9.dev20.dist-info/DELVEWHEEL +2 -0
  48. python_sat-1.9.dev20.dist-info/METADATA +47 -0
  49. python_sat-1.9.dev20.dist-info/RECORD +52 -0
  50. python_sat-1.9.dev20.dist-info/WHEEL +5 -0
  51. python_sat-1.9.dev20.dist-info/licenses/LICENSE.txt +21 -0
  52. python_sat-1.9.dev20.dist-info/top_level.txt +4 -0
pysat/integer.py ADDED
@@ -0,0 +1,2768 @@
1
+ #!/usr/bin/env python
2
+ #-*- coding:utf-8 -*-
3
+ ##
4
+ ## integer.py
5
+ ##
6
+ ## Created on: Jan 27, 2026
7
+ ## Author: Alexey Ignatiev
8
+ ## E-mail: alexey.ignatiev@monash.edu
9
+ ##
10
+
11
+ """
12
+ ===============
13
+ List of classes
14
+ ===============
15
+
16
+ .. autosummary::
17
+ :nosignatures:
18
+
19
+ Integer
20
+ LinearExpr
21
+ IntegerEngine
22
+
23
+ ==================
24
+ Module description
25
+ ==================
26
+
27
+ This module provides a small set of *experimental modelling and solving
28
+ techniques* for finite-domain integer variables and linear constraints,
29
+ implemented on top of :class:`.BooleanEngine`. Integer variables are
30
+ encoded into Boolean literals, while integer linear constraints are
31
+ translated into Boolean linear (pseudo-Boolean) constraints with
32
+ non-negative weights. Domain constraints are clausified into CNF clauses
33
+ and can be added directly to the SAT solver; the propagator is used only
34
+ for the remaining linear constraints.
35
+
36
+ The implementation is intentionally lightweight and aims to be *simple*
37
+ and *illustrative*, providing working solutions that can be extended over
38
+ time. It is **not** a full-featured CP solver and is not meant to be
39
+ competitive with state-of-the-art CP or SMT tooling. It can serve
40
+ educational and prototyping purposes and may evolve into a more complete
41
+ front-end as (and if) additional constraints and encodings are added.
42
+
43
+ Given an integer variable :math:`x \\in D`, the supported encodings of
44
+ integer domains are:
45
+
46
+ - Direct (value / one-hot) encoding [1]_ where a Boolean literal
47
+ :math:`d_i \\triangleq (x=i)` is introduced for each value :math:`i
48
+ \\in D`. Exactly one value is allowed to be true.
49
+
50
+ - Order (bound) encoding [2]_ [3]_ introduces Boolean literals :math:`o_i
51
+ \\triangleq (x \\geq i)` for each value :math:`i \\in D` representing
52
+ the corresponding thresholds, with monotonicity constraints :math:`o_i
53
+ \\rightarrow o_{i-1}`.
54
+
55
+ - Coupled encoding: both direct and order encodings are present with
56
+ *channeling* clauses :math:`d_i \\leftrightarrow (o_i \\land
57
+ \\neg{o_{i+1}})` linking them [4]_ [5]_, and *no separate one-hot
58
+ constraints* are added for :math:`d_i` because the order chain and
59
+ channeling suffice to imply exactly one value (cf. constraints (3)–(7)
60
+ in [4]_).
61
+
62
+ .. [1] T. Walsh. *SAT v CSP*. CP 2000. pp. 441-456
63
+
64
+ .. [2] N. Tamura, A. Taga, S. Kitagawa, M. Banbara. *Compiling Finite
65
+ Linear CSP into SAT*. Constraints 2009. vol. 14(2). pp. 254-272
66
+
67
+ .. [3] C. Ansótegui and F. Manyà. *Mapping Problems with Finite-Domain
68
+ Variables into Problems with Boolean Variables*. SAT (Selected
69
+ Papers) 2004. pp. 1-15
70
+
71
+ .. [4] A. Ignatiev, Y. Izza, P. J. Stuckey, and J. Marques-Silva.
72
+ *Using MaxSAT for Efficient Explanations of Tree Ensembles*.
73
+ AAAI 2022. pp. 3776-3785
74
+
75
+ .. [5] T. Walsh. *Permutation Problems and Channelling Constraints*.
76
+ LPAR 2001. pp. 377-391
77
+
78
+ .. [6] F. Ulrich-Oltean, P. Nightingale, J. A. Walker. *Learning to
79
+ select SAT encodings for pseudo-Boolean and linear integer
80
+ constraints*. Constraints 2023. vol. 28(3). pp. 397-426
81
+
82
+ Encodings can be mixed across variables (and even within the same linear
83
+ constraint), as each variable :math:`x` provides its own translation of a
84
+ term :math:`c \\cdot x`. Assuming the domain of variable :math:`x` is
85
+ :math:`D`, in the case of direct encoding, we have :math:`x = \\sum_{i\\in
86
+ D}{i \\cdot d_i}` and so :math:`c \\cdot x = \\sum_{i\\in D}{c \\cdot i
87
+ \\cdot d_i}`. In the case of order encoding, it holds that :math:`x =
88
+ \\min(D) + \\sum_{i=\\min(D)+1}^{\\max(D)}{o_i}`, so :math:`c \\cdot x = c
89
+ \\cdot \\min(D) + \\sum_{i=\\min(D)+1}^{\\max(D)}{c \\cdot o_i}`. Next, we
90
+ apply the same rule to compute an offset value :math:`shift=-\\min_{i\\in
91
+ D}{(c \\cdot i)}` such that each coefficient is incremented by
92
+ :math:`shift`.
93
+
94
+ **Translation of linear constraints** over integer variables roughly
95
+ follows the ideas described in [6]_. Consider a linear constraint over
96
+ integer variables :math:`x_j` with numeric coefficients and the right-hand
97
+ side: :math:`\\sum_j c_j \\cdot x_j \\leq b`. The aim of the
98
+ transformation is to *booleanize* each term :math:`c_j \\cdot x_j` of this
99
+ expression (see the paragraph above) so the whole constraint becomes a
100
+ pseudo-Boolean (PB) constraint: a sum of *non-negative* weights on
101
+ *positive* Boolean literals. The flow is as follows:
102
+
103
+ 1. Express each integer variable as a Boolean sum using variables \
104
+ :math:`d_i` or :math:`o_i`, depending on the encoding of the \
105
+ domain of :math:`x_j`, i.e. either direct or order.
106
+ 2. Multiply the resulting sum by the coefficient :math:`c_j`.
107
+ 3. If any resulting weights are negative, which happens when \
108
+ :math:`c_j<0`, add a constant shift so that all weights become \
109
+ non-negative.
110
+ 4. Drop all zero-weight literals (there is one per integer variable).
111
+
112
+ The final Boolean linear constraint has the form: :math:`\\sum_k w_k
113
+ \\cdot l_k \\leq b'`, where :math:`l_k` are Boolean literals representing
114
+ the integers involved, :math:`w_k > 0` are weights, and :math:`b'` is the
115
+ *shifted* bound.
116
+
117
+ Consider an example constraint :math:`x - y \\le 2` with :math:`x` and
118
+ :math:`y` sharing the domain :math:`\\{1,2,3\\}` and assume *direct*
119
+ encoding. Assume Boolean variables :math:`x_i` and :math:`y_i` play the
120
+ role of direct encoding variables :math:`d_i` from above. Observe that
121
+ :math:`x = 1 \\cdot x_1 + 2 \\cdot x_2 + 3 \\cdot x_3` while :math:`-y =
122
+ -1 \\cdot y_1 - 2 \\cdot y_2 - 3 \\cdot y_3`. The shifts for :math:`x` and
123
+ :math:`y` are :math:`-1` and :math:`3`, respectively. The total shift is
124
+ :math:`-1 + 3 = 2`, to be added to the right-hand side. The expressions
125
+ for the terms are updated as follows: :math:`x = 0 \\cdot x_1 + 1 \\cdot
126
+ x_2 + 2 \\cdot x_3` and :math:`-y = 2 \\cdot y_1 + 1 \\cdot y_2 + 0 \\cdot
127
+ y_3`. Removing zero-weight literals results in the final PB constraint:
128
+ :math:`x_2 + 2\\cdot x_3 + 2\\cdot y_1 + y_2 \\leq 4`.
129
+
130
+ Consider another example, this time with an order encoding: :math:`x + y
131
+ \\leq 2` with the domain of both :math:`x` and :math:`y` being
132
+ :math:`[1,2]`. Assuming :math:`x_i` and :math:`y_i` play the role of order
133
+ encoding variables :math:`o_i` from above, we have: :math:`x=1 + x_2` and
134
+ :math:`y=1 + y_2`. The shift in both cases is calculated to be :math:`-1`
135
+ totalling to :math:`-2`, to be added to both sides of the inequality.
136
+ Therefore, the final PB constraint is :math:`x_2 + y_2 \\leq 0`.
137
+
138
+ The module also includes a minimal expression DSL (see
139
+ :class:`.LinearExpr`) so that constraints can be written in a natural form
140
+ (e.g. ``X + Y <= 4``). Supported syntactic sugar includes ``+``, ``-``,
141
+ unary negation, scalar ``*`` by numeric constants, builtin ``abs()``, and
142
+ comparisons ``<=``, ``>=``, ``<``, ``>``, ``==``, ``!=``. Reification is
143
+ supported at the engine level via :meth:`IntegerEngine.add_linear`.
144
+ Equality between two plain :class:`.Integer` objects is a special case:
145
+ ``X == Y`` performs Python identity comparison and does **not** build a
146
+ modelling constraint. To express variable-variable equality, use
147
+ ``X.as_expr() == Y.as_expr()`` (preferred, as it enables the specialized
148
+ ``eq_vars`` encoding) or equivalently ``X - Y == 0``. By contrast,
149
+ ``X != Y`` and order comparisons such as ``X < Y`` work directly on
150
+ :class:`.Integer` objects and return constraints.
151
+ Notably **unsupported** are variable-variable multiplication, division,
152
+ and modulus.
153
+
154
+ Integer variables can be declared to operate with either ``'float'`` or
155
+ ``'decimal'`` coefficients, which is handled by the ``numeric``
156
+ parameter. Integer coefficients can be mixed with either mode. However,
157
+ float and decimal coefficients must not be mixed in the same constraints,
158
+ hence one cannot mix integers with float and decimal numeric modes.
159
+
160
+ Below is a compact 4x4 Sudoku example (digits 1..4). It demonstrates
161
+ direct-encoded integer variables, AllDifferent constraints, and the use
162
+ of the propagator. This is intentionally lightweight and meant to be
163
+ illustrative.
164
+
165
+ .. code-block:: python
166
+
167
+ >>> from pysat.integer import Integer, IntegerEngine
168
+ >>> from pysat.solvers import Cadical195
169
+ >>>
170
+ >>> # 4x4 Sudoku (2x2 blocks), 0 denotes empty
171
+ >>> givens = [
172
+ ... [0, 0, 2, 0],
173
+ ... [0, 0, 0, 3],
174
+ ... [0, 1, 0, 0],
175
+ ... [4, 0, 0, 0],
176
+ ... ]
177
+ >>>
178
+ >>> X = [[Integer(f'x{r}{c}', 1, 4, encoding='direct')
179
+ ... for c in range(4)] for r in range(4)]
180
+ >>>
181
+ >>> eng = IntegerEngine([v for row in X for v in row], adaptive=False)
182
+ >>>
183
+ >>> # rows and columns
184
+ >>> for r in range(4):
185
+ ... eng.add_alldifferent(X[r])
186
+ >>> for c in range(4):
187
+ ... eng.add_alldifferent([X[r][c] for r in range(4)])
188
+ >>>
189
+ >>> # 2x2 blocks
190
+ >>> for br in (0, 2):
191
+ ... for bc in (0, 2):
192
+ ... block = [X[r][c] for r in range(br, br + 2)
193
+ ... for c in range(bc, bc + 2)]
194
+ ... eng.add_alldifferent(block)
195
+ >>>
196
+ >>> # clues
197
+ >>> for r in range(4):
198
+ ... for c in range(4):
199
+ ... if givens[r][c]:
200
+ ... eng.add_linear(X[r][c] == givens[r][c])
201
+ >>>
202
+ >>> with Cadical195() as solver:
203
+ ... solver.connect_propagator(eng)
204
+ ... eng.setup_observe(solver)
205
+ ... if solver.solve():
206
+ ... model = solver.get_model()
207
+ ... values = eng.decode_model(model)
208
+ ... for r in range(4):
209
+ ... row = [values[X[r][c]] for c in range(4)]
210
+ ... print(row)
211
+ [1, 3, 2, 4]
212
+ [2, 4, 1, 3]
213
+ [3, 1, 4, 2]
214
+ [4, 2, 3, 1]
215
+
216
+ ==============
217
+ Module details
218
+ ==============
219
+ """
220
+
221
+ #
222
+ #==============================================================================
223
+ from collections import defaultdict
224
+ from decimal import Decimal
225
+ import math
226
+ import numbers
227
+
228
+ from pysat.card import CardEnc, EncType as CardEncType
229
+ from pysat.engines import BooleanEngine
230
+ from pysat.formula import CNF, Formula
231
+ from pysat.pb import PBEnc, EncType as PBEncType, NoSuchEncodingError, \
232
+ _pindakaas_db, _pindakaas_cnf
233
+ import pindakaas # after pysat.pb, which checks that it is installed
234
+
235
+
236
+ # importable components
237
+ #==============================================================================
238
+ __all__ = ['Integer', 'Int', 'LinearExpr', 'IntegerEngine']
239
+
240
+
241
+ #
242
+ #==============================================================================
243
+ class Integer:
244
+ """
245
+ Finite-domain integer variable with a configurable CNF encoding.
246
+
247
+ Supported encodings are:
248
+
249
+ - ``'direct'``: one-hot / value encoding
250
+ - ``'order'``: order / bound encoding
251
+ - ``'coupled'``: both encodings with channeling clauses
252
+
253
+ Direct encoding uses Boolean literals :math:`d_i \\triangleq (x=i)`
254
+ for each value :math:`i` in the domain and a cardinality encoding to
255
+ enforce that exactly one value is chosen. Order encoding introduces
256
+ Boolean literals :math:`o_i \\triangleq (x\\ge i)` and adds
257
+ monotonicity implications between them. Coupled encoding mixes the two
258
+ types of literals but **omits** explicit one-hot constraints for
259
+ :math:`d_i` since the order chain plus channeling already imply
260
+ exactly one value.
261
+
262
+ :param name: variable name
263
+ :param lb: lower bound
264
+ :param ub: upper bound
265
+ :param encoding: domain encoding ('direct', 'order', or 'coupled')
266
+ :param card_enc: cardinality encoding for the direct encoding (ignored for coupled encoding)
267
+ :param vpool: external variable pool (optional)
268
+ :param numeric: numeric mode ('float' or 'decimal')
269
+
270
+ :type name: str
271
+ :type lb: int
272
+ :type ub: int
273
+ :type encoding: str
274
+ :type card_enc: int
275
+ :type vpool: :class:`pysat.formula.IDPool` or None
276
+ :type numeric: str
277
+
278
+ Example:
279
+
280
+ .. code-block:: python
281
+
282
+ >>> from pysat.integer import Integer
283
+ >>> X = Integer('X', 0, 3, encoding='direct')
284
+ >>> Y = Integer('Y', 0, 3, encoding='order')
285
+ >>> c1 = X + Y <= 4
286
+ >>> c2 = X - Y >= 1
287
+ >>> print(c1)
288
+ ('linear', [[2, 3, 4, 5, 6, 7], 4, {2: 1, 3: 2, 4: 3, 5: 1, 6: 1, 7: 1}])
289
+ >>> print(c2)
290
+ ('linear', [[1, 2, 3, 5, 6, 7], 2, {1: 3, 2: 2, 3: 1, 5: 1, 6: 1, 7: 1}])
291
+
292
+ .. note::
293
+
294
+ Integer coefficients attached to the variables can be mixed with
295
+ either numeric mode. Mixing floats with decimals raises
296
+ ``TypeError``.
297
+ """
298
+
299
+ def __init__(self, name, lb, ub, encoding='direct',
300
+ card_enc=CardEncType.seqcounter, vpool=None,
301
+ numeric='float'):
302
+ """
303
+ Constructor.
304
+ """
305
+
306
+ # some common-sense assumptions
307
+ assert lb <= ub, 'Lower bound cannot be greater than upper bound'
308
+ assert encoding in ('direct', 'order', 'coupled'), f'Unknown encoding {encoding}'
309
+ assert numeric in ('float', 'decimal'), f'Unknown numeric mode {numeric}'
310
+
311
+ self.name = name
312
+ self.lb = lb
313
+ self.ub = ub
314
+ self.domain = list(range(lb, ub + 1))
315
+ self.encoding = encoding
316
+ self.card_enc = card_enc
317
+ self.numeric = numeric
318
+ self.clauses = None
319
+
320
+ # deciding which IDPool to use
321
+ self.vpool = vpool if vpool is not None else Formula.export_vpool()
322
+
323
+ # ensure IDs are registered upfront to keep vpool stable
324
+ if encoding in ('direct', 'coupled'):
325
+ for v in self.domain:
326
+ self.vpool.id((self, 'eq', v))
327
+ if encoding in ('order', 'coupled'):
328
+ for k in range(lb + 1, ub + 1):
329
+ self.vpool.id((self, 'ge', k))
330
+
331
+ def _coerce_number(self, value):
332
+ """
333
+ Coerce a numeric (coefficient) value to the variable's numeric
334
+ mode.
335
+
336
+ Integers are accepted in both modes. Mixing floats with decimals
337
+ is not allowed and raises ``TypeError``.
338
+ """
339
+
340
+ if self.numeric == 'decimal':
341
+ if isinstance(value, Decimal):
342
+ return value
343
+ if isinstance(value, numbers.Integral):
344
+ return Decimal(value)
345
+
346
+ raise TypeError('Decimal mode does not accept non-decimal values')
347
+
348
+ else: # 'float'
349
+ if isinstance(value, Decimal):
350
+ raise TypeError('Float mode does not accept Decimal values')
351
+ if isinstance(value, numbers.Real):
352
+ return value
353
+
354
+ # neither decimal nor float
355
+ raise TypeError('Numeric value expected')
356
+
357
+ def equals(self, value):
358
+ """
359
+ Return the Boolean literal corresponding to :math:`x = value`.
360
+
361
+ Only available with the direct (or coupled) encoding.
362
+
363
+ :param value: domain value
364
+ :type value: int
365
+
366
+ :rtype: int
367
+ """
368
+
369
+ assert self.encoding in ('direct', 'coupled'), 'Direct encoding is disabled'
370
+ assert self.lb <= value <= self.ub, 'Value is outside the domain'
371
+ return self.vpool.id((self, 'eq', value))
372
+
373
+ def ge(self, value):
374
+ """
375
+ Return the literal representing :math:`x \\geq value`.
376
+
377
+ Only available with the order (or coupled) encoding.
378
+
379
+ :param value: lower bound value
380
+ :type value: int
381
+
382
+ :rtype: int
383
+ """
384
+
385
+ assert self.encoding in ('order', 'coupled'), 'Order encoding is disabled'
386
+ assert self.lb + 1 <= value <= self.ub, 'Value is outside the order domain'
387
+ return self.vpool.id((self, 'ge', value))
388
+
389
+ def atleast(self, value):
390
+ """
391
+ Return the literal representing :math:`x \\geq value`. Same as
392
+ :meth:`ge`.
393
+ """
394
+
395
+ return self.ge(value)
396
+
397
+ def le(self, value):
398
+ """
399
+ Return the literal representing :math:`x \\leq value`.
400
+
401
+ Only available with the order (or coupled) encoding.
402
+
403
+ :param value: upper bound value
404
+ :type value: int
405
+
406
+ :rtype: int
407
+ """
408
+
409
+ assert self.encoding in ('order', 'coupled'), 'Order encoding is disabled'
410
+ assert self.lb <= value <= self.ub - 1, 'Value is outside the order domain'
411
+
412
+ # X <= value <=> not (X >= value + 1)
413
+ return -self.vpool.id((self, 'ge', value + 1))
414
+
415
+ def atmost(self, value):
416
+ """
417
+ Return the literal representing :math:`X \\leq value`.
418
+ """
419
+
420
+ return self.le(value)
421
+
422
+ def __repr__(self):
423
+ """
424
+ String representation of the variable.
425
+ """
426
+
427
+ return f'{self.__class__.__name__}({self.name}, {self.lb}..{self.ub})'
428
+
429
+ def __str__(self):
430
+ """
431
+ String representation of the variable.
432
+ """
433
+
434
+ return f'{self.name}'
435
+
436
+ def __hash__(self):
437
+ """
438
+ Hash by identity to keep :class:`.Integer` usable as dict keys.
439
+ """
440
+
441
+ return object.__hash__(self)
442
+
443
+ def as_expr(self):
444
+ """
445
+ Convert the variable to a linear expression, i.e. an object of
446
+ class :class:`LinearExpr`. Given ``self``, a new object of class
447
+ :class:`LinearExpr` is returned.
448
+ """
449
+
450
+ return LinearExpr({self: 1}, 0, numeric=self.numeric)
451
+
452
+ def __add__(self, other):
453
+ """
454
+ Add a variable to another expression or numeric value.
455
+ """
456
+
457
+ return self.as_expr() + other
458
+
459
+ def __radd__(self, other):
460
+ """
461
+ Right-add for numeric values and expressions.
462
+ """
463
+
464
+ return other + self.as_expr()
465
+
466
+ def __sub__(self, other):
467
+ """
468
+ Subtract another expression or numeric value.
469
+ """
470
+
471
+ return self.as_expr() - other
472
+
473
+ def __rsub__(self, other):
474
+ """
475
+ Right-subtract for numeric values and expressions.
476
+ """
477
+
478
+ return other - self.as_expr()
479
+
480
+ def __mul__(self, other):
481
+ """
482
+ Multiply by a numeric coefficient.
483
+ """
484
+
485
+ try:
486
+ coeff = self._coerce_number(other)
487
+ except TypeError:
488
+ return NotImplemented
489
+
490
+ return LinearExpr({self: coeff}, 0, numeric=self.numeric)
491
+
492
+ def __rmul__(self, other):
493
+ """
494
+ Right-multiply by a numeric coefficient.
495
+ """
496
+
497
+ return self.__mul__(other)
498
+
499
+ def __le__(self, other):
500
+ """
501
+ Build a ``<=`` constraint against a numeric value.
502
+ """
503
+
504
+ return self.as_expr() <= other
505
+
506
+ def __ge__(self, other):
507
+ """
508
+ Build a ``>=`` constraint against a numeric value.
509
+ """
510
+
511
+ return self.as_expr() >= other
512
+
513
+ def __lt__(self, other):
514
+ """
515
+ Build a ``<`` constraint against a numeric value.
516
+ """
517
+
518
+ return self.as_expr() < other
519
+
520
+ def __gt__(self, other):
521
+ """
522
+ Build a ``>`` constraint against a numeric value.
523
+ """
524
+
525
+ return self.as_expr() > other
526
+
527
+ def __eq__(self, other):
528
+ """
529
+ Equality comparison against a numeric value or identity check.
530
+
531
+ Note that ``Integer == Integer`` is reserved for Python object
532
+ identity so that :class:`.Integer` remains usable as a dict key.
533
+ It therefore does **not** construct a modelling constraint. For
534
+ variable-variable equality, use ``x.as_expr() == y.as_expr()`` or
535
+ ``x - y == 0``.
536
+ """
537
+
538
+ if isinstance(other, Integer):
539
+ return self is other
540
+
541
+ return self.as_expr() == other
542
+
543
+ def __ne__(self, other):
544
+ """
545
+ Inequality comparison against another :class:`.Integer` variable.
546
+
547
+ Unlike :meth:`__eq__`, ``Integer != Integer`` does construct a
548
+ modelling constraint.
549
+ """
550
+
551
+ if isinstance(other, Integer):
552
+ return ('ne', self, other)
553
+
554
+ return self.as_expr().__ne__(other)
555
+
556
+ def abs(self):
557
+ """
558
+ Return an absolute-value wrapper for this variable.
559
+ """
560
+
561
+ return AbsExpr(self)
562
+
563
+ def __abs__(self):
564
+ """
565
+ Builtin abs() support.
566
+ """
567
+
568
+ return self.abs()
569
+
570
+ def decode(self, model):
571
+ """
572
+ Decode the integer value of the variable given a SAT model
573
+ assigning the Boolean variables encoding the domain.
574
+
575
+ :param model: SAT model (list of literals)
576
+ :type model: list(int)
577
+
578
+ :rtype: int or None
579
+ """
580
+
581
+ def is_satisfied(lit):
582
+ # a literal beyond the model occurs in no clause, so it is free
583
+ return abs(lit) <= len(model) and model[abs(lit) - 1] == lit
584
+
585
+ if self.encoding == 'direct':
586
+ for value in self.domain:
587
+ if is_satisfied(self.vpool.id((self, 'eq', value))):
588
+ return value
589
+ return
590
+
591
+ # for order or coupled encodings,
592
+ # applying binary search, due to monotonicity
593
+ lo, hi = self.lb + 1, self.ub
594
+ value = self.lb
595
+
596
+ while lo <= hi:
597
+ mid = (lo + hi) // 2
598
+ if is_satisfied(self.vpool.id((self, 'ge', mid))):
599
+ value = mid
600
+ lo = mid + 1
601
+ else:
602
+ hi = mid - 1
603
+
604
+ return value
605
+
606
+ def encode(self, value):
607
+ """
608
+ Encode an integer value as a list of true literals.
609
+
610
+ For direct encoding, the list contains a single Boolean literal
611
+ :math:`d_i` such that :math:`d_i \\triangleq (x=i)`.
612
+
613
+ For order encoding, the list contains at most two Boolean literals
614
+ :math:`o_i` and :math:`\\neg{o_{i+1}}` such that :math:`o_i
615
+ \\triangleq (x \\ge i)` and :math:`o_{i+1} \\triangleq (x \\ge
616
+ i+1)`. For the bound values, only one literal is used.
617
+
618
+ :param value: domain value
619
+ :type value: int or None
620
+
621
+ :rtype: list(int)
622
+ """
623
+
624
+ if value is None:
625
+ return []
626
+
627
+ if self.encoding in ('direct', 'coupled'):
628
+ return [self.equals(value)]
629
+
630
+ # order encoding: pin value with adjacent bounds
631
+ if self.lb == self.ub:
632
+ # variable has a singleton domain - no variables are created at all
633
+ return []
634
+ if value == self.lb:
635
+ return [-self.ge(self.lb + 1)]
636
+ if value == self.ub:
637
+ return [self.ge(self.ub)]
638
+ return [self.ge(value), -self.ge(value + 1)]
639
+
640
+ def domain_clauses(self):
641
+ """
642
+ Return CNF clauses encoding the variable's domain.
643
+
644
+ Example:
645
+
646
+ .. code-block:: python
647
+
648
+ >>> from pysat.integer import Integer
649
+ >>> X = Integer('X', 1, 3, encoding='direct')
650
+ >>> cnf = X.domain_clauses()
651
+ >>> print(cnf)
652
+ [[1, 2, 3], [-1, 4], [-4, 5], [-2, -4], [-2, 5], [-3, -5]]
653
+
654
+ :rtype: list(list(int))
655
+ """
656
+
657
+ if self.clauses is not None:
658
+ return self.clauses
659
+
660
+ # we are going to keep our clauses here
661
+ self.clauses = []
662
+
663
+ if self.encoding == 'direct':
664
+ lits = [self.equals(val) for val in self.domain]
665
+ cnf = CardEnc.equals(lits=lits, bound=1, vpool=self.vpool,
666
+ encoding=self.card_enc)
667
+ self.clauses.extend(cnf.clauses)
668
+
669
+ if self.encoding in ('order', 'coupled'):
670
+ for val in range(self.lb + 2, self.ub + 1):
671
+ self.clauses.append([-self.ge(val), +self.ge(val - 1)])
672
+
673
+ if self.encoding == 'coupled':
674
+ self._add_channeling()
675
+
676
+ return self.clauses
677
+
678
+ def linearize(self, coeff):
679
+ """
680
+ Return a pair (weights, shift) encoding ``coeff * X`` using
681
+ Boolean literals. The sum of weights for a satisfying assignment
682
+ equals ``coeff * X + shift``.
683
+
684
+ The shift is the **negative of the minimum value** of ``coeff *
685
+ X`` over the variable's domain. This normalization ensures all
686
+ weights are non-negative (required by :mod:`pysat.pb` and
687
+ :class:`.BooleanEngine`) while preserving equivalence by adjusting
688
+ the bound by the same constant. When the minimum term is 0 (e.g.,
689
+ domain includes 0 with a non-negative coefficient), the shift is 0
690
+ and no adjustment occurs. For order encoding this corresponds to
691
+ ``-coeff * lb`` when ``coeff >= 0`` and ``-coeff * ub`` when
692
+ ``coeff < 0``, derived from the threshold identity for ``X``.
693
+
694
+ For direct encoding, this is simply a per-value weight map. For
695
+ example, if :math:`x \\in \\{0, 1, 2\\}` and :math:`2x` is
696
+ requested, then literals for values 0, 1, 2 get weights 0, 2, 4,
697
+ respectively. For order encoding, we use the identity :math:`x =
698
+ lb + \\sum_{k=lb+1}^{ub} (x \\geq k)`.
699
+
700
+ :param coeff: coefficient
701
+ :type coeff: int, float, or Decimal
702
+
703
+ :rtype: (dict, number)
704
+
705
+ Consider an example (direct encoding, :math:`x \\in \\{0,1,2\\}`):
706
+
707
+ .. code-block:: python
708
+
709
+ >>> x = Integer('X', 0, 2, encoding='direct')
710
+ >>> wmap, shift = x.linearize(-3)
711
+ >>> print(shift)
712
+ 6
713
+ >>> # weights correspond to literals [x=0], [x=1], [x=2]
714
+ >>> print([wmap[x.equals(v)] for v in x.domain])
715
+ [6, 3, 0]
716
+
717
+ Here the minimum term is ``-3 * 2 = -6``, so the shift is ``6``,
718
+ which makes all weights in the final expression non-negative.
719
+
720
+ Consider another example, this time with a negative coefficient:
721
+
722
+ .. code-block:: python
723
+
724
+ >>> y = Integer('y', 1, 3, encoding='direct')
725
+ >>> wmap, shift = y.linearize(-4)
726
+ >>> print(shift)
727
+ 12
728
+ >>> # weights correspond to literals [y=1], [y=2], [y=3]
729
+ >>> print([wmap[y.equals(v)] for v in y.domain])
730
+ [8, 4, 0]
731
+
732
+ The minimum term is ``-4 * 3 = -12``, so the shift is ``12``.
733
+ """
734
+
735
+ # first, making sure we are using a *compatible* coefficient
736
+ coeff = self._coerce_number(coeff)
737
+
738
+ # weights map is going to be stored here
739
+ wmap = defaultdict(lambda: 0)
740
+
741
+ if self.encoding in ('order', 'coupled'):
742
+ if coeff >= 0:
743
+ shift = -coeff * self.lb
744
+ for k in range(self.lb + 1, self.ub + 1):
745
+ wmap[self.ge(k)] += +coeff
746
+ else:
747
+ shift = -coeff * self.ub
748
+ for k in range(self.lb, self.ub):
749
+ wmap[self.le(k)] += -coeff
750
+
751
+ else: # direct encoding
752
+ shift = -coeff * (self.lb if coeff >= 0 else self.ub)
753
+ for v in self.domain:
754
+ wmap[self.equals(v)] += coeff * v + shift
755
+
756
+ return wmap, shift
757
+
758
+ def _linear_term(self, kind, wmap):
759
+ """
760
+ Inverse of :meth:`linearize` for one kind of literal. Given a map
761
+ from domain values ``v`` to pairs ``(lit, weight)``, where ``lit``
762
+ is the literal of :math:`x \\geq v` (``kind == 'ge'``) or of
763
+ :math:`x = v` (``kind == 'eq'``), possibly negated, return
764
+ ``(coeff, const)`` such that the weighted sum of the literals
765
+ equals ``coeff * X + const``. Return ``None`` if the map does not
766
+ have this form.
767
+
768
+ :param kind: ``'ge'`` or ``'eq'``
769
+ :param wmap: literals and their weights by domain value
770
+ :type kind: str
771
+ :type wmap: dict
772
+
773
+ :rtype: tuple or None
774
+ """
775
+
776
+ if kind == 'ge':
777
+ # x = lb + sum_k [x >= k] and ub - x = sum_k -[x >= k]
778
+ signs = {lit > 0 for lit, _ in wmap.values()}
779
+ wvals = {wght for _, wght in wmap.values()}
780
+ if len(wmap) != self.ub - self.lb or len(signs) != 1 or len(wvals) != 1:
781
+ return None
782
+
783
+ wght = wvals.pop()
784
+ return (wght, -wght * self.lb) if signs.pop() else (-wght, wght * self.ub)
785
+
786
+ # direct literals, the weights must be affine in the values
787
+ if any(lit < 0 for lit, _ in wmap.values()):
788
+ return None
789
+
790
+ wvals = [wmap[v][1] if v in wmap else 0 for v in self.domain]
791
+ coeff = wvals[1] - wvals[0] if len(wvals) > 1 else 0
792
+ if any(w != wvals[0] + coeff * i for i, w in enumerate(wvals)):
793
+ return None
794
+
795
+ return coeff, wvals[0] - coeff * self.lb
796
+
797
+ def _add_channeling(self):
798
+ """
799
+ Clauses linking direct and order encodings.
800
+
801
+ Let :math:`d_i` denote :math:`x=i` and :math:`o_i` denote
802
+ :math:`x \\ge i`. We add the endpoint equivalences
803
+
804
+ - :math:`d_{lb} \\leftrightarrow \\neg o_{lb+1}`
805
+ - :math:`d_{ub} \\leftrightarrow o_{ub}`
806
+
807
+ and for each interior value :math:`i` in :math:`(lb, ub)`:
808
+
809
+ - :math:`d_i \\leftrightarrow (o_i \\land \\neg o_{i+1})`.
810
+
811
+ Together with the order chain, these clauses imply that exactly
812
+ one value literal is true, i.e. such a constraint is not
813
+ explicitly added.
814
+ """
815
+
816
+ # if the domain is a singleton, we need to
817
+ # make sure the direct variable is set to true
818
+ if self.lb == self.ub:
819
+ self.clauses.append([self.equals(self.lb)])
820
+ return
821
+
822
+ # lower end-point
823
+ self.clauses.append([-self.equals(self.lb), -self.ge(self.lb + 1)])
824
+ self.clauses.append([+self.ge(self.lb + 1), +self.equals(self.lb)])
825
+
826
+ # upper end-point
827
+ self.clauses.append([-self.equals(self.ub), +self.ge(self.ub)])
828
+ self.clauses.append([-self.ge(self.ub), +self.equals(self.ub)])
829
+
830
+ # interior values
831
+ for v in range(self.lb + 1, self.ub):
832
+ self.clauses.append([-self.equals(v), +self.ge(v)])
833
+ self.clauses.append([-self.equals(v), -self.ge(v + 1)])
834
+ self.clauses.append([-self.ge(v), +self.ge(v + 1), +self.equals(v)])
835
+
836
+
837
+ #
838
+ #==============================================================================
839
+ class Int(Integer):
840
+ """
841
+ This is just an alias for the Integer class.
842
+ """
843
+
844
+ pass
845
+
846
+
847
+ #
848
+ #==============================================================================
849
+ class LinearExpr:
850
+ """
851
+ Minimal linear expression builder for :class:`Integer` with numeric
852
+ coefficients. Supports comparisons against numeric values and other
853
+ linear expressions.
854
+
855
+ The resulting comparisons return :class:`.BooleanEngine`-style
856
+ constraints (tuples) that can be passed to
857
+ :meth:`.IntegerEngine.add_linear`. Syntactic sugar supports ``+``,
858
+ ``-``, unary negation, scalar ``*`` by numeric constants, builtin
859
+ ``abs()``, and comparisons. Reification is supported at the engine
860
+ level via :meth:`.IntegerEngine.add_linear`. Variable-variable
861
+ multiplication, division, and modulus are not supported. In
862
+ particular, variable-variable equality is best written as
863
+ ``x.as_expr() == y.as_expr()`` so that the specialized ``eq_vars``
864
+ encoding can be used.
865
+
866
+ Example:
867
+
868
+ .. code-block:: python
869
+
870
+ >>> from pysat.integer import Integer
871
+ >>> X = Integer('X', 0, 3)
872
+ >>> Y = Integer('Y', 0, 3, encoding='order')
873
+ >>> c1 = X + Y <= 4
874
+ >>> c2 = 2 * X - Y >= 1
875
+ >>> print(c1)
876
+ ('linear', [[2, 3, 4, 5, 6, 7], 4, {2: 1, 3: 2, 4: 3, 5: 1, 6: 1, 7: 1}])
877
+ >>> print(c2)
878
+ ('linear', [[1, 2, 3, 5, 6, 7], 5, {1: 6, 2: 4, 3: 2, 5: 1, 6: 1, 7: 1}])
879
+
880
+ .. note::
881
+
882
+ Integer constants (coefficients) can be mixed with either numeric
883
+ mode. Mixing floats with decimals raises ``TypeError``.
884
+ """
885
+
886
+ def __init__(self, terms=None, const=0, numeric='float'):
887
+ """
888
+ Constructor.
889
+ """
890
+
891
+ self.terms = terms or {}
892
+ self.const = const
893
+
894
+ # mode
895
+ self.numeric = numeric
896
+
897
+ def _check_numeric(self, numeric):
898
+ """
899
+ Check if the numeric mode is consistent with the current mode.
900
+ """
901
+
902
+ if self.numeric != numeric:
903
+ raise ValueError('Mixed numeric modes in linear expression')
904
+
905
+ def _coerce_number(self, value):
906
+ """
907
+ Convert a given value to the appropriate numeric type.
908
+ """
909
+
910
+ if self.numeric == 'decimal':
911
+ if isinstance(value, Decimal):
912
+ return value
913
+ if isinstance(value, numbers.Integral):
914
+ return Decimal(value)
915
+ raise TypeError('Decimal mode does not accept non-decimal values')
916
+
917
+ # float
918
+ if isinstance(value, Decimal):
919
+ raise TypeError('Float mode does not accept Decimal values')
920
+ if isinstance(value, numbers.Real):
921
+ return value
922
+
923
+ # well, it's neither decimal not float
924
+ raise TypeError('Numeric value expected')
925
+
926
+ def _add_term(self, var, coeff):
927
+ """
928
+ Add a term ``coeff * var`` to the expression.
929
+ """
930
+
931
+ if coeff == 0:
932
+ # nothing to add
933
+ return
934
+
935
+ # updating the coefficient of this variable
936
+ self.terms[var] = self.terms.get(var, 0) + coeff
937
+
938
+ # if we canceled the term by this update
939
+ if self.terms[var] == 0:
940
+ del self.terms[var]
941
+
942
+ def _singleton(self):
943
+ """
944
+ Return the single :class:`.Integer` variable if this is a plain
945
+ variable expression; otherwise return ``None``.
946
+ """
947
+
948
+ if self.const == 0 and len(self.terms) == 1:
949
+ var, coeff = next(iter(self.terms.items()))
950
+ return var if coeff == 1 else None
951
+
952
+ def _norm(self, other):
953
+ """
954
+ Normalize the operand: check numeric mode and convert to
955
+ LinearExpr or coerced constant. Returns a pair (expr, val).
956
+ """
957
+
958
+ # constant numeric value
959
+ try:
960
+ val = self._coerce_number(other)
961
+ return None, val
962
+ except TypeError:
963
+ pass
964
+
965
+ # Integer variable
966
+ if isinstance(other, Integer):
967
+ self._check_numeric(other.numeric)
968
+ return other.as_expr(), None
969
+
970
+ # Linear expression
971
+ if isinstance(other, LinearExpr):
972
+ self._check_numeric(other.numeric)
973
+ return other, None
974
+
975
+ return NotImplemented, None
976
+
977
+ def bounds(self):
978
+ """
979
+ Compute a (min, max) pair for the expression over variable domains.
980
+ """
981
+
982
+ # taking into account the constant term
983
+ minv = self.const
984
+ maxv = self.const
985
+
986
+ # computing both ming and max values
987
+ for var, coeff in self.terms.items():
988
+ if coeff >= 0:
989
+ minv += coeff * var.lb
990
+ maxv += coeff * var.ub
991
+ else:
992
+ minv += coeff * var.ub
993
+ maxv += coeff * var.lb
994
+
995
+ return minv, maxv
996
+
997
+ def __add__(self, other):
998
+ """
999
+ Add a numeric value, variable, or expression.
1000
+ """
1001
+
1002
+ expr, val = self._norm(other)
1003
+ if expr is NotImplemented:
1004
+ return NotImplemented
1005
+
1006
+ if expr:
1007
+ res = LinearExpr(dict(self.terms), self.const + expr.const,
1008
+ numeric=self.numeric)
1009
+ for v, c in expr.terms.items():
1010
+ res._add_term(v, c)
1011
+ return res
1012
+
1013
+ return LinearExpr(dict(self.terms), self.const + val,
1014
+ numeric=self.numeric)
1015
+
1016
+ def __radd__(self, other):
1017
+ """
1018
+ Right-add for numeric values and expressions.
1019
+ """
1020
+
1021
+ return self.__add__(other)
1022
+
1023
+ def __sub__(self, other):
1024
+ """
1025
+ Subtract a numeric value, variable, or expression.
1026
+ """
1027
+
1028
+ expr, val = self._norm(other)
1029
+ if expr is NotImplemented:
1030
+ return NotImplemented
1031
+
1032
+ if expr:
1033
+ res = LinearExpr(dict(self.terms), self.const - expr.const,
1034
+ numeric=self.numeric)
1035
+ for v, c in expr.terms.items():
1036
+ res._add_term(v, -c)
1037
+ return res
1038
+
1039
+ return LinearExpr(dict(self.terms), self.const - val,
1040
+ numeric=self.numeric)
1041
+
1042
+ def __rsub__(self, other):
1043
+ """
1044
+ Right-subtract for numeric values and expressions.
1045
+ """
1046
+
1047
+ return (-1) * self + other
1048
+
1049
+ def __mul__(self, other):
1050
+ """
1051
+ Multiply by a numeric coefficient.
1052
+ """
1053
+
1054
+ try:
1055
+ value = self._coerce_number(other)
1056
+ except TypeError:
1057
+ return NotImplemented
1058
+ if value == 1:
1059
+ return LinearExpr(dict(self.terms), self.const, numeric=self.numeric)
1060
+
1061
+ # general case of multiplying by a constant numeric value
1062
+ res = LinearExpr({}, self.const * value, numeric=self.numeric)
1063
+ for v, c in self.terms.items():
1064
+ res.terms[v] = c * value
1065
+ return res
1066
+
1067
+ def __rmul__(self, other):
1068
+ """
1069
+ Right-multiply by a numeric coefficient.
1070
+ """
1071
+
1072
+ return self.__mul__(other)
1073
+
1074
+ def __neg__(self):
1075
+ """
1076
+ Unary negation.
1077
+ """
1078
+
1079
+ return (-1) * self
1080
+
1081
+ def abs(self):
1082
+ """
1083
+ Return an absolute-value wrapper for this expression.
1084
+ """
1085
+
1086
+ return AbsExpr(self)
1087
+
1088
+ def __abs__(self):
1089
+ """
1090
+ Builtin ``abs()`` support.
1091
+ """
1092
+
1093
+ return self.abs()
1094
+
1095
+ def __le__(self, other):
1096
+ """
1097
+ Build a ``<=`` constraint against a numeric value.
1098
+ """
1099
+
1100
+ expr, val = self._norm(other)
1101
+ if expr is NotImplemented:
1102
+ return NotImplemented
1103
+
1104
+ if expr:
1105
+ return (self - expr) <= 0
1106
+
1107
+ terms = [(c, v) for v, c in self.terms.items()]
1108
+ return LinearExpr.pb_linear_leq(terms, val - self.const)
1109
+
1110
+ def __ge__(self, other):
1111
+ """
1112
+ Build a ``>=`` constraint against a numeric value.
1113
+ """
1114
+
1115
+ expr, val = self._norm(other)
1116
+ if expr is NotImplemented:
1117
+ return NotImplemented
1118
+
1119
+ if expr:
1120
+ return (self - expr) >= 0
1121
+
1122
+ return (-self) <= (-val)
1123
+
1124
+ def __lt__(self, other):
1125
+ """
1126
+ Build a ``<`` constraint against a numeric value.
1127
+ """
1128
+
1129
+ expr, val = self._norm(other)
1130
+ if expr is NotImplemented:
1131
+ return NotImplemented
1132
+
1133
+ if expr:
1134
+ return (self - expr) <= -1
1135
+
1136
+ if self.numeric == 'decimal':
1137
+ return self <= val.next_minus()
1138
+
1139
+ return self <= math.nextafter(float(val), float('-inf'))
1140
+
1141
+ def __gt__(self, other):
1142
+ """
1143
+ Build a ``>`` constraint against a numeric value.
1144
+ """
1145
+
1146
+ expr, val = self._norm(other)
1147
+ if expr is NotImplemented:
1148
+ return NotImplemented
1149
+
1150
+ if expr:
1151
+ return (self - expr) >= 1
1152
+
1153
+ if self.numeric == 'decimal':
1154
+ return self >= val.next_plus()
1155
+
1156
+ return self >= math.nextafter(float(val), float('inf'))
1157
+
1158
+ def __eq__(self, other):
1159
+ """
1160
+ Build an ``==`` constraint against a numeric value or expression.
1161
+
1162
+ Returns a list with two inequalities.
1163
+
1164
+ Note that if both sides are plain :class:`.Integer` variables
1165
+ (coefficient 1 and no constant involved), a specialized equality
1166
+ constraint is emitted to allow more direct domain pruning.
1167
+ """
1168
+
1169
+ # checking if the inputs are singletons
1170
+ svar = self._singleton()
1171
+ expr, val = self._norm(other)
1172
+ if expr is NotImplemented:
1173
+ return NotImplemented
1174
+
1175
+ # singletons
1176
+ if svar is not None:
1177
+ if isinstance(other, Integer):
1178
+ return ('eq_vars', svar, other)
1179
+ if expr and expr._singleton() is not None:
1180
+ return ('eq_vars', svar, expr._singleton())
1181
+
1182
+ # generic linear expressions
1183
+ if expr:
1184
+ diff = self - expr
1185
+ return ('lin_eq', [diff <= 0, diff >= 0])
1186
+
1187
+ return ('lin_eq', [self <= val, self >= val])
1188
+
1189
+ def __ne__(self, other):
1190
+ """
1191
+ Build a != constraint against another expression or numeric value.
1192
+ """
1193
+
1194
+ expr, val = self._norm(other)
1195
+ if expr is NotImplemented:
1196
+ return NotImplemented
1197
+
1198
+ if expr:
1199
+ return ('ne_linear', self, expr)
1200
+
1201
+ return ('ne_linear', self, LinearExpr({}, val, numeric=self.numeric))
1202
+
1203
+
1204
+ @staticmethod
1205
+ def pb_linear_leq(terms, bound):
1206
+ """
1207
+ Build a PySAT ``'linear'`` constraint over Boolean variables
1208
+ (cardinality / pseudo-Boolean) representing:
1209
+
1210
+ sum_i coeff_i * X_i <= bound
1211
+
1212
+ where the left-hand side of the constraint signifies a list of
1213
+ pairs ``(coeff_i, X_i)`` with ``coeff_i`` being a number and
1214
+ ``X_i`` being :class:`Integer` and ``bound`` is a number.
1215
+
1216
+ :param terms: terms of the left-hand side
1217
+ :param bound: right-hand side
1218
+
1219
+ :type terms: list of pairs ``(coeff, Integer)``
1220
+ :type bound: number
1221
+
1222
+ Example:
1223
+
1224
+ .. code-block:: python
1225
+
1226
+ >>> from pysat.integer import Integer, LinearExpr
1227
+ >>> X = Integer('X', 0, 2)
1228
+ >>> Y = Integer('Y', 0, 2, encoding='order')
1229
+ >>> c = LinearExpr.pb_linear_leq([(1, X), (2, Y)], 2)
1230
+ >>> print(c)
1231
+ ('linear', [[2, 3, 4, 5], 2, {2: 1, 3: 2, 4: 2, 5: 2}])
1232
+
1233
+ :rtype: tuple
1234
+ """
1235
+
1236
+ # do nothing if there are no terms
1237
+ terms = terms or []
1238
+
1239
+ if terms:
1240
+ # checking all the numeric types
1241
+ var0 = terms[0][1]
1242
+ numeric = var0.numeric
1243
+ for _, var in terms:
1244
+ if var.numeric != numeric:
1245
+ raise ValueError('Mixed numeric modes in linear expression')
1246
+ bound = var0._coerce_number(bound)
1247
+
1248
+ # resulting stuff
1249
+ lits, weights, shift = [], {}, 0
1250
+
1251
+ # applying linearization for each term
1252
+ for coeff, var in terms:
1253
+ wmap, sh = var.linearize(coeff)
1254
+ shift += sh
1255
+
1256
+ for lit, wght in wmap.items():
1257
+ weights[lit] = weights.get(lit, 0) + wght
1258
+ lits.append(lit)
1259
+
1260
+ # dropping zero-weight literals,
1261
+ # as they do not affect PB reasoning
1262
+ weights = {lit: wght for lit, wght in weights.items() if wght != 0}
1263
+ lits = sorted(weights.keys())
1264
+
1265
+ return ('linear', [lits, bound + shift, weights])
1266
+
1267
+
1268
+ #
1269
+ #==============================================================================
1270
+ class AbsExpr:
1271
+ """
1272
+ Absolute-value wrapper for :class:`LinearExpr`.
1273
+ """
1274
+
1275
+ def __init__(self, expr):
1276
+ """
1277
+ Initialiser.
1278
+ """
1279
+
1280
+ if isinstance(expr, Integer):
1281
+ expr = expr.as_expr()
1282
+
1283
+ if not isinstance(expr, LinearExpr):
1284
+ raise TypeError('Absolute value requires a LinearExpr or Integer')
1285
+
1286
+ self.expr = expr
1287
+
1288
+ def __le__(self, other):
1289
+ """
1290
+ Less-than-or-equal constraint.
1291
+ """
1292
+
1293
+ return ('abs_le', self.expr, other)
1294
+
1295
+ def __ge__(self, other):
1296
+ """
1297
+ Greater-than-or-equal constraint.
1298
+ """
1299
+
1300
+ return ('abs_ge', self.expr, other)
1301
+
1302
+ def __eq__(self, other):
1303
+ """
1304
+ Equality constraint.
1305
+ """
1306
+
1307
+ return ('abs_eq', self.expr, other)
1308
+
1309
+
1310
+ #
1311
+ #==============================================================================
1312
+ class IntegerEngine(BooleanEngine):
1313
+ """
1314
+ Thin wrapper around :class:`.BooleanEngine`: converts integer
1315
+ constraints to pseudo-Boolean linear constraints and delegates
1316
+ reasoning to :class:`.BooleanEngine`.
1317
+
1318
+ This is a somewhat experimental, simple and illustrative
1319
+ implementation rather than a performance-optimized CP solver.
1320
+
1321
+ Example:
1322
+
1323
+ .. code-block:: python
1324
+
1325
+ >>> from pysat.integer import Integer, IntegerEngine
1326
+ >>> from pysat.solvers import Solver
1327
+ >>> X = Integer('X', 0, 3)
1328
+ >>> Y = Integer('Y', 0, 3)
1329
+ >>> eng = IntegerEngine([X, Y], adaptive=True)
1330
+ >>> eng.add_linear(X + Y <= 4)
1331
+ >>> eng.add_linear(X != Y)
1332
+ >>> with Solver(name='cd195') as solver:
1333
+ ... solver.connect_propagator(eng)
1334
+ ... eng.setup_observe(solver)
1335
+ ... while solver.solve():
1336
+ ... model = solver.get_model()
1337
+ ... vals = eng.decode_model(model)
1338
+ ... print('model:', {var.name: value for var, value in vals.items()})
1339
+ ... blits = eng.encode_model(vals)
1340
+ ... solver.add_clause([-l for l in blits])
1341
+ model: {'X': 1, 'Y': 0}
1342
+ model: {'X': 2, 'Y': 0}
1343
+ model: {'X': 0, 'Y': 1}
1344
+ model: {'X': 2, 'Y': 1}
1345
+ model: {'X': 1, 'Y': 2}
1346
+ model: {'X': 0, 'Y': 2}
1347
+ """
1348
+
1349
+ def __init__(self, vars=None, constraints=None, adaptive=True, vpool=None):
1350
+ """
1351
+ Constructor.
1352
+
1353
+ :param vars: :class:`.Integer` variables
1354
+ :param constraints: optional constraints to bootstrap with
1355
+ :param adaptive: enable adaptive mode in the Boolean engine
1356
+ :param vpool: optional shared IDPool for auxiliary variables
1357
+
1358
+ :type vars: list of :class:`.Integer`
1359
+ :type constraints: list of :class:`.LinearExpr`
1360
+ :type adaptive: bool
1361
+ :type vpool: :class:`.IDPool` or None
1362
+ """
1363
+
1364
+ # sets for storing encoded and attached variables
1365
+ self._venc, self._vatt = set(), set()
1366
+
1367
+ # cache of linear constraints and reified constraints
1368
+ self._lcons = []
1369
+
1370
+ # flag to determine whether we've already done setup_observe()
1371
+ self._attached = False
1372
+
1373
+ # variables, if any, and their domain clauses
1374
+ self.integers, self.clauses = vars or [], []
1375
+
1376
+ # de-duplication caches for linear and high-level constraints
1377
+ self._lcons_seen = set()
1378
+ self._constr_seen = set()
1379
+
1380
+ # variable manager
1381
+ self.vpool = vpool
1382
+
1383
+ # checking related to the vpool
1384
+ if self.integers and self.vpool is None:
1385
+ self.vpool = self.integers[0].vpool
1386
+ if self.vpool is not None:
1387
+ for var in self.integers:
1388
+ if var.vpool is not self.vpool:
1389
+ raise ValueError('All variables must share the same IDPool')
1390
+
1391
+ # processing all the variables known so far
1392
+ for var in self.integers:
1393
+ self._encode_domain(var)
1394
+
1395
+ # BooleanEngine's constructor
1396
+ super().__init__(bootstrap_with=[], adaptive=adaptive)
1397
+
1398
+ # adding constraints, if any
1399
+ for constr in constraints or []:
1400
+ self.add_linear(constr)
1401
+
1402
+ def add_var(self, var):
1403
+ """
1404
+ Add a new integer variable and its domain clauses.
1405
+
1406
+ :param var: integer variable
1407
+ :type var: :class:`Integer`
1408
+ """
1409
+
1410
+ # do nothing if the variable is already known
1411
+ if var in self._venc:
1412
+ return
1413
+
1414
+ # first, we need to make sure all variables share the same IDPool
1415
+ if self.vpool is None:
1416
+ self.vpool = var.vpool
1417
+ elif var.vpool is not self.vpool:
1418
+ raise ValueError('All variables must share the same IDPool')
1419
+
1420
+ if var not in self.integers:
1421
+ self.integers.append(var)
1422
+
1423
+ self._encode_domain(var)
1424
+
1425
+ def _is_new_constr(self, key, selv):
1426
+ """
1427
+ Check if a logical constraint with a given selector is new.
1428
+ """
1429
+
1430
+ if (key, selv) in self._constr_seen:
1431
+ return False
1432
+
1433
+ self._constr_seen.add((key, selv))
1434
+ return True
1435
+
1436
+ def _encode_domain(self, var):
1437
+ """
1438
+ Cache and add domain clauses for a variable once per engine.
1439
+ """
1440
+
1441
+ # do nothing if this variable is already encoded
1442
+ if var in self._venc:
1443
+ return
1444
+
1445
+ # marking the variable as encoded
1446
+ self._venc.add(var)
1447
+
1448
+ # encoding process
1449
+ for cl in var.domain_clauses():
1450
+ self._add_clause(cl)
1451
+
1452
+ # if we are attached, pass the variable and its clauses to the solver
1453
+ if self._attached:
1454
+ self._vatt.add(var)
1455
+
1456
+ def _add_clause(self, clause, selv=None):
1457
+ """
1458
+ Add a clause to the internal list. If a solver is attached, add
1459
+ the clause to the solver as well. When the selector variable
1460
+ ``selv`` is provided, add its negation to guard the clause's
1461
+ enabled / disabled state.
1462
+ """
1463
+
1464
+ # a selector is given: relaxing the clause first
1465
+ if selv is not None:
1466
+ clause = [-selv] + clause
1467
+
1468
+ # adding the clause to the internal clause list
1469
+ self.clauses.append(clause)
1470
+
1471
+ # if we are attached to a solver, pass the clause to the solver too
1472
+ if self._attached:
1473
+ self.solver.add_clause(clause)
1474
+
1475
+ def _expr_key(self, expr):
1476
+ """
1477
+ Build a stable key for a :class:`LinearExpr` object.
1478
+ """
1479
+
1480
+ items = tuple(sorted(expr.terms.items(), key=lambda it: id(it[0])))
1481
+ return ('lin_expr', items, expr.const, expr.numeric)
1482
+
1483
+ def _pair_key(self, left, right):
1484
+ """
1485
+ Build an order-insensitive key for a pair of objects.
1486
+ """
1487
+
1488
+ return (left, right) if id(left) <= id(right) else (right, left)
1489
+
1490
+ def _linear_key(self, constr):
1491
+ """
1492
+ Build a key and normalized components for a
1493
+ :class:`.BooleanEngine` linear constraint. Not to be confused with
1494
+ :meth:`_expr_key`.
1495
+ """
1496
+
1497
+ lits, bound = constr[1][0], constr[1][1]
1498
+ weights = {} if len(constr[1]) == 2 else constr[1][2]
1499
+
1500
+ if not weights:
1501
+ weights = {l: 1 for l in lits}
1502
+ if any(l not in weights for l in lits):
1503
+ raise ValueError('PB encoding requires explicit weights for all literals')
1504
+
1505
+ ltup = tuple(sorted(lits, key=id))
1506
+ wtup = tuple(weights[lit] for lit in ltup)
1507
+
1508
+ return (ltup, wtup, bound, lits, weights)
1509
+
1510
+ def _guard_linear(self, selv, parts):
1511
+ """
1512
+ Add a linear constraint guarded by a given selector literal.
1513
+
1514
+ The resulting encoding enforces ``selv -> linear(parts)``. If the
1515
+ underlying linear is unsatisfiable, ``selv`` is forced to false.
1516
+ """
1517
+
1518
+ ltup, wtup, bound, lits, weights = parts
1519
+ max_sum = sum(wtup)
1520
+ if bound >= max_sum:
1521
+ return
1522
+
1523
+ if bound < 0:
1524
+ self._add_clause([-selv])
1525
+ return
1526
+
1527
+ wght = max_sum - bound
1528
+ rweights = dict(weights)
1529
+ rweights[selv] = wght
1530
+ rlits = list(lits) + [selv]
1531
+
1532
+ rcons = ('linear', [rlits, bound + wght, rweights])
1533
+ self._record_linear(rcons)
1534
+ self._add_constraint_internal(rcons)
1535
+
1536
+ def _get_selv(self, key):
1537
+ """
1538
+ Allocate (or reuse) a selector literal for a given key in the
1539
+ current variable pool.
1540
+ """
1541
+
1542
+ if self.vpool is None:
1543
+ self.vpool = Formula.export_vpool()
1544
+
1545
+ return self.vpool.id(key)
1546
+
1547
+ def _singleton_var(self, expr):
1548
+ """
1549
+ Return the underlying variable for a plain expression ``x``.
1550
+ """
1551
+
1552
+ if isinstance(expr, Integer):
1553
+ return expr
1554
+
1555
+ if isinstance(expr, LinearExpr):
1556
+ return expr._singleton()
1557
+
1558
+ def _add_ge(self, var, value, selv=None):
1559
+ """
1560
+ Add a lower-bound literal if it is non-trivial.
1561
+ """
1562
+
1563
+ if value <= var.lb:
1564
+ return True
1565
+ if value > var.ub:
1566
+ self._add_clause([], selv=selv)
1567
+ return False
1568
+
1569
+ self._add_clause([var.ge(value)], selv=selv)
1570
+ return True
1571
+
1572
+ def _add_le(self, var, value, selv=None):
1573
+ """
1574
+ Add an upper-bound literal if it is non-trivial.
1575
+ """
1576
+
1577
+ if value >= var.ub:
1578
+ return True
1579
+ if value < var.lb:
1580
+ self._add_clause([], selv=selv)
1581
+ return False
1582
+
1583
+ self._add_clause([var.le(value)], selv=selv)
1584
+ return True
1585
+
1586
+ def _add_val_constr(self, var, pred, selv=None):
1587
+ """
1588
+ Check a constraint on a variable's value for triviality.
1589
+ Returns the list of allowed values if the constraint is non-trivial.
1590
+ """
1591
+
1592
+ vals = [v for v in var.domain if pred(v)]
1593
+
1594
+ if not vals:
1595
+ self._add_clause([], selv=selv)
1596
+ return None
1597
+
1598
+ if len(vals) == len(var.domain):
1599
+ return None
1600
+
1601
+ return vals
1602
+
1603
+ def _add_abs_var_le(self, var, bound, selv=None):
1604
+ """
1605
+ Add ``|var| <= bound`` using domain literals only.
1606
+ """
1607
+
1608
+ vals = self._add_val_constr(var, lambda v: abs(v) <= bound, selv=selv)
1609
+ if vals:
1610
+ if var.encoding == 'direct':
1611
+ for v in var.domain:
1612
+ if abs(v) > bound:
1613
+ self._add_clause([-var.equals(v)], selv=selv)
1614
+ else:
1615
+ self._add_ge(var, vals[0], selv=selv)
1616
+ self._add_le(var, vals[-1], selv=selv)
1617
+
1618
+ def _add_abs_var_ge(self, var, bound, selv=None):
1619
+ """
1620
+ Add ``|var| >= bound`` using domain literals only.
1621
+ """
1622
+
1623
+ vals = self._add_val_constr(var, lambda v: abs(v) >= bound, selv=selv)
1624
+ if not vals:
1625
+ return
1626
+
1627
+ if var.encoding == 'direct':
1628
+ for v in var.domain:
1629
+ if abs(v) < bound:
1630
+ self._add_clause([-var.equals(v)], selv=selv)
1631
+ else:
1632
+ # for order encoding, we exclude a central interval
1633
+ # of values v such that abs(v) < bound
1634
+ excluded = [v for v in var.domain if abs(v) < bound]
1635
+ lo, hi = excluded[0], excluded[-1]
1636
+
1637
+ cl = []
1638
+ if lo > var.lb:
1639
+ cl.append(var.le(lo - 1))
1640
+ if hi < var.ub:
1641
+ cl.append(var.ge(hi + 1))
1642
+
1643
+ self._add_clause(cl, selv=selv)
1644
+
1645
+ def _add_abs_var_eq(self, var, bound, selv=None):
1646
+ """
1647
+ Add ``|var| == bound`` using domain literals only.
1648
+ """
1649
+
1650
+ vals = self._add_val_constr(var, lambda v: abs(v) == bound, selv=selv)
1651
+ if vals:
1652
+ if var.encoding == 'direct':
1653
+ self._add_clause([var.equals(v) for v in vals], selv=selv)
1654
+ else:
1655
+ lo, hi = vals[0], vals[-1]
1656
+ self._add_ge(var, lo, selv=selv)
1657
+ self._add_le(var, hi, selv=selv)
1658
+ if lo < hi:
1659
+ self._add_clause([var.le(lo), var.ge(hi)], selv=selv)
1660
+
1661
+ def _add_lin_list(self, conss, key, reif):
1662
+ """
1663
+ Add a list of linear constraints (conjunction). Returns a selector.
1664
+ """
1665
+
1666
+ selv = self._get_selv(key) if reif else None
1667
+
1668
+ if self._is_new_constr(key, selv):
1669
+ for c in conss:
1670
+ if reif:
1671
+ self._guard_linear(selv, self._linear_key(c))
1672
+ elif self._record_linear(c):
1673
+ self._add_constraint_internal(c)
1674
+
1675
+ return selv
1676
+
1677
+ def add_linear(self, constr, reified=False):
1678
+ """
1679
+ Add a constraint or a bundle of constraints to the engine.
1680
+
1681
+ Accepts a single :class:`.LinearExpr`-style constraint (e.g. ``X +
1682
+ Y <= 4``), a list of such constraints (from ``==``), or a
1683
+ :class:`.BooleanEngine`'s ``'linear'`` constraint tuple (or a list
1684
+ of them). The ``!=`` operator is supported between Integer
1685
+ variables and between linear expressions (via auxiliary Boolean
1686
+ variables).
1687
+
1688
+ Note that in the case of variants of :class:`.LinearExpr`
1689
+ constraints, those are internally transformed into
1690
+ :class:`.BooleanEngine`-style PB constraint first.
1691
+
1692
+ :param constr: constraint or list of constraints
1693
+ :type constr: LinearExpr | tuple | list
1694
+ :param reified: if ``True``, return a selector literal for the constraint
1695
+ :type reified: bool
1696
+ """
1697
+
1698
+ # dispatching to specialized methods if needed
1699
+ if isinstance(constr, tuple) and constr:
1700
+ tag = constr[0]
1701
+
1702
+ if tag == 'abs_le':
1703
+ return self.add_abs_le(constr[1], constr[2], reified=reified)
1704
+ if tag == 'abs_ge':
1705
+ return self.add_abs_ge(constr[1], constr[2], reified=reified)
1706
+ if tag == 'abs_eq':
1707
+ return self.add_abs_eq(constr[1], constr[2], reified=reified)
1708
+ if tag == 'eq_vars':
1709
+ return self.add_equal(constr[1], constr[2], reified=reified)
1710
+ if tag == 'ne':
1711
+ return self.add_not_equal(constr[1], constr[2], reified=reified)
1712
+
1713
+ # equality or inequality of linear expressions
1714
+ if tag in ('lin_eq', 'ne_linear'):
1715
+ if tag == 'lin_eq':
1716
+ lkeys = tuple(sorted(self._linear_key(c)[:3] for c in constr[1]))
1717
+ return self._add_lin_list(constr[1], ('lin_eq', lkeys), reified)
1718
+
1719
+ # ne_linear: (left - right <= -1) OR (left - right >= 1)
1720
+ diff = constr[1] - constr[2]
1721
+ c1, c2 = diff <= -1, diff >= 1
1722
+ lks = sorted([self._linear_key(c1)[:3], self._linear_key(c2)[:3]])
1723
+
1724
+ key = ('lin_ne', tuple(lks))
1725
+ selv = self._get_selv(key) if reified else None
1726
+
1727
+ if self._is_new_constr(key, selv):
1728
+ s1, s2 = self._reify_linear(c1), self._reify_linear(c2)
1729
+ if s1 is not None and s2 is not None:
1730
+ self._add_clause([s1, s2], selv=selv)
1731
+ return selv
1732
+
1733
+ # atomic linear constraint
1734
+ if tag == 'linear':
1735
+ if reified:
1736
+ return self._reify_linear(constr)
1737
+ if self._record_linear(constr):
1738
+ self._add_constraint_internal(constr)
1739
+ return
1740
+
1741
+ # a collection of atomic linear constraints
1742
+ if isinstance(constr, (list, tuple)) and constr:
1743
+ lkeys = tuple(sorted(self._linear_key(c)[:3] for c in constr))
1744
+ return self._add_lin_list(constr, ('lin_le_list', lkeys), reified)
1745
+
1746
+ if not constr:
1747
+ return
1748
+
1749
+ raise TypeError('Unsupported constraint type; expected linear tuple or list of tuples')
1750
+
1751
+ def _record_linear(self, constr, parts=None):
1752
+ """
1753
+ Store a linear constraint for later clausification.
1754
+ """
1755
+
1756
+ if not (isinstance(constr, (list, tuple)) and constr and constr[0] == 'linear'):
1757
+ return False
1758
+
1759
+ if parts is None:
1760
+ parts = self._linear_key(constr)
1761
+
1762
+ key = parts[:3]
1763
+ if key in self._lcons_seen:
1764
+ return False
1765
+
1766
+ self._lcons_seen.add(key)
1767
+ self._lcons.append(constr)
1768
+ return True
1769
+
1770
+ def add_alldifferent(self, vars, reified=False):
1771
+ """
1772
+ Add an AllDifferent constraint over variables (for direct/coupled
1773
+ encoding only).
1774
+
1775
+ :param vars: variables to be all-different
1776
+ :type vars: list(:class:`Integer`)
1777
+ :param reified: if ``True``, return a selector literal for the constraint
1778
+ :type reified: bool
1779
+ """
1780
+
1781
+ # no variables -> nothing to do
1782
+ if not vars:
1783
+ return
1784
+
1785
+ # de-duplication
1786
+ key = ('int_alldiff', tuple(sorted(vars, key=id)))
1787
+ selv = self._get_selv(key) if reified else None
1788
+ if not self._is_new_constr(key, selv):
1789
+ return selv
1790
+
1791
+ # every variable must share the same IDPool,
1792
+ # and its domainencoding must not be 'order'
1793
+ vpool = vars[0].vpool
1794
+ for var in vars:
1795
+ self.add_var(var)
1796
+ if var.vpool is not vpool:
1797
+ raise ValueError('AllDifferent variables must share the same IDPool')
1798
+ if var.encoding not in ('direct', 'coupled'):
1799
+ raise ValueError('AllDifferent requires direct or coupled encoding')
1800
+
1801
+ # we care only about the intersection of all domains
1802
+ values = set()
1803
+ for var in vars:
1804
+ values.update(var.domain)
1805
+
1806
+ # modelling all-differents are a bunch of AtMost1 constraints
1807
+ for v in values:
1808
+ lits = [var.equals(v) for var in vars if v in var.domain]
1809
+ if len(lits) > 1:
1810
+ if reified:
1811
+ t = self._reify_linear(('linear', [lits, 1]))
1812
+ if t is not None:
1813
+ self._add_clause([t], selv=selv)
1814
+ else:
1815
+ self.add_linear(('linear', [lits, 1]))
1816
+
1817
+ return selv
1818
+
1819
+ def _reify_linear(self, constr):
1820
+ """
1821
+ Reify a :class:`.BooleanEngine`'s ``'linear'`` constraint.
1822
+
1823
+ Returns a selector literal ``b`` such that ``b -> constraint``.
1824
+ Returns ``None`` if the constraint is tautological.
1825
+ """
1826
+
1827
+ # we can verify only BooleanEngine's linears
1828
+ if not (isinstance(constr, tuple) and constr and constr[0] == 'linear'):
1829
+ raise TypeError('Expected a BooleanEngine linear constraint')
1830
+
1831
+ parts = self._linear_key(constr)
1832
+ ltup, wtup, bound, _, _ = parts
1833
+ max_sum = sum(wtup)
1834
+ if bound >= max_sum:
1835
+ # the constraint is trivially satisfiable
1836
+ return
1837
+
1838
+ rkey = ('lin_le', (ltup, wtup, bound))
1839
+ if self.vpool is None:
1840
+ self.vpool = Formula.export_vpool()
1841
+
1842
+ if rkey in self.vpool.obj2id:
1843
+ return self.vpool.id(rkey)
1844
+
1845
+ selv = self._get_selv(rkey)
1846
+ self._guard_linear(selv, parts)
1847
+ return selv
1848
+
1849
+ def _reify_lin_conj(self, conss, key):
1850
+ """
1851
+ Reify a conjunction of linear constraints under a single
1852
+ selector.
1853
+
1854
+ The returned selector literal ``b`` satisfies
1855
+ ``b -> (c1 and c2 and ...)`` for the constraints in ``conss``.
1856
+ Tautological conjuncts are ignored; if every conjunct is
1857
+ tautological, the method returns ``None``. If any conjunct is
1858
+ unsatisfiable, the returned selector is forced to false.
1859
+ """
1860
+
1861
+ parts_list = []
1862
+ for constr in conss:
1863
+ if not (isinstance(constr, tuple) and constr and constr[0] == 'linear'):
1864
+ raise TypeError('Expected a list of BooleanEngine linear constraints')
1865
+
1866
+ parts = self._linear_key(constr)
1867
+ _, wtup, bound, _, _ = parts
1868
+ max_sum = sum(wtup)
1869
+
1870
+ if bound < 0:
1871
+ selv = self._get_selv(key)
1872
+ if self._is_new_constr(key, selv):
1873
+ self._add_clause([-selv])
1874
+ return selv
1875
+
1876
+ if bound < max_sum:
1877
+ parts_list.append(parts)
1878
+
1879
+ if not parts_list:
1880
+ return
1881
+
1882
+ selv = self._get_selv(key)
1883
+ if self._is_new_constr(key, selv):
1884
+ for parts in parts_list:
1885
+ self._guard_linear(selv, parts)
1886
+
1887
+ return selv
1888
+
1889
+ def add_not_equal(self, left, right, reified=False):
1890
+ """
1891
+ Add a not-equal constraint on two :class:`Integer` variables: left
1892
+ != right (for direct/coupled encodings only).
1893
+
1894
+ :param left: left variable
1895
+ :param right: right variable
1896
+ :type left: :class:`Integer`
1897
+ :type right: :class:`Integer`
1898
+ :param reified: if ``True``, return a selector literal ``b`` such
1899
+ that ``b -> (left != right)``
1900
+ :type reified: bool
1901
+ """
1902
+
1903
+ # common-sense checks first
1904
+ if left.vpool is not right.vpool:
1905
+ raise ValueError('NotEqual variables must share the same IDPool')
1906
+
1907
+ self.add_var(left)
1908
+ self.add_var(right)
1909
+
1910
+ if left.encoding not in ('direct', 'coupled'):
1911
+ raise ValueError('NotEqual requires direct or coupled encoding on left')
1912
+ if right.encoding not in ('direct', 'coupled'):
1913
+ raise ValueError('NotEqual requires direct or coupled encoding on right')
1914
+
1915
+ # de-duplication
1916
+ key = ('int_ne', self._pair_key(left, right))
1917
+ selv = self._get_selv(key) if reified else None
1918
+ if not self._is_new_constr(key, selv):
1919
+ return selv
1920
+
1921
+ # adding CNF clauses for value inequality
1922
+ values = set(left.domain).intersection(right.domain)
1923
+ for v in values:
1924
+ cl = [-left.equals(v), -right.equals(v)]
1925
+ self._add_clause(cl, selv=selv)
1926
+
1927
+ return selv
1928
+
1929
+ def _add_eq_dir(self, v1, v2, lb, ub, selv):
1930
+ """
1931
+ Enforce equality of two variables for direct-direct encoding.
1932
+ """
1933
+
1934
+ for v in range(lb, ub + 1):
1935
+ self._add_clause([-v1.equals(v), +v2.equals(v)], selv=selv)
1936
+ self._add_clause([+v1.equals(v), -v2.equals(v)], selv=selv)
1937
+
1938
+ def _add_eq_ord(self, v1, v2, lb, ub, selv):
1939
+ """
1940
+ Enforce equality of two variables for order-order encoding.
1941
+ """
1942
+
1943
+ for k in range(lb + 1, ub + 1):
1944
+ self._add_clause([-v1.ge(k), +v2.ge(k)], selv=selv)
1945
+ self._add_clause([+v1.ge(k), -v2.ge(k)], selv=selv)
1946
+
1947
+ def _add_eq_mix(self, dvar, ovar, lb, ub, selv):
1948
+ """
1949
+ Enforce equality of two variables for mixed direct-order encoding.
1950
+ Done by channeling the two encodings
1951
+ """
1952
+
1953
+ for v in range(lb, ub + 1):
1954
+ deq = dvar.equals(v)
1955
+ final = [deq] # third clause to add
1956
+
1957
+ # 1. dvar=v -> ovar>=v
1958
+ if v > ovar.lb:
1959
+ self._add_clause([-deq, ovar.ge(v)], selv=selv)
1960
+ final.append(-ovar.ge(v))
1961
+
1962
+ # 2. dvar=v -> NOT ovar>=v+1
1963
+ if v < ovar.ub:
1964
+ self._add_clause([-deq, -ovar.ge(v + 1)], selv=selv)
1965
+ final.append(ovar.ge(v + 1))
1966
+
1967
+ # 3. (ovar>=v AND NOT ovar>=v+1) -> dvar=v
1968
+ self._add_clause(final, selv=selv)
1969
+
1970
+ def add_equal(self, left, right, reified=False):
1971
+ """
1972
+ Add an equality constraint on two :class:`Integer` variables.
1973
+
1974
+ Uses direct/order encodings when available, falling back to a
1975
+ linear equality otherwise.
1976
+
1977
+ The intersection of domains is enforced upfront to prune
1978
+ impossible values early. If the intersection is empty, the
1979
+ constraint is unsatisfiable.
1980
+
1981
+ If ``reified`` is ``True``, a selector literal ``b`` is returned
1982
+ such that ``b -> (left == right)``.
1983
+ """
1984
+
1985
+ if left.vpool is not right.vpool:
1986
+ raise ValueError('Equal variables must share the same IDPool')
1987
+
1988
+ self.add_var(left)
1989
+ self.add_var(right)
1990
+
1991
+ # de-duplication
1992
+ key = ('int_eq', self._pair_key(left, right))
1993
+ selv = self._get_selv(key) if reified else None
1994
+ if not self._is_new_constr(key, selv):
1995
+ return selv
1996
+
1997
+ lb, ub = max(left.lb, right.lb), min(left.ub, right.ub)
1998
+ if lb > ub:
1999
+ self._add_clause([], selv=selv)
2000
+ return selv
2001
+
2002
+ # domain pruning for both variables
2003
+ for var in (left, right):
2004
+ if var.encoding in ('direct'):
2005
+ for v in var.domain:
2006
+ if v < lb or v > ub:
2007
+ self._add_clause([-var.equals(v)], selv=selv)
2008
+
2009
+ if var.encoding in ('order', 'coupled'):
2010
+ if lb > var.lb:
2011
+ self._add_clause([var.ge(lb)], selv=selv)
2012
+ if ub < var.ub and ub + 1 <= var.ub:
2013
+ self._add_clause([-var.ge(ub + 1)], selv=selv)
2014
+
2015
+ # strategy 1: direct vs direct
2016
+ if left.encoding in ('direct', 'coupled') and right.encoding in ('direct', 'coupled'):
2017
+ self._add_eq_dir(left, right, lb, ub, selv)
2018
+
2019
+ # strategy 2: order vs order
2020
+ elif left.encoding in ('order', 'coupled') and right.encoding in ('order', 'coupled'):
2021
+ self._add_eq_ord(left, right, lb, ub, selv)
2022
+
2023
+ # strategy 3: mixed (direct vs order)
2024
+ else:
2025
+ if left.encoding in ('direct', 'coupled'):
2026
+ self._add_eq_mix(left, right, lb, ub, selv)
2027
+ else:
2028
+ self._add_eq_mix(right, left, lb, ub, selv)
2029
+
2030
+ return selv
2031
+
2032
+ def _prep_abs(self, expr, bound, reif, tag):
2033
+ """
2034
+ Normalize and de-duplicate an absolute-value constraint.
2035
+
2036
+ Returns a triplet ``(expr, selv, var)`` where ``expr`` is the
2037
+ normalized :class:`LinearExpr`, ``selv`` is the top-level selector
2038
+ when ``reif`` is requested (or ``None`` otherwise), and ``var`` is
2039
+ the underlying :class:`Integer` when ``expr`` is a plain variable.
2040
+ If the constraint was already added before, ``expr`` is returned
2041
+ as ``None`` and the existing selector (if any) is reused.
2042
+ """
2043
+
2044
+ if isinstance(expr, Integer):
2045
+ expr = expr.as_expr()
2046
+
2047
+ if not isinstance(expr, LinearExpr):
2048
+ raise TypeError('Absolute value requires a LinearExpr or Integer')
2049
+
2050
+ # de-duplication
2051
+ key = (tag, self._expr_key(expr), bound)
2052
+ selv = self._get_selv(key) if reif else None
2053
+ if not self._is_new_constr(key, selv):
2054
+ return None, selv, None
2055
+
2056
+ return expr, selv, self._singleton_var(expr)
2057
+
2058
+ def add_abs_le(self, expr, bound, reified=False):
2059
+ """
2060
+ Add an absolute value constraint: ``|expr| <= bound``.
2061
+
2062
+ For a plain variable expression, a direct domain-based encoding is
2063
+ used. Otherwise the constraint is encoded as the conjunction
2064
+ ``expr <= bound`` and ``-expr <= bound``. If ``reified`` is
2065
+ ``True``, the returned selector literal ``b`` satisfies
2066
+ ``b -> (|expr| <= bound)``.
2067
+
2068
+ :param expr: linear expression or Integer
2069
+ :param bound: numeric bound
2070
+ :param reified: if ``True``, return a selector literal for the constraint
2071
+ :type reified: bool
2072
+ """
2073
+
2074
+ expr, selv, var = self._prep_abs(expr, bound, reified, 'abs_le')
2075
+ if expr is None:
2076
+ return selv
2077
+
2078
+ if bound < 0:
2079
+ # unsatisfiable
2080
+ self._add_clause([], selv=selv)
2081
+ return selv
2082
+
2083
+ # plain-variable constraints can be handled natively
2084
+ if var is not None:
2085
+ self.add_var(var)
2086
+ self._add_abs_var_le(var, bound, selv=selv)
2087
+ return selv
2088
+
2089
+ conss = [expr <= bound, (-expr) <= bound]
2090
+
2091
+ if reified:
2092
+ key = ('abs_le_and', self._expr_key(expr), bound)
2093
+ branch = self._reify_lin_conj(conss, key)
2094
+ if branch is not None:
2095
+ self._add_clause([branch], selv=selv)
2096
+ return selv
2097
+
2098
+ for constr in conss:
2099
+ self.add_linear(constr)
2100
+ return
2101
+
2102
+ def add_abs_eq(self, expr, bound, reified=False):
2103
+ """
2104
+ Add an absolute value constraint: ``|expr| == bound``.
2105
+
2106
+ For a plain variable expression, a direct domain-based encoding is
2107
+ used. Otherwise the constraint is encoded as the disjunction
2108
+ ``(expr == bound) or (expr == -bound)``, where each equality is
2109
+ represented internally as a conjunction of two linear
2110
+ inequalities. If ``reified`` is ``True``, the returned selector
2111
+ literal ``b`` satisfies ``b -> (|expr| == bound)``.
2112
+
2113
+ :param expr: linear expression or Integer
2114
+ :param bound: numeric bound
2115
+ :param reified: if ``True``, return a selector literal for the constraint
2116
+ :type reified: bool
2117
+ """
2118
+
2119
+ expr, selv, var = self._prep_abs(expr, bound, reified, 'abs_eq')
2120
+ if expr is None:
2121
+ return selv
2122
+
2123
+ if bound < 0:
2124
+ # unsatisfiable
2125
+ self._add_clause([], selv=selv)
2126
+ return selv
2127
+
2128
+ # plain-variable constraints can be handled natively
2129
+ if var is not None:
2130
+ self.add_var(var)
2131
+ self._add_abs_var_eq(var, bound, selv=selv)
2132
+ return selv
2133
+
2134
+ pos = [expr <= +bound, expr >= +bound]
2135
+ neg = [expr <= -bound, expr >= -bound]
2136
+
2137
+ # |expr| == bound <=> (expr == bound) OR (expr == -bound)
2138
+ s1 = self._reify_lin_conj(pos, ('abs_eq_pos', self._expr_key(expr), bound))
2139
+ s2 = self._reify_lin_conj(neg, ('abs_eq_neg', self._expr_key(expr), bound))
2140
+ if s1 is None or s2 is None:
2141
+ return selv
2142
+
2143
+ self._add_clause([s1] if s1 == s2 else [s1, s2], selv=selv)
2144
+
2145
+ return selv
2146
+
2147
+ def add_abs_ge(self, expr, bound, reified=False):
2148
+ """
2149
+ Add an absolute value constraint: ``|expr| >= bound``.
2150
+
2151
+ For a plain variable expression, a direct domain-based encoding is
2152
+ used. Otherwise the constraint is encoded as the disjunction
2153
+ ``(expr >= bound) or (expr <= -bound)``. If ``reified`` is
2154
+ ``True``, the returned selector literal ``b`` satisfies
2155
+ ``b -> (|expr| >= bound)``.
2156
+
2157
+ :param expr: linear expression or Integer
2158
+ :param bound: numeric bound
2159
+ :param reified: if ``True``, return a selector literal for the constraint
2160
+ :type reified: bool
2161
+ """
2162
+
2163
+ expr, selv, var = self._prep_abs(expr, bound, reified, 'abs_ge')
2164
+ if expr is None:
2165
+ return selv
2166
+
2167
+ if bound <= 0:
2168
+ # |expr| >= 0 is always true
2169
+ return selv
2170
+
2171
+ # plain-variable constraints can be handled natively
2172
+ if var is not None:
2173
+ self.add_var(var)
2174
+ self._add_abs_var_ge(var, bound, selv=selv)
2175
+ return selv
2176
+
2177
+ # |expr| >= bound <=> (expr >= bound) OR (expr <= -bound)
2178
+ s1 = self._reify_lin_conj([expr >= +bound], ('abs_ge_pos', self._expr_key(expr), bound))
2179
+ s2 = self._reify_lin_conj([expr <= -bound], ('abs_ge_neg', self._expr_key(expr), bound))
2180
+ if s1 is None or s2 is None:
2181
+ # one side is tautological => disjunction always true
2182
+ return selv
2183
+
2184
+ self._add_clause([s1, s2], selv=selv)
2185
+ return selv
2186
+
2187
+ def clausify(self, cardenc=CardEncType.seqcounter, pbenc=PBEncType.best,
2188
+ intenc=PBEncType.best):
2189
+ """
2190
+ Clausify all constraints and return a CNF encoding.
2191
+
2192
+ Linear constraints over :class:`Integer` variables are encoded by
2193
+ Pindakaas as *integer* linear constraints, using the encoding
2194
+ ``intenc``. The literals of each :class:`Integer` are passed to
2195
+ Pindakaas as an integer variable, which lets it use integer
2196
+ encodings, e.g. the mixed radix encoding.
2197
+
2198
+ The remaining linear constraints, and all of them if ``intenc``
2199
+ is ``None``, are converted using the user-specified cardinality
2200
+ and pseudo-Boolean encoding. The former are utilized for
2201
+ unweighted constraints, while the latter are utilized for
2202
+ weighted constraints.
2203
+
2204
+ Note that parameters ``cardenc``, ``pbenc`` and ``intenc``
2205
+ default to :attr:`.card.EncType.seqcounter`,
2206
+ :class:`pysat.pb.EncType.best` and
2207
+ :class:`pysat.pb.EncType.best`, respectively. The *native*
2208
+ encodings produce no clauses and raise :class:`ValueError`.
2209
+
2210
+ :param cardenc: cardinality encoding for unweighted constraints
2211
+ :param pbenc: PB encoding for weighted constraints
2212
+ :param intenc: encoding for integer linear constraints, or ``None``
2213
+ :type cardenc: int
2214
+ :type pbenc: int
2215
+ :type intenc: int or None
2216
+
2217
+ :rtype: :class:`pysat.formula.CNF`
2218
+ """
2219
+
2220
+ # native constraints are not clauses, and would be lost
2221
+ if cardenc == CardEncType.native or PBEncType.native in (pbenc, intenc):
2222
+ raise ValueError('Native encodings cannot be clausified')
2223
+ if intenc is not None and intenc not in PBEncType._to_encoder:
2224
+ raise NoSuchEncodingError(intenc)
2225
+
2226
+ # we are going to store the resulting formula here
2227
+ res = CNF()
2228
+
2229
+ # first, checking if there is work to do
2230
+ if not self.integers:
2231
+ if self._lcons:
2232
+ # it should not happen that we've got constraints but no vars
2233
+ raise ValueError('Cannot clausify linear constraints without integer variables')
2234
+
2235
+ # returning an empty formula
2236
+ return res
2237
+
2238
+ # making sure all variables share the same IDPool
2239
+ vpool = self.integers[0].vpool
2240
+ for i in range(1, len(self.integers)):
2241
+ if self.integers[i].vpool is not vpool:
2242
+ raise ValueError('All variables must share the same IDPool')
2243
+
2244
+ # include domain clauses and any extra CNF clauses
2245
+ # (e.g. reified disjunctions) already cached in the engine
2246
+ res.extend(self.clauses)
2247
+
2248
+ # separating integer linear constraints from Boolean ones
2249
+ lcons, iconss = self._lcons, []
2250
+ if intenc is not None:
2251
+ lcons = []
2252
+ for cs in self._lcons:
2253
+ icons = self._integer_terms(cs, vpool)
2254
+ if icons:
2255
+ iconss.append(icons)
2256
+ else:
2257
+ lcons.append(cs)
2258
+
2259
+ # the main part is to encode all linear constraints
2260
+ for cs in lcons:
2261
+ lits, bound = cs[1][0], cs[1][1]
2262
+ weights = {} if len(cs[1]) == 2 else cs[1][2]
2263
+
2264
+ if not weights:
2265
+ wght = 1
2266
+ else:
2267
+ wvals = list(weights.values())
2268
+ if len(set(wvals)) == 1:
2269
+ wght = wvals[0]
2270
+ else:
2271
+ wght = None
2272
+
2273
+ # we have a common weight wght => cardinality constraint
2274
+ if wght is not None:
2275
+ if isinstance(bound, Decimal):
2276
+ # Decimal's // rounds towards zero, not down
2277
+ new_bound = math.floor(bound / wght)
2278
+ else:
2279
+ new_bound = int(math.floor(bound // wght))
2280
+
2281
+ if new_bound < 0:
2282
+ # the bound is negative => constraint is unsatisfiable
2283
+ res.append([])
2284
+ continue
2285
+ if new_bound >= len(lits):
2286
+ # the constraint is unfalsifiable
2287
+ continue
2288
+
2289
+ enc = CardEnc.atmost(lits=lits, bound=new_bound, vpool=vpool,
2290
+ encoding=cardenc)
2291
+ res.extend(enc.clauses)
2292
+ continue
2293
+
2294
+ # the weights are not all the same => PB constraint
2295
+ # first checking the bound and literal weights for being int-like
2296
+ if not self._intlike(bound):
2297
+ raise ValueError('PB encoding requires integer bound')
2298
+ if any(l not in weights for l in lits):
2299
+ raise ValueError('PB encoding requires explicit weights for all literals')
2300
+ wlist = [weights[l] for l in lits]
2301
+ if not all(self._intlike(w) for w in wlist):
2302
+ raise ValueError('PB encoding requires integer weights')
2303
+
2304
+ bound_int = int(bound) if not isinstance(bound, Decimal) else int(bound)
2305
+ wlist = [int(w) if not isinstance(w, Decimal) else int(w)
2306
+ for w in wlist]
2307
+
2308
+ if bound_int < 0:
2309
+ # the weights are non-negative => unsatisfiable
2310
+ res.append([])
2311
+ continue
2312
+
2313
+ enc = PBEnc.atmost(lits=lits, weights=wlist, bound=bound_int,
2314
+ vpool=vpool, encoding=pbenc)
2315
+ res.extend(enc.clauses)
2316
+
2317
+ # integer linear constraints go last, as they share one encoding
2318
+ if iconss:
2319
+ res.extend(self._clausify_integer(iconss, vpool, intenc))
2320
+
2321
+ return res
2322
+
2323
+ def _integer_terms(self, cs, vpool):
2324
+ """
2325
+ Recover the integer structure of a linear constraint. The
2326
+ literals of each :class:`Integer` recognized by
2327
+ :meth:`Integer._linear_term` become a single integer term.
2328
+ Return a triple ``(terms, rest, bound)``, where ``terms`` is a
2329
+ list of ``(var, kind, coeff)``, ``rest`` is a list of the
2330
+ remaining weighted literals, and the constraint is ``terms +
2331
+ rest <= bound``. Return ``None`` if there are no integer terms or
2332
+ if the numbers are not integers.
2333
+ """
2334
+
2335
+ _, _, bound, lits, weights = self._linear_key(cs)
2336
+
2337
+ if not self._intlike(bound) or not all(self._intlike(weights[l]) for l in lits):
2338
+ return None
2339
+
2340
+ # grouping the literals by their integer variable and kind
2341
+ groups, rest = defaultdict(dict), []
2342
+ for l in lits:
2343
+ obj, wght = vpool.obj(abs(l)), int(weights[l])
2344
+ if isinstance(obj, tuple) and len(obj) == 3 and isinstance(obj[0], Integer):
2345
+ group = groups[obj[0], obj[1]]
2346
+ if obj[2] not in group:
2347
+ group[obj[2]] = (l, wght)
2348
+ continue
2349
+
2350
+ # a Boolean literal, or the negation of one already in its group
2351
+ rest.append((l, wght))
2352
+
2353
+ terms, bound = [], int(bound)
2354
+ for (var, kind), wmap in groups.items():
2355
+ term = var._linear_term(kind, wmap)
2356
+ if term is None:
2357
+ rest.extend(wmap.values())
2358
+ else:
2359
+ coeff, const = term
2360
+ if coeff:
2361
+ terms.append((var, kind, coeff))
2362
+ bound -= const
2363
+
2364
+ return (terms, rest, bound) if terms else None
2365
+
2366
+ def _clausify_integer(self, iconss, vpool, intenc):
2367
+ """
2368
+ Encode integer linear constraints, as returned by
2369
+ :meth:`_integer_terms`, with Pindakaas. Return the list of
2370
+ clauses.
2371
+ """
2372
+
2373
+ encoder, options = PBEncType._to_encoder[intenc]
2374
+ Lit = pindakaas.Lit.from_raw
2375
+
2376
+ # new variables must also avoid literals that are not in the pool
2377
+ top_id = max([vpool.top] + [abs(l) for _, rest, _ in iconss for l, _ in rest])
2378
+ db = _pindakaas_db(top_id)
2379
+ ivars = {} # integer variables, created lazily
2380
+
2381
+ def ivar(var, kind):
2382
+ if (var, kind) not in ivars:
2383
+ if kind == 'ge':
2384
+ lits = [var.ge(k) for k in range(var.lb + 1, var.ub + 1)]
2385
+ make = db.int_var_from_order_literals
2386
+ else:
2387
+ lits = [var.equals(v) for v in var.domain]
2388
+ make = db.int_var_from_direct_literals
2389
+ ivars[var, kind] = make(var.domain, [Lit(l) for l in lits])
2390
+ return ivars[var, kind]
2391
+
2392
+ try:
2393
+ for terms, rest, bound in iconss:
2394
+ lhs = 0
2395
+ for var, kind, coeff in terms:
2396
+ lhs += coeff * ivar(var, kind)
2397
+ for l, w in rest:
2398
+ lhs += w * Lit(l)
2399
+
2400
+ db.add_encoding(lhs <= bound, encoder, **options)
2401
+ except pindakaas.Unsatisfiable:
2402
+ return [[]]
2403
+
2404
+ return _pindakaas_cnf(db, top_id, vpool=vpool).clauses
2405
+
2406
+ @staticmethod
2407
+ def _intlike(value):
2408
+ """
2409
+ Checking if the value can be converted to integer.
2410
+ """
2411
+
2412
+ # yes, nothing to do (plain ints first, as the ABC check is slow)
2413
+ if type(value) is int or isinstance(value, numbers.Integral):
2414
+ return True
2415
+
2416
+ # yes, it's a convertable decimal
2417
+ if isinstance(value, Decimal):
2418
+ return value == value.to_integral_value()
2419
+
2420
+ # yes, it's a convertable float
2421
+ if isinstance(value, float):
2422
+ return value.is_integer()
2423
+
2424
+ # no, we can't convert it
2425
+ return False
2426
+
2427
+ def _add_constraint_internal(self, constr):
2428
+ """
2429
+ Add a constraint regardless of whether the solver is connected.
2430
+ """
2431
+
2432
+ if self.solver is None:
2433
+ # the solver does not exist yet, we can't do setup_observe()
2434
+ cs = self._add_constraint(constr)
2435
+ cs.register_watched(self.wlst)
2436
+
2437
+ for lit in cs.lits:
2438
+ var = abs(lit)
2439
+ if var not in self.vset:
2440
+ self.vset.add(var)
2441
+ self.value[var] = None
2442
+ self.level[var] = None
2443
+ self.fixed[var] = False
2444
+
2445
+ # keep the observed variable list in sync before setup_observe()
2446
+ self.vars = sorted(self.vset)
2447
+
2448
+ cs.attach_values(self.value)
2449
+ else:
2450
+ # all good, resorting to BooleanEngine's add_constraint()
2451
+ super().add_constraint(constr)
2452
+
2453
+ @staticmethod
2454
+ def pb_linear_leq(terms, bound):
2455
+ """
2456
+ Convenience wrapper for building a PySAT ``'linear'`` constraint
2457
+ (cardinality / pseudo-Boolean) from integer terms. Calls
2458
+ :meth:`.LinearExpr.pb_linear_leq`.
2459
+ """
2460
+
2461
+ return LinearExpr.pb_linear_leq(terms, bound)
2462
+
2463
+ def setup_observe(self, solver):
2464
+ """
2465
+ Inform the solver about observed variables and add domain clauses.
2466
+
2467
+ :param solver: SAT solver instance
2468
+ :type solver: :class:`pysat.solvers.Cadical195`
2469
+ """
2470
+
2471
+ # first, calling BooleanEngine's setup_observe()
2472
+ # this is required for registering more Boolean self.vars variables
2473
+ # if we managed to create them since last time the method was called
2474
+ # note that self.vars is populated by collecting
2475
+ # variables appearing in linear constraints only
2476
+ super().setup_observe(solver)
2477
+
2478
+ # next, adding domain clauses for all the known variables
2479
+ # setup_observe() is to be called once but more
2480
+ # variables can be added later through add_var()
2481
+ if not self._attached:
2482
+ for var in self._venc:
2483
+ if var in self._vatt:
2484
+ continue
2485
+
2486
+ for cl in var.domain_clauses():
2487
+ self.solver.add_clause(cl)
2488
+
2489
+ self._vatt.add(var)
2490
+
2491
+ for cl in self.clauses:
2492
+ self.solver.add_clause(cl)
2493
+
2494
+ # flagging that setup_observe() has been called
2495
+ self._attached = True
2496
+
2497
+ def decode_model(self, model, vars=None):
2498
+ """
2499
+ Given a SAT model (list of int identifiers), return a mapping
2500
+ ``{Integer: integer_value}`` for the engine's integer variables.
2501
+
2502
+ If ``vars`` is provided, decode only those variables.
2503
+
2504
+ Example:
2505
+
2506
+ .. code-block:: python
2507
+
2508
+ >>> model = solver.get_model()
2509
+ >>> values = eng.decode_model(model, vars=[X, Y])
2510
+ >>> print(values)
2511
+
2512
+ :param model: SAT model (list of literals)
2513
+ :param vars: subset of variables to decode (optional)
2514
+ :type model: list(int)
2515
+ :type vars: list(:class:`Integer`) or None
2516
+
2517
+ :rtype: dict
2518
+ """
2519
+
2520
+ res = {}
2521
+
2522
+ # resorting to variables' own ability to derive their values
2523
+ for var in vars if vars is not None else self.integers:
2524
+ res[var] = var.decode(model)
2525
+
2526
+ return res
2527
+
2528
+ def encode_model(self, model, vars=None):
2529
+ """
2530
+ Given an integer-level model, return a list of true literals.
2531
+
2532
+ ``model`` can be a dict mapping :class:`Integer` objects
2533
+ (preferred) or variable names to values, or a list/tuple of values
2534
+ corresponding to ``vars`` (or ``self.integers`` if ``vars`` is
2535
+ None).
2536
+
2537
+ This is the inverse of :meth:`.decode_model` at the integer level.
2538
+
2539
+ :param model: integer-level model
2540
+ :param vars: variable order for list/tuple models (optional)
2541
+ :type model: dict or list
2542
+ :type vars: list(:class:`Integer`) or None
2543
+
2544
+ :rtype: list(int)
2545
+ """
2546
+
2547
+ # if no target is given, using all variables
2548
+ if vars is None:
2549
+ vars = self.integers
2550
+
2551
+ # preparing the values list
2552
+ if isinstance(model, dict):
2553
+ values = []
2554
+ for var in vars:
2555
+ if var in model:
2556
+ values.append(model[var])
2557
+ else:
2558
+ values.append(model.get(var.name))
2559
+ else:
2560
+ values = list(model)
2561
+ if len(values) != len(vars):
2562
+ raise ValueError('Model length does not match variables list')
2563
+
2564
+ # the actual translation is done by variables themselves
2565
+ lits = []
2566
+ for var, value in zip(vars, values):
2567
+ lits.extend(var.encode(value))
2568
+
2569
+ return lits
2570
+
2571
+ def decode(self, lits, vars=None):
2572
+ """
2573
+ Given a list of assigned Boolean literals, return a mapping
2574
+ ``{Integer: integer_value}`` for those variables whose value is
2575
+ fixed by the assignment alone. Unlike :meth:`.decode_model`, this
2576
+ method does not require a full model and only looks at literals
2577
+ present in ``lits``. Also note that the method does not require
2578
+ the input literals to be in any particular order.
2579
+
2580
+ If multiple inconsistent *equality* literals are present for the
2581
+ same variable, the value is reported as a list of all observed
2582
+ values, which (if an issue) can be used to find the culprit in the
2583
+ encoding. For order-based literals, if the inferred bounds fix the
2584
+ value, the value is reported as an integer; otherwise, the value
2585
+ is reported as the list of all values within the inferred bounds.
2586
+ If order-based bounds are contradictory (lower bound exceeds upper
2587
+ bound), the value is reported as an empty list.
2588
+
2589
+ Example:
2590
+
2591
+ .. code-block:: python
2592
+
2593
+ >>> st, props = solver.propagate(assumptions=clues)
2594
+ >>> vals = eng.decode(props)
2595
+ >>> # vals[i] is an int if the value is fixed,
2596
+ >>> # a list if conflicting if multiple values are present,
2597
+ >>> # or [] if contradictory bounds were derived.
2598
+
2599
+ :param lits: literals in the assignment
2600
+ :param vars: subset of variables to decode (optional)
2601
+ :type lits: iterable(int)
2602
+ :type vars: list(:class:`Integer`) or None
2603
+
2604
+ :rtype: dict
2605
+ """
2606
+
2607
+ # don't anything about any variables!
2608
+ if self.vpool is None:
2609
+ return {}
2610
+
2611
+ # optional filtering by a subset of variables
2612
+ allowed = None if vars is None else set(vars)
2613
+
2614
+ # per-variable state tracked only when we see relevant literals
2615
+ eq_true, lbounds, ubounds = {}, {}, {}
2616
+
2617
+ seen = set()
2618
+ for lit in lits:
2619
+ if lit == 0:
2620
+ # literal '0' makes no sense
2621
+ raise ValueError('Literal 0 is not allowed')
2622
+
2623
+ if lit not in seen:
2624
+ seen.add(lit)
2625
+
2626
+ # getting the object given a literal
2627
+ obj = self.vpool.obj(abs(lit))
2628
+
2629
+ # we expect a tuple of (var, 'kind', value)
2630
+ if not obj or not isinstance(obj, tuple) or len(obj) != 3:
2631
+ # not a variable we know!
2632
+ continue
2633
+
2634
+ var, kind, value = obj
2635
+ if not isinstance(var, Integer) or kind not in ('eq', 'ge'):
2636
+ # still not our variable!
2637
+ continue
2638
+
2639
+ # checking if the variable is among those we are interested in
2640
+ if allowed is not None and var not in allowed:
2641
+ continue
2642
+
2643
+ if kind == 'eq':
2644
+ if lit > 0:
2645
+ eq_true.setdefault(var, set()).add(value)
2646
+ else: # kind == 'ge'
2647
+ if var not in lbounds:
2648
+ lbounds[var] = var.lb
2649
+ ubounds[var] = var.ub
2650
+
2651
+ # refining the bounds if we can
2652
+ if lit > 0:
2653
+ if value > lbounds[var]:
2654
+ lbounds[var] = value
2655
+ else:
2656
+ bound = value - 1
2657
+ if bound < ubounds[var]:
2658
+ ubounds[var] = bound
2659
+
2660
+ # now, we are going to collate the data and put it here
2661
+ res = {}
2662
+
2663
+ # direct/coupled encodings expose equality literals
2664
+ for var, values in eq_true.items():
2665
+ if len(values) == 1:
2666
+ # taking the concrete value we found
2667
+ res[var] = next(iter(values))
2668
+ else:
2669
+ # multiple values are found
2670
+ res[var] = sorted(values)
2671
+
2672
+ # order/coupled encodings may be fixed by bounds alone
2673
+ for var in lbounds:
2674
+ if var in res:
2675
+ # well, we already have a concrete value for this variable
2676
+ continue
2677
+
2678
+ if lbounds[var] == ubounds[var]:
2679
+ # concrete value defined from the bounds
2680
+ res[var] = lbounds[var]
2681
+
2682
+ elif lbounds[var] < ubounds[var]:
2683
+ # the bounds represent a valid range
2684
+ res[var] = list(range(lbounds[var], ubounds[var] + 1))
2685
+ else:
2686
+ # conflicting bounds detected; report as an empty list
2687
+ # to signal contradiction without raising
2688
+ res[var] = []
2689
+
2690
+ return res
2691
+
2692
+ def decode_assignment(self, lits, vars=None):
2693
+ """
2694
+ Backward-compatible alias for :meth:`decode`.
2695
+ """
2696
+
2697
+ return self.decode(lits, vars=vars)
2698
+
2699
+
2700
+ # example usage
2701
+ #==============================================================================
2702
+ if __name__ == '__main__':
2703
+ # example 1: satisfiable, mixed encodings, all constraint types, enumeration
2704
+ from decimal import Decimal
2705
+ from pysat.solvers import Solver
2706
+
2707
+ X = Integer('X', 0, 3) # direct
2708
+ Y = Integer('Y', 0, 3, encoding='direct') # direct
2709
+ Z = Integer('Z', 0, 3, encoding='coupled') # coupled
2710
+ D = Integer('D', 0, 3, numeric='decimal') # decimal
2711
+ E = Integer('E', 0, 3, numeric='decimal') # decimal
2712
+
2713
+ eng = IntegerEngine([X, Y, Z, D, E], adaptive=False)
2714
+
2715
+ # linear constraints
2716
+ eng.add_linear(X + Y <= 4 - Z)
2717
+ eng.add_linear(X + Y >= 3)
2718
+ eng.add_linear(Z - X <= 1)
2719
+ eng.add_linear(X - Z <= 1)
2720
+
2721
+ # not equal and AllDifferent (direct/coupled only)
2722
+ eng.add_linear(X != Z)
2723
+ eng.add_alldifferent([X, Y, Z])
2724
+
2725
+ # decimal linear constraint
2726
+ eng.add_linear(Decimal('1') * D + Decimal('2') * E <= Decimal('3'))
2727
+ eng.add_linear(Decimal('1') * D + Decimal('1') * E == Decimal('2'))
2728
+
2729
+ # model enumeration using integer-level blocking
2730
+ with Solver(name='cd19', bootstrap_with=None) as solver:
2731
+ solver.connect_propagator(eng)
2732
+ eng.setup_observe(solver)
2733
+ while solver.solve():
2734
+ model = solver.get_model()
2735
+ vals = eng.decode_model(model)
2736
+ print('ENG example:', {var.name: value for var, value in vals.items()})
2737
+ blits = eng.encode_model(vals)
2738
+ solver.add_clause([-l for l in blits])
2739
+
2740
+ print('')
2741
+
2742
+ # example 2: satisfiable, clausified (same model set as example 1)
2743
+ cnf = eng.clausify()
2744
+ with Solver(name='g3', bootstrap_with=cnf.clauses) as solver:
2745
+ while solver.solve():
2746
+ model = solver.get_model()
2747
+ vals = eng.decode_model(model)
2748
+ print('CNF example:', {var.name: value for var, value in vals.items()})
2749
+ blits = eng.encode_model(vals)
2750
+ solver.add_clause([-l for l in blits])
2751
+
2752
+ # example 3: should be unsatisfiable
2753
+ A = Integer('A', 0, 2)
2754
+ B = Integer('B', 0, 2)
2755
+ eng2 = IntegerEngine([A, B], adaptive=False)
2756
+ eng2.add_linear(A + B <= 1)
2757
+ eng2.add_linear(A - B >= 2)
2758
+
2759
+ with Solver(name='cd19', bootstrap_with=None) as solver:
2760
+ solver.connect_propagator(eng2)
2761
+ eng2.setup_observe(solver)
2762
+ sat = solver.solve()
2763
+ if sat:
2764
+ model = solver.get_model()
2765
+ vals = eng2.decode_model(model)
2766
+ print('UNSAT example (wrong!):', {var.name: value for var, value in vals.items()})
2767
+ else:
2768
+ print('UNSAT example: UNSAT')