python-sat 1.9.dev6__cp313-cp313-pyemscripten_2025_0_wasm32.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of python-sat might be problematic. Click here for more details.

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