python-sat 1.8.dev28__cp310-cp310-macosx_11_0_arm64.whl → 1.8.dev30__cp310-cp310-macosx_11_0_arm64.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 (24) hide show
  1. pycard.cpython-310-darwin.so +0 -0
  2. pysat/__init__.py +2 -2
  3. pysat/card.py +1 -1
  4. pysat/integer.py +1945 -0
  5. pysolvers.cpython-310-darwin.so +0 -0
  6. {python_sat-1.8.dev28.dist-info → python_sat-1.8.dev30.dist-info}/METADATA +1 -1
  7. {python_sat-1.8.dev28.dist-info → python_sat-1.8.dev30.dist-info}/RECORD +24 -23
  8. {python_sat-1.8.dev28.data → python_sat-1.8.dev30.data}/scripts/approxmc.py +0 -0
  9. {python_sat-1.8.dev28.data → python_sat-1.8.dev30.data}/scripts/bbscan.py +0 -0
  10. {python_sat-1.8.dev28.data → python_sat-1.8.dev30.data}/scripts/bica.py +0 -0
  11. {python_sat-1.8.dev28.data → python_sat-1.8.dev30.data}/scripts/fm.py +0 -0
  12. {python_sat-1.8.dev28.data → python_sat-1.8.dev30.data}/scripts/genhard.py +0 -0
  13. {python_sat-1.8.dev28.data → python_sat-1.8.dev30.data}/scripts/lbx.py +0 -0
  14. {python_sat-1.8.dev28.data → python_sat-1.8.dev30.data}/scripts/lsu.py +0 -0
  15. {python_sat-1.8.dev28.data → python_sat-1.8.dev30.data}/scripts/mcsls.py +0 -0
  16. {python_sat-1.8.dev28.data → python_sat-1.8.dev30.data}/scripts/models.py +0 -0
  17. {python_sat-1.8.dev28.data → python_sat-1.8.dev30.data}/scripts/musx.py +0 -0
  18. {python_sat-1.8.dev28.data → python_sat-1.8.dev30.data}/scripts/optux.py +0 -0
  19. {python_sat-1.8.dev28.data → python_sat-1.8.dev30.data}/scripts/primer.py +0 -0
  20. {python_sat-1.8.dev28.data → python_sat-1.8.dev30.data}/scripts/rc2.py +0 -0
  21. {python_sat-1.8.dev28.data → python_sat-1.8.dev30.data}/scripts/unigen.py +0 -0
  22. {python_sat-1.8.dev28.dist-info → python_sat-1.8.dev30.dist-info}/WHEEL +0 -0
  23. {python_sat-1.8.dev28.dist-info → python_sat-1.8.dev30.dist-info}/licenses/LICENSE.txt +0 -0
  24. {python_sat-1.8.dev28.dist-info → python_sat-1.8.dev30.dist-info}/top_level.txt +0 -0
pysat/integer.py ADDED
@@ -0,0 +1,1945 @@
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, and comparisons ``<=``,
142
+ ``>=``, ``<``, ``>``, ``==``, ``!=``. Notably **unsupported** are
143
+ variable-variable multiplication, division, modulus, absolute values, and
144
+ reification.
145
+
146
+ Integer variables can be declared to operate with either ``'float'`` or
147
+ ``'decimal'`` coefficients, which is handled by the ``numeric``
148
+ parameter. Integer coefficients can be mixed with either mode. However,
149
+ float and decimal coefficients must not be mixed in the same constraints,
150
+ hence one cannot mix integers with float and decimal numeric modes.
151
+
152
+ Below is a compact 4x4 Sudoku example (digits 1..4). It demonstrates
153
+ direct-encoded integer variables, AllDifferent constraints, and the use
154
+ of the propagator. This is intentionally lightweight and meant to be
155
+ illustrative.
156
+
157
+ .. code-block:: python
158
+
159
+ >>> from pysat.integer import Integer, IntegerEngine
160
+ >>> from pysat.solvers import Cadical195
161
+ >>>
162
+ >>> # 4x4 Sudoku (2x2 blocks), 0 denotes empty
163
+ >>> givens = [
164
+ ... [0, 0, 2, 0],
165
+ ... [0, 0, 0, 3],
166
+ ... [0, 1, 0, 0],
167
+ ... [4, 0, 0, 0],
168
+ ... ]
169
+ >>>
170
+ >>> X = [[Integer(f'x{r}{c}', 1, 4, encoding='direct')
171
+ ... for c in range(4)] for r in range(4)]
172
+ >>>
173
+ >>> eng = IntegerEngine([v for row in X for v in row], adaptive=False)
174
+ >>>
175
+ >>> # rows and columns
176
+ >>> for r in range(4):
177
+ ... eng.add_alldifferent(X[r])
178
+ >>> for c in range(4):
179
+ ... eng.add_alldifferent([X[r][c] for r in range(4)])
180
+ >>>
181
+ >>> # 2x2 blocks
182
+ >>> for br in (0, 2):
183
+ ... for bc in (0, 2):
184
+ ... block = [X[r][c] for r in range(br, br + 2)
185
+ ... for c in range(bc, bc + 2)]
186
+ ... eng.add_alldifferent(block)
187
+ >>>
188
+ >>> # clues
189
+ >>> for r in range(4):
190
+ ... for c in range(4):
191
+ ... if givens[r][c]:
192
+ ... eng.add_linear(X[r][c] == givens[r][c])
193
+ >>>
194
+ >>> with Cadical195() as solver:
195
+ ... solver.connect_propagator(eng)
196
+ ... eng.setup_observe(solver)
197
+ ... if solver.solve():
198
+ ... model = solver.get_model()
199
+ ... values = eng.decode_model(model)
200
+ ... for r in range(4):
201
+ ... row = [values[X[r][c]] for c in range(4)]
202
+ ... print(row)
203
+ [1, 3, 2, 4]
204
+ [2, 4, 1, 3]
205
+ [3, 1, 4, 2]
206
+ [4, 2, 3, 1]
207
+
208
+ ==============
209
+ Module details
210
+ ==============
211
+ """
212
+
213
+ #
214
+ #==============================================================================
215
+ from collections import defaultdict
216
+ from decimal import Decimal
217
+ import math
218
+ import numbers
219
+
220
+ from pysat.card import CardEnc, EncType as CardEncType
221
+ from pysat.engines import BooleanEngine
222
+ from pysat.formula import CNF, Formula
223
+ from pysat.pb import PBEnc, EncType as PBEncType
224
+
225
+
226
+ # importable components
227
+ #==============================================================================
228
+ __all__ = ['Integer', 'Int', 'LinearExpr', 'IntegerEngine']
229
+
230
+
231
+ #
232
+ #==============================================================================
233
+ class Integer:
234
+ """
235
+ Finite-domain integer variable with a configurable CNF encoding.
236
+
237
+ Supported encodings are:
238
+
239
+ - ``'direct'``: one-hot / value encoding
240
+ - ``'order'``: order / bound encoding
241
+ - ``'coupled'``: both encodings with channeling clauses
242
+
243
+ Direct encoding uses Boolean literals :math:`d_i \\triangleq (x=i)`
244
+ for each value :math:`i` in the domain and a cardinality encoding to
245
+ enforce that exactly one value is chosen. Order encoding introduces
246
+ Boolean literals :math:`o_i \\triangleq (x\\ge i)` and adds
247
+ monotonicity implications between them. Coupled encoding mixes the two
248
+ types of literals but **omits** explicit one-hot constraints for
249
+ :math:`d_i` since the order chain plus channeling already imply
250
+ exactly one value.
251
+
252
+ :param name: variable name
253
+ :param lb: lower bound
254
+ :param ub: upper bound
255
+ :param encoding: domain encoding ('direct', 'order', or 'coupled')
256
+ :param card_enc: cardinality encoding for the direct encoding (ignored for coupled encoding)
257
+ :param vpool: external variable pool (optional)
258
+ :param numeric: numeric mode ('float' or 'decimal')
259
+
260
+ :type name: str
261
+ :type lb: int
262
+ :type ub: int
263
+ :type encoding: str
264
+ :type card_enc: int
265
+ :type vpool: :class:`pysat.formula.IDPool` or None
266
+ :type numeric: str
267
+
268
+ Example:
269
+
270
+ .. code-block:: python
271
+
272
+ >>> from pysat.integer import Integer
273
+ >>> X = Integer('X', 0, 3, encoding='direct')
274
+ >>> Y = Integer('Y', 0, 3, encoding='order')
275
+ >>> c1 = X + Y <= 4
276
+ >>> c2 = X - Y >= 1
277
+ >>> print(c1)
278
+ ('linear', [[2, 3, 4, 5, 6, 7], 4, {2: 1, 3: 2, 4: 3, 5: 1, 6: 1, 7: 1}])
279
+ >>> print(c2)
280
+ ('linear', [[1, 2, 3, 5, 6, 7], 2, {1: 3, 2: 2, 3: 1, 5: 1, 6: 1, 7: 1}])
281
+
282
+ .. note::
283
+
284
+ Integer coefficients attached to the variables can be mixed with
285
+ either numeric mode. Mixing floats with decimals raises
286
+ ``TypeError``.
287
+ """
288
+
289
+ def __init__(self, name, lb, ub, encoding='direct',
290
+ card_enc=CardEncType.seqcounter, vpool=None,
291
+ numeric='float'):
292
+ """
293
+ Constructor.
294
+ """
295
+
296
+ # some common-sense assumptions
297
+ assert lb <= ub, 'Lower bound cannot be greater than upper bound'
298
+ assert encoding in ('direct', 'order', 'coupled'), f'Unknown encoding {encoding}'
299
+ assert numeric in ('float', 'decimal'), f'Unknown numeric mode {numeric}'
300
+
301
+ self.name = name
302
+ self.lb = lb
303
+ self.ub = ub
304
+ self.domain = list(range(lb, ub + 1))
305
+ self.encoding = encoding
306
+ self.card_enc = card_enc
307
+ self.numeric = numeric
308
+ self.clauses = None
309
+
310
+ # deciding which IDPool to use
311
+ self.vpool = vpool if vpool is not None else Formula.export_vpool()
312
+
313
+ # ensure IDs are registered upfront to keep vpool stable
314
+ if encoding in ('direct', 'coupled'):
315
+ for v in self.domain:
316
+ self.vpool.id((self, 'eq', v))
317
+ if encoding in ('order', 'coupled'):
318
+ for k in range(lb + 1, ub + 1):
319
+ self.vpool.id((self, 'ge', k))
320
+
321
+ def _coerce_number(self, value):
322
+ """
323
+ Coerce a numeric (coefficient) value to the variable's numeric
324
+ mode.
325
+
326
+ Integers are accepted in both modes. Mixing floats with decimals
327
+ is not allowed and raises ``TypeError``.
328
+ """
329
+
330
+ if self.numeric == 'decimal':
331
+ if isinstance(value, Decimal):
332
+ return value
333
+ if isinstance(value, numbers.Integral):
334
+ return Decimal(value)
335
+ raise TypeError('Decimal mode does not accept non-decimal values')
336
+
337
+ if isinstance(value, Decimal):
338
+ raise TypeError('Float mode does not accept Decimal values')
339
+ if isinstance(value, numbers.Real):
340
+ return value
341
+ raise TypeError('Numeric value expected')
342
+
343
+ def equals(self, value):
344
+ """
345
+ Return the Boolean literal corresponding to :math:`x = value`.
346
+
347
+ Only available with the direct (or coupled) encoding.
348
+
349
+ :param value: domain value
350
+ :type value: int
351
+
352
+ :rtype: int
353
+ """
354
+
355
+ assert self.encoding in ('direct', 'coupled'), 'Direct encoding is disabled'
356
+ assert self.lb <= value <= self.ub, 'Value is outside the domain'
357
+ return self.vpool.id((self, 'eq', value))
358
+
359
+ def ge(self, value):
360
+ """
361
+ Return the literal representing :math:`x \\geq value`.
362
+
363
+ Only available with the order (or coupled) encoding.
364
+
365
+ :param value: lower bound value
366
+ :type value: int
367
+
368
+ :rtype: int
369
+ """
370
+
371
+ assert self.encoding in ('order', 'coupled'), 'Order encoding is disabled'
372
+ assert self.lb + 1 <= value <= self.ub, 'Value is outside the order domain'
373
+ return self.vpool.id((self, 'ge', value))
374
+
375
+ def atleast(self, value):
376
+ """
377
+ Return the literal representing :math:`x \\geq value`. Same as
378
+ :meth:`ge`.
379
+ """
380
+
381
+ return self.ge(value)
382
+
383
+ def le(self, value):
384
+ """
385
+ Return the literal representing :math:`x \\leq value`.
386
+
387
+ Only available with the order (or coupled) encoding.
388
+
389
+ :param value: upper bound value
390
+ :type value: int
391
+
392
+ :rtype: int
393
+ """
394
+
395
+ assert self.encoding in ('order', 'coupled'), 'Order encoding is disabled'
396
+ assert self.lb <= value <= self.ub - 1, 'Value is outside the order domain'
397
+
398
+ # X <= value <=> not (X >= value + 1)
399
+ return -self.vpool.id((self, 'ge', value + 1))
400
+
401
+ def atmost(self, value):
402
+ """
403
+ Return the literal representing :math:`X \\leq value`.
404
+ """
405
+
406
+ return self.le(value)
407
+
408
+ def __repr__(self):
409
+ """
410
+ String representation of the variable.
411
+ """
412
+
413
+ return f'{self.__class__.__name__}({self.name}, {self.lb}..{self.ub})'
414
+
415
+ def __str__(self):
416
+ """
417
+ String representation of the variable.
418
+ """
419
+
420
+ return f'{self.name}'
421
+
422
+ def __hash__(self):
423
+ """
424
+ Hash by identity to keep Integer usable as dict keys.
425
+ """
426
+
427
+ return object.__hash__(self)
428
+
429
+ def _as_expr(self):
430
+ """
431
+ Convert the variable to a linear expression.
432
+ """
433
+
434
+ return LinearExpr({self: 1}, 0, numeric=self.numeric)
435
+
436
+ def __add__(self, other):
437
+ """
438
+ Add a variable to another expression or numeric value.
439
+ """
440
+
441
+ return self._as_expr() + other
442
+
443
+ def __radd__(self, other):
444
+ """
445
+ Right-add for numeric values and expressions.
446
+ """
447
+
448
+ return other + self._as_expr()
449
+
450
+ def __sub__(self, other):
451
+ """
452
+ Subtract another expression or numeric value.
453
+ """
454
+
455
+ return self._as_expr() - other
456
+
457
+ def __rsub__(self, other):
458
+ """
459
+ Right-subtract for numeric values and expressions.
460
+ """
461
+
462
+ return other - self._as_expr()
463
+
464
+ def __mul__(self, other):
465
+ """
466
+ Multiply by a numeric coefficient.
467
+ """
468
+
469
+ try:
470
+ coeff = self._coerce_number(other)
471
+ except TypeError:
472
+ return NotImplemented
473
+ return LinearExpr({self: coeff}, 0, numeric=self.numeric)
474
+
475
+ def __rmul__(self, other):
476
+ """
477
+ Right-multiply by a numeric coefficient.
478
+ """
479
+
480
+ return self.__mul__(other)
481
+
482
+ def __le__(self, other):
483
+ """
484
+ Build a <= constraint against a numeric value.
485
+ """
486
+
487
+ return self._as_expr() <= other
488
+
489
+ def __ge__(self, other):
490
+ """
491
+ Build a >= constraint against a numeric value.
492
+ """
493
+
494
+ return self._as_expr() >= other
495
+
496
+ def __lt__(self, other):
497
+ """
498
+ Build a < constraint against a numeric value.
499
+ """
500
+
501
+ return self._as_expr() < other
502
+
503
+ def __gt__(self, other):
504
+ """
505
+ Build a > constraint against a numeric value.
506
+ """
507
+
508
+ return self._as_expr() > other
509
+
510
+ def __eq__(self, other):
511
+ """
512
+ Equality comparison against a numeric value or identity check.
513
+ """
514
+
515
+ if isinstance(other, Integer):
516
+ return self is other
517
+
518
+ return self._as_expr() == other
519
+
520
+ def __ne__(self, other):
521
+ """
522
+ Inequality comparison against another Integer variable.
523
+ """
524
+
525
+ if isinstance(other, Integer):
526
+ return ('ne', self, other)
527
+ return self._as_expr().__ne__(other)
528
+
529
+ def decode(self, model):
530
+ """
531
+ Decode the integer value of the variable given a SAT model
532
+ assigning the Boolean variables encoding the domain.
533
+
534
+ :param model: SAT model (list of literals)
535
+ :type model: list(int)
536
+
537
+ :rtype: int or None
538
+ """
539
+
540
+ def is_satisfied(lit):
541
+ return model[abs(lit) - 1] == lit
542
+
543
+ if self.encoding == 'direct':
544
+ for value in self.domain:
545
+ if is_satisfied(self.vpool.id((self, 'eq', value))):
546
+ return value
547
+ return None
548
+
549
+ # for order or coupled encodings,
550
+ # applying binary search, due to monotonicity
551
+ lo, hi = self.lb + 1, self.ub
552
+ value = self.lb
553
+
554
+ while lo <= hi:
555
+ mid = (lo + hi) // 2
556
+ if is_satisfied(self.vpool.id((self, 'ge', mid))):
557
+ value = mid
558
+ lo = mid + 1
559
+ else:
560
+ hi = mid - 1
561
+
562
+ return value
563
+
564
+ def encode(self, value):
565
+ """
566
+ Encode an integer value as a list of true literals.
567
+
568
+ For direct encoding, the list contains a single Boolean literal
569
+ :math:`d_i` such that :math:`d_i \\triangleq (x=i)`.
570
+
571
+ For order encoding, the list contains at most two Boolean literals
572
+ :math:`o_i` and :math:`\\neg{o_{i+1}}` such that :math:`o_i
573
+ \\triangleq (x \\ge i)` and :math:`o_{i+1} \\triangleq (x \\ge
574
+ i+1)`. For the bound values, only one literal is used.
575
+
576
+ :param value: domain value
577
+ :type value: int or None
578
+
579
+ :rtype: list(int)
580
+ """
581
+
582
+ if value is None:
583
+ return []
584
+
585
+ if self.encoding in ('direct', 'coupled'):
586
+ return [self.equals(value)]
587
+
588
+ # order encoding: pin value with adjacent bounds
589
+ if self.lb == self.ub:
590
+ # variable has a singleton domain - no variables are created at all
591
+ return []
592
+ if value == self.lb:
593
+ return [-self.ge(self.lb + 1)]
594
+ if value == self.ub:
595
+ return [self.ge(self.ub)]
596
+ return [self.ge(value), -self.ge(value + 1)]
597
+
598
+ def domain_clauses(self):
599
+ """
600
+ Return CNF clauses encoding the variable's domain.
601
+
602
+ Example:
603
+
604
+ .. code-block:: python
605
+
606
+ >>> from pysat.integer import Integer
607
+ >>> X = Integer('X', 1, 3, encoding='direct')
608
+ >>> cnf = X.domain_clauses()
609
+ >>> print(cnf)
610
+ [[1, 2, 3], [-1, 4], [-4, 5], [-2, -4], [-2, 5], [-3, -5]]
611
+
612
+ :rtype: list(list(int))
613
+ """
614
+
615
+ if self.clauses is not None:
616
+ return self.clauses
617
+
618
+ # we are going to keep our clauses here
619
+ self.clauses = []
620
+
621
+ if self.encoding == 'direct':
622
+ lits = [self.equals(val) for val in self.domain]
623
+ cnf = CardEnc.equals(lits=lits, bound=1, vpool=self.vpool,
624
+ encoding=self.card_enc)
625
+ self.clauses.extend(cnf.clauses)
626
+
627
+ if self.encoding in ('order', 'coupled'):
628
+ for val in range(self.lb + 2, self.ub + 1):
629
+ self.clauses.append([-self.ge(val), +self.ge(val - 1)])
630
+
631
+ if self.encoding == 'coupled':
632
+ self._add_channeling()
633
+
634
+ return self.clauses
635
+
636
+ def linearize(self, coeff):
637
+ """
638
+ Return a pair (weights, shift) encoding ``coeff * X`` using
639
+ Boolean literals. The sum of weights for a satisfying assignment
640
+ equals ``coeff * X + shift``.
641
+
642
+ The shift is the **negative of the minimum value** of ``coeff *
643
+ X`` over the variable's domain. This normalization ensures all
644
+ weights are non-negative (required by :mod:`pysat.pb` and
645
+ :class:`.BooleanEngine`) while preserving equivalence by adjusting
646
+ the bound by the same constant. When the minimum term is 0 (e.g.,
647
+ domain includes 0 with a non-negative coefficient), the shift is 0
648
+ and no adjustment occurs. For order encoding this corresponds to
649
+ ``-coeff * lb`` when ``coeff >= 0`` and ``-coeff * ub`` when
650
+ ``coeff < 0``, derived from the threshold identity for ``X``.
651
+
652
+ For direct encoding, this is simply a per-value weight map. For
653
+ example, if :math:`x \\in \\{0, 1, 2\\}` and :math:`2x` is
654
+ requested, then literals for values 0, 1, 2 get weights 0, 2, 4,
655
+ respectively. For order encoding, we use the identity :math:`x =
656
+ lb + \\sum_{k=lb+1}^{ub} (x \\geq k)`.
657
+
658
+ :param coeff: coefficient
659
+ :type coeff: int, float, or Decimal
660
+
661
+ :rtype: (dict, number)
662
+
663
+ Consider an example (direct encoding, :math:`x \\in \\{0,1,2\\}`):
664
+
665
+ .. code-block:: python
666
+
667
+ >>> x = Integer('X', 0, 2, encoding='direct')
668
+ >>> wmap, shift = x.linearize(-3)
669
+ >>> print(shift)
670
+ 6
671
+ >>> # weights correspond to literals [x=0], [x=1], [x=2]
672
+ >>> print([wmap[x.equals(v)] for v in x.domain])
673
+ [6, 3, 0]
674
+
675
+ Here the minimum term is ``-3 * 2 = -6``, so the shift is ``6``,
676
+ which makes all weights in the final expression non-negative.
677
+
678
+ Consider another example, this time with a negative coefficient:
679
+
680
+ .. code-block:: python
681
+
682
+ >>> y = Integer('y', 1, 3, encoding='direct')
683
+ >>> wmap, shift = y.linearize(-4)
684
+ >>> print(shift)
685
+ 12
686
+ >>> # weights correspond to literals [y=1], [y=2], [y=3]
687
+ >>> print([wmap[y.equals(v)] for v in y.domain])
688
+ [8, 4, 0]
689
+
690
+ The minimum term is ``-4 * 3 = -12``, so the shift is ``12``.
691
+ """
692
+
693
+ # first, making sure we are using a *compatible* coefficient
694
+ coeff = self._coerce_number(coeff)
695
+
696
+ # weights map is going to be stored here
697
+ wmap = defaultdict(lambda: 0)
698
+
699
+ if self.encoding in ('order', 'coupled'):
700
+ if coeff >= 0:
701
+ shift = -coeff * self.lb
702
+ for k in range(self.lb + 1, self.ub + 1):
703
+ wmap[self.ge(k)] += +coeff
704
+ else:
705
+ shift = -coeff * self.ub
706
+ for k in range(self.lb, self.ub):
707
+ wmap[self.le(k)] += -coeff
708
+
709
+ else: # direct encoding
710
+ shift = -coeff * (self.lb if coeff >= 0 else self.ub)
711
+ for v in self.domain:
712
+ wmap[self.equals(v)] += coeff * v + shift
713
+
714
+ return wmap, shift
715
+
716
+ def _add_channeling(self):
717
+ """
718
+ Clauses linking direct and order encodings.
719
+
720
+ Let :math:`d_i` denote :math:`x=i` and :math:`o_i` denote
721
+ :math:`x \\ge i`. We add the endpoint equivalences
722
+
723
+ - :math:`d_{lb} \\leftrightarrow \\neg o_{lb+1}`
724
+ - :math:`d_{ub} \\leftrightarrow o_{ub}`
725
+
726
+ and for each interior value :math:`i` in :math:`(lb, ub)`:
727
+
728
+ - :math:`d_i \\leftrightarrow (o_i \\land \\neg o_{i+1})`.
729
+
730
+ Together with the order chain, these clauses imply that exactly
731
+ one value literal is true, i.e. such a constraint is not
732
+ explicitly added.
733
+ """
734
+
735
+ # if the domain is a singleton, we need to
736
+ # make sure the direct variable is set to true
737
+ if self.lb == self.ub:
738
+ self.clauses.append([self.equals(self.lb)])
739
+ return
740
+
741
+ # lower end-point
742
+ self.clauses.append([-self.equals(self.lb), -self.ge(self.lb + 1)])
743
+ self.clauses.append([+self.ge(self.lb + 1), +self.equals(self.lb)])
744
+
745
+ # upper end-point
746
+ self.clauses.append([-self.equals(self.ub), +self.ge(self.ub)])
747
+ self.clauses.append([-self.ge(self.ub), +self.equals(self.ub)])
748
+
749
+ # interior values
750
+ for v in range(self.lb + 1, self.ub):
751
+ self.clauses.append([-self.equals(v), +self.ge(v)])
752
+ self.clauses.append([-self.equals(v), -self.ge(v + 1)])
753
+ self.clauses.append([-self.ge(v), +self.ge(v + 1), +self.equals(v)])
754
+
755
+
756
+ #
757
+ #==============================================================================
758
+ class Int(Integer):
759
+ """
760
+ This is just an alias for the Integer class.
761
+ """
762
+
763
+ pass
764
+
765
+
766
+ #
767
+ #==============================================================================
768
+ class LinearExpr:
769
+ """
770
+ Minimal linear expression builder for :class:`Integer` with numeric
771
+ coefficients. Supports comparisons against numeric values.
772
+
773
+ The resulting comparisons return :class:`.BooleanEngine`-style
774
+ constraints (tuples) that can be passed to
775
+ :meth:`.IntegerEngine.add_linear`. Syntactic sugar supports ``+``,
776
+ ``-``, unary negation, scalar ``*`` by numeric constants, and
777
+ comparisons. Variable-variable multiplication, division, modulus,
778
+ absolute values, and reification are not supported.
779
+
780
+ Example:
781
+
782
+ .. code-block:: python
783
+
784
+ >>> from pysat.integer import Integer
785
+ >>> X = Integer('X', 0, 3)
786
+ >>> Y = Integer('Y', 0, 3, encoding='order')
787
+ >>> c1 = X + Y <= 4
788
+ >>> c2 = 2 * X - Y >= 1
789
+ >>> print(c1)
790
+ ('linear', [[2, 3, 4, 5, 6, 7], 4, {2: 1, 3: 2, 4: 3, 5: 1, 6: 1, 7: 1}])
791
+ >>> print(c2)
792
+ ('linear', [[1, 2, 3, 5, 6, 7], 5, {1: 6, 2: 4, 3: 2, 5: 1, 6: 1, 7: 1}])
793
+
794
+ .. note::
795
+
796
+ Integer constants (coefficients) can be mixed with either numeric
797
+ mode. Mixing floats with decimals raises ``TypeError``.
798
+ """
799
+
800
+ def __init__(self, terms=None, const=0, numeric='float'):
801
+ """
802
+ Constructor.
803
+ """
804
+
805
+ self.terms = terms or {}
806
+ self.const = const
807
+
808
+ # mode
809
+ self.numeric = numeric
810
+
811
+ def _check_numeric(self, numeric):
812
+ """
813
+ Check if the numeric mode is consistent with the current mode.
814
+ """
815
+
816
+ if self.numeric != numeric:
817
+ raise ValueError('Mixed numeric modes in linear expression')
818
+
819
+ def _coerce_number(self, value):
820
+ """
821
+ Convert a given value to the appropriate numeric type.
822
+ """
823
+
824
+ if self.numeric == 'decimal':
825
+ if isinstance(value, Decimal):
826
+ return value
827
+ if isinstance(value, numbers.Integral):
828
+ return Decimal(value)
829
+ raise TypeError('Decimal mode does not accept non-decimal values')
830
+
831
+ # float
832
+ if isinstance(value, Decimal):
833
+ raise TypeError('Float mode does not accept Decimal values')
834
+ if isinstance(value, numbers.Real):
835
+ return value
836
+
837
+ # well, it's neither decimal not float
838
+ raise TypeError('Numeric value expected')
839
+
840
+ def _add_term(self, var, coeff):
841
+ """
842
+ Add a term ``coeff * var`` to the expression.
843
+ """
844
+
845
+ if coeff == 0:
846
+ # nothing to add
847
+ return
848
+
849
+ # updating the coefficient of this variable
850
+ self.terms[var] = self.terms.get(var, 0) + coeff
851
+
852
+ # if we canceled the term by this update
853
+ if self.terms[var] == 0:
854
+ del self.terms[var]
855
+
856
+ def bounds(self):
857
+ """
858
+ Compute a (min, max) pair for the expression over variable domains.
859
+ """
860
+
861
+ # taking into account the constant term
862
+ minv = self.const
863
+ maxv = self.const
864
+
865
+ # computing both ming and max values
866
+ for var, coeff in self.terms.items():
867
+ if coeff >= 0:
868
+ minv += coeff * var.lb
869
+ maxv += coeff * var.ub
870
+ else:
871
+ minv += coeff * var.ub
872
+ maxv += coeff * var.lb
873
+
874
+ return minv, maxv
875
+
876
+ def __add__(self, other):
877
+ """
878
+ Add a numeric value, variable, or expression.
879
+ """
880
+
881
+ # constant numeric value
882
+ try:
883
+ value = self._coerce_number(other)
884
+ except TypeError:
885
+ value = None
886
+ if value is not None:
887
+ return LinearExpr(dict(self.terms), self.const + value,
888
+ numeric=self.numeric)
889
+
890
+ # another Integer
891
+ if isinstance(other, Integer):
892
+ self._check_numeric(other.numeric)
893
+ other = other._as_expr()
894
+
895
+ # another LinearExpr
896
+ if isinstance(other, LinearExpr):
897
+ self._check_numeric(other.numeric)
898
+ res = LinearExpr(dict(self.terms), self.const + other.const,
899
+ numeric=self.numeric)
900
+ for v, c in other.terms.items():
901
+ res._add_term(v, c)
902
+ return res
903
+
904
+ return NotImplemented
905
+
906
+ def __radd__(self, other):
907
+ """
908
+ Right-add for numeric values and expressions.
909
+ """
910
+
911
+ return self.__add__(other)
912
+
913
+ def __sub__(self, other):
914
+ """
915
+ Subtract a numeric value, variable, or expression.
916
+ """
917
+
918
+ # constant numeric value
919
+ try:
920
+ value = self._coerce_number(other)
921
+ except TypeError:
922
+ value = None
923
+ if value is not None:
924
+ return LinearExpr(dict(self.terms), self.const - value,
925
+ numeric=self.numeric)
926
+
927
+ # another Integer
928
+ if isinstance(other, Integer):
929
+ self._check_numeric(other.numeric)
930
+ other = other._as_expr()
931
+
932
+ # another LinearExpr
933
+ if isinstance(other, LinearExpr):
934
+ self._check_numeric(other.numeric)
935
+ res = LinearExpr(dict(self.terms), self.const - other.const,
936
+ numeric=self.numeric)
937
+ for v, c in other.terms.items():
938
+ res._add_term(v, -c)
939
+ return res
940
+
941
+ return NotImplemented
942
+
943
+ def __rsub__(self, other):
944
+ """
945
+ Right-subtract for numeric values and expressions.
946
+ """
947
+
948
+ return (-1) * self + other
949
+
950
+ def __mul__(self, other):
951
+ """
952
+ Multiply by a numeric coefficient.
953
+ """
954
+
955
+ try:
956
+ value = self._coerce_number(other)
957
+ except TypeError:
958
+ return NotImplemented
959
+ if value == 1:
960
+ return self
961
+
962
+ # general case of multiplying by a constant numeric value
963
+ res = LinearExpr({}, self.const * value, numeric=self.numeric)
964
+ for v, c in self.terms.items():
965
+ res.terms[v] = c * value
966
+ return res
967
+
968
+ def __rmul__(self, other):
969
+ """
970
+ Right-multiply by a numeric coefficient.
971
+ """
972
+
973
+ return self.__mul__(other)
974
+
975
+ def __neg__(self):
976
+ """
977
+ Unary negation.
978
+ """
979
+
980
+ return (-1) * self
981
+
982
+ def __le__(self, other):
983
+ """
984
+ Build a <= constraint against a numeric value.
985
+ """
986
+
987
+ if isinstance(other, Integer):
988
+ self._check_numeric(other.numeric)
989
+ return (self - other) <= 0
990
+
991
+ if isinstance(other, LinearExpr):
992
+ self._check_numeric(other.numeric)
993
+ return (self - other) <= 0
994
+ try:
995
+ bound = self._coerce_number(other)
996
+ except TypeError:
997
+ return NotImplemented
998
+
999
+ terms = [(c, v) for v, c in self.terms.items()]
1000
+ return LinearExpr.pb_linear_leq(terms, bound - self.const)
1001
+
1002
+ def __ge__(self, other):
1003
+ """
1004
+ Build a >= constraint against a numeric value.
1005
+ """
1006
+
1007
+ if isinstance(other, Integer):
1008
+ self._check_numeric(other.numeric)
1009
+ return (self - other) >= 0
1010
+
1011
+ if isinstance(other, LinearExpr):
1012
+ self._check_numeric(other.numeric)
1013
+ return (self - other) >= 0
1014
+ try:
1015
+ bound = self._coerce_number(other)
1016
+ except TypeError:
1017
+ return NotImplemented
1018
+
1019
+ return (-self) <= (-bound)
1020
+
1021
+ def __lt__(self, other):
1022
+ """
1023
+ Build a < constraint against a numeric value.
1024
+ """
1025
+
1026
+ if isinstance(other, Integer):
1027
+ self._check_numeric(other.numeric)
1028
+ return (self - other) <= -1
1029
+
1030
+ if isinstance(other, LinearExpr):
1031
+ self._check_numeric(other.numeric)
1032
+ return (self - other) <= -1
1033
+ try:
1034
+ bound = self._coerce_number(other)
1035
+ except TypeError:
1036
+ return NotImplemented
1037
+
1038
+ if self.numeric == 'decimal':
1039
+ return self <= bound.next_minus()
1040
+
1041
+ return self <= math.nextafter(float(bound), float('-inf'))
1042
+
1043
+ def __gt__(self, other):
1044
+ """
1045
+ Build a > constraint against a numeric value.
1046
+ """
1047
+
1048
+ if isinstance(other, Integer):
1049
+ self._check_numeric(other.numeric)
1050
+ return (self - other) >= 1
1051
+
1052
+ if isinstance(other, LinearExpr):
1053
+ self._check_numeric(other.numeric)
1054
+ return (self - other) >= 1
1055
+ try:
1056
+ bound = self._coerce_number(other)
1057
+ except TypeError:
1058
+ return NotImplemented
1059
+
1060
+ if self.numeric == 'decimal':
1061
+ return self >= bound.next_plus()
1062
+
1063
+ return self >= math.nextafter(float(bound), float('inf'))
1064
+
1065
+ def __eq__(self, other):
1066
+ """
1067
+ Build an == constraint against a numeric value.
1068
+
1069
+ Returns a list with two inequalities.
1070
+ """
1071
+
1072
+ try:
1073
+ bound = self._coerce_number(other)
1074
+ except TypeError:
1075
+ return NotImplemented
1076
+
1077
+ return [self <= bound, self >= bound]
1078
+
1079
+ def __ne__(self, other):
1080
+ """
1081
+ Build a != constraint against another expression or numeric value.
1082
+ """
1083
+
1084
+ # other is an integer variable
1085
+ if isinstance(other, Integer):
1086
+ self._check_numeric(other.numeric)
1087
+ other = other._as_expr()
1088
+
1089
+ # other is a linear expression
1090
+ if isinstance(other, LinearExpr):
1091
+ self._check_numeric(other.numeric)
1092
+ return ('ne_linear', self, other)
1093
+
1094
+ # other is a constant number
1095
+ try:
1096
+ bound = self._coerce_number(other)
1097
+ except TypeError:
1098
+ return NotImplemented
1099
+
1100
+ return ('ne_linear', self, LinearExpr({}, bound, numeric=self.numeric))
1101
+
1102
+ @staticmethod
1103
+ def pb_linear_leq(terms, bound):
1104
+ """
1105
+ Build a PySAT ``'linear'`` constraint over Boolean variables
1106
+ (cardinality / pseudo-Boolean) representing:
1107
+
1108
+ sum_i coeff_i * X_i <= bound
1109
+
1110
+ where the left-hand side of the constraint signifies a list of
1111
+ pairs ``(coeff_i, X_i)`` with ``coeff_i`` being a number and
1112
+ ``X_i`` being :class:`Integer` and ``bound`` is a number.
1113
+
1114
+ :param terms: terms of the left-hand side
1115
+ :param bound: right-hand side
1116
+
1117
+ :type terms: list of pairs ``(coeff, Integer)``
1118
+ :type bound: number
1119
+
1120
+ Example:
1121
+
1122
+ .. code-block:: python
1123
+
1124
+ >>> from pysat.integer import Integer, LinearExpr
1125
+ >>> X = Integer('X', 0, 2)
1126
+ >>> Y = Integer('Y', 0, 2, encoding='order')
1127
+ >>> c = LinearExpr.pb_linear_leq([(1, X), (2, Y)], 2)
1128
+ >>> print(c)
1129
+ ('linear', [[2, 3, 4, 5], 2, {2: 1, 3: 2, 4: 2, 5: 2}])
1130
+
1131
+ :rtype: tuple
1132
+ """
1133
+
1134
+ # do nothing if there are no terms
1135
+ terms = terms or []
1136
+
1137
+ if terms:
1138
+ # checking all the numeric types
1139
+ var0 = terms[0][1]
1140
+ numeric = var0.numeric
1141
+ for _, var in terms:
1142
+ if var.numeric != numeric:
1143
+ raise ValueError('Mixed numeric modes in linear expression')
1144
+ bound = var0._coerce_number(bound)
1145
+
1146
+ # resulting stuff
1147
+ lits, weights, shift = [], {}, 0
1148
+
1149
+ # applying linearization for each term
1150
+ for coeff, var in terms:
1151
+ wmap, sh = var.linearize(coeff)
1152
+ shift += sh
1153
+
1154
+ for lit, wght in wmap.items():
1155
+ weights[lit] = weights.get(lit, 0) + wght
1156
+ lits.append(lit)
1157
+
1158
+ # dropping zero-weight literals,
1159
+ # as they do not affect PB reasoning
1160
+ weights = {lit: wght for lit, wght in weights.items() if wght != 0}
1161
+ lits = sorted(weights.keys())
1162
+
1163
+ return ('linear', [lits, bound + shift, weights])
1164
+
1165
+
1166
+ #
1167
+ #==============================================================================
1168
+ class IntegerEngine(BooleanEngine):
1169
+ """
1170
+ Thin wrapper around :class:`.BooleanEngine`: converts integer
1171
+ constraints to pseudo-Boolean linear constraints and delegates
1172
+ reasoning to :class:`.BooleanEngine`.
1173
+
1174
+ This is a somewhat experimental, simple and illustrative
1175
+ implementation rather than a performance-optimized CP solver.
1176
+
1177
+ Example:
1178
+
1179
+ .. code-block:: python
1180
+
1181
+ >>> from pysat.integer import Integer, IntegerEngine
1182
+ >>> from pysat.solvers import Solver
1183
+ >>> X = Integer('X', 0, 3)
1184
+ >>> Y = Integer('Y', 0, 3)
1185
+ >>> eng = IntegerEngine([X, Y], adaptive=True)
1186
+ >>> eng.add_linear(X + Y <= 4)
1187
+ >>> eng.add_linear(X != Y)
1188
+ >>> with Solver(name='cd195') as solver:
1189
+ ... solver.connect_propagator(eng)
1190
+ ... eng.setup_observe(solver)
1191
+ ... while solver.solve():
1192
+ ... model = solver.get_model()
1193
+ ... vals = eng.decode_model(model)
1194
+ ... print('model:', {var.name: value for var, value in vals.items()})
1195
+ ... blits = eng.encode_model(vals)
1196
+ ... solver.add_clause([-l for l in blits])
1197
+ model: {'X': 1, 'Y': 0}
1198
+ model: {'X': 2, 'Y': 0}
1199
+ model: {'X': 0, 'Y': 1}
1200
+ model: {'X': 2, 'Y': 1}
1201
+ model: {'X': 1, 'Y': 2}
1202
+ model: {'X': 0, 'Y': 2}
1203
+ """
1204
+
1205
+ def __init__(self, vars=None, constraints=None, adaptive=True, vpool=None):
1206
+ """
1207
+ Constructor.
1208
+
1209
+ :param vars: :class:`.Integer` variables
1210
+ :param constraints: optional constraints to bootstrap with
1211
+ :param adaptive: enable adaptive mode in the Boolean engine
1212
+ :param vpool: optional shared IDPool for auxiliary variables
1213
+
1214
+ :type vars: list of :class:`.Integer`
1215
+ :type constraints: list of :class:`.LinearExpr`
1216
+ :type adaptive: bool
1217
+ :type vpool: :class:`.IDPool` or None
1218
+ """
1219
+
1220
+ # sets for storing encoded and attached variables
1221
+ self._venc, self._vatt = set(), set()
1222
+
1223
+ # cache of linear constraints and reified constraints
1224
+ self._lcons = []
1225
+
1226
+ # flag to determine whether we've already done setup_observe()
1227
+ self._attached = False
1228
+
1229
+ # variables, if any, and their domain clauses
1230
+ self.integers, self.clauses = vars or [], []
1231
+
1232
+ # variable manager
1233
+ self.vpool = vpool
1234
+
1235
+ # checking related to the vpool
1236
+ if self.integers and self.vpool is None:
1237
+ self.vpool = self.integers[0].vpool
1238
+ if self.vpool is not None:
1239
+ for var in self.integers:
1240
+ if var.vpool is not self.vpool:
1241
+ raise ValueError('All variables must share the same IDPool')
1242
+
1243
+ # processing all the variables known so far
1244
+ for var in self.integers:
1245
+ self._encode_domain(var)
1246
+
1247
+ # BooleanEngine's constructor
1248
+ super().__init__(bootstrap_with=[], adaptive=adaptive)
1249
+
1250
+ # adding constraints, if any
1251
+ for constr in constraints or []:
1252
+ self.add_linear(constr)
1253
+
1254
+ def add_var(self, var):
1255
+ """
1256
+ Add a new integer variable and its domain clauses.
1257
+
1258
+ :param var: integer variable
1259
+ :type var: :class:`Integer`
1260
+ """
1261
+
1262
+ # first, we need to make sure all variables share the same IDPool
1263
+ if self.vpool is None:
1264
+ self.vpool = var.vpool
1265
+ elif var.vpool is not self.vpool:
1266
+ raise ValueError('All variables must share the same IDPool')
1267
+
1268
+ self.integers.append(var)
1269
+ self._encode_domain(var)
1270
+
1271
+ def _encode_domain(self, var):
1272
+ """
1273
+ Cache and add domain clauses for a variable once per engine.
1274
+ """
1275
+
1276
+ # do nothing if this variable is already encoded
1277
+ if var in self._venc:
1278
+ return
1279
+
1280
+ # marking the variable as encoded
1281
+ self._venc.add(var)
1282
+
1283
+ # encoding process
1284
+ clauses = var.domain_clauses()
1285
+ self.clauses.extend(clauses)
1286
+
1287
+ # if we are attached, pass the variable and its clauses to the solver
1288
+ if self._attached:
1289
+ for cl in clauses:
1290
+ self.solver.add_clause(cl)
1291
+
1292
+ self._vatt.add(var)
1293
+
1294
+ def add_linear(self, constraint):
1295
+ """
1296
+ Add a constraint or a bundle of constraints to the engine.
1297
+
1298
+ Accepts a single :class:`.LinearExpr`-style constraint (e.g. ``X +
1299
+ Y <= 4``), a list of such constraints (from ``==``), or a
1300
+ :class:`.BooleanEngine`'s ``'linear'`` constraint tuple (or a list
1301
+ of them). The ``!=`` operator is supported between Integer
1302
+ variables and between linear expressions (via auxiliary Boolean
1303
+ variables).
1304
+
1305
+ Note that in the case of variants of :class:`.LinearExpr`
1306
+ constraints, those are internally transformed into
1307
+ :class:`.BooleanEngine`-style PB constraint first.
1308
+
1309
+ :param constraint: constraint or list of constraints
1310
+ :type constraint: LinearExpr | tuple | list
1311
+ """
1312
+
1313
+ # it's a '!=' constraint on two Integer variables
1314
+ if isinstance(constraint, tuple) and constraint and constraint[0] == 'ne':
1315
+ self.add_not_equal(constraint[1], constraint[2])
1316
+ return
1317
+
1318
+ # it's a '!=' constraint on two LinearExpr objects
1319
+ if isinstance(constraint, tuple) and constraint and constraint[0] == 'ne_linear':
1320
+ left, right = constraint[1], constraint[2]
1321
+ if not isinstance(left, LinearExpr) or not isinstance(right, LinearExpr):
1322
+ raise TypeError('Linear != requires LinearExpr arguments')
1323
+
1324
+ # reified disjunction: (left-right <= -1) or (left-right >= 1)
1325
+ diff = left - right
1326
+ c1, c2 = diff <= -1, diff >= 1
1327
+ s1, s2 = self._reify_linear(c1), self._reify_linear(c2)
1328
+ if s1 is None or s2 is None:
1329
+ # one side is tautological => disjunction always true
1330
+ return
1331
+
1332
+ # enforce the disjunction of s1 or s2
1333
+ self.clauses.append([s1, s2])
1334
+ if self._attached:
1335
+ self.solver.add_clause([s1, s2])
1336
+
1337
+ return
1338
+
1339
+ # a linear constraint or a collection of those
1340
+ if isinstance(constraint, (list, tuple)):
1341
+ # empty constraint
1342
+ if not constraint:
1343
+ return
1344
+
1345
+ # a single constraint
1346
+ if constraint and isinstance(constraint[0], str):
1347
+ self._record_linear(constraint)
1348
+ self._add_constraint_internal(constraint)
1349
+ return
1350
+
1351
+ # a collection of constraints
1352
+ if constraint and all(
1353
+ isinstance(c, (list, tuple)) and c and isinstance(c[0], str)
1354
+ for c in constraint
1355
+ ):
1356
+ for c in constraint:
1357
+ self._record_linear(c)
1358
+ self._add_constraint_internal(c)
1359
+ return
1360
+
1361
+ raise TypeError('Unsupported constraint type; expected linear tuple or list of tuples')
1362
+
1363
+ def _record_linear(self, constraint):
1364
+ """
1365
+ Store a linear constraint for later clausification.
1366
+ """
1367
+
1368
+ if isinstance(constraint, (list, tuple)) and constraint and constraint[0] == 'linear':
1369
+ self._lcons.append(constraint)
1370
+
1371
+ def add_alldifferent(self, vars):
1372
+ """
1373
+ Add an AllDifferent constraint over variables (for direct/coupled
1374
+ encoding only).
1375
+
1376
+ :param vars: variables to be all-different
1377
+ :type vars: list(:class:`Integer`)
1378
+ """
1379
+
1380
+ # no variables -> nothing to do
1381
+ if not vars:
1382
+ return
1383
+
1384
+ # every variable must share the same IDPool,
1385
+ # and its domainencoding must not be 'order'
1386
+ vpool = vars[0].vpool
1387
+ for var in vars:
1388
+ if var.vpool is not vpool:
1389
+ raise ValueError('AllDifferent variables must share the same IDPool')
1390
+ if var.encoding not in ('direct', 'coupled'):
1391
+ raise ValueError('AllDifferent requires direct or coupled encoding')
1392
+
1393
+ # we care only about the intersection of all domains
1394
+ values = set()
1395
+ for var in vars:
1396
+ values.update(var.domain)
1397
+
1398
+ # modelling all-differents are a bunch of AtMost1 constraints
1399
+ for v in values:
1400
+ lits = [var.equals(v) for var in vars if v in var.domain]
1401
+ if len(lits) > 1:
1402
+ self.add_linear(('linear', [lits, 1]))
1403
+
1404
+ def _reify_linear(self, constraint):
1405
+ """
1406
+ Reify a :class:`.BooleanEngine`'s ``'linear'`` constraint.
1407
+
1408
+ Returns a selector literal ``b`` such that ``b -> constraint``.
1409
+ Returns ``None`` if the constraint is tautological.
1410
+ """
1411
+
1412
+ # we can verify only BooleanEngine's linears
1413
+ if not (isinstance(constraint, tuple) and constraint and constraint[0] == 'linear'):
1414
+ raise TypeError('Expected a BooleanEngine linear constraint')
1415
+
1416
+ lits, bound = constraint[1][0], constraint[1][1]
1417
+ weights = {} if len(constraint[1]) == 2 else constraint[1][2]
1418
+
1419
+ if not weights:
1420
+ weights = {l: 1 for l in lits}
1421
+ if any(l not in weights for l in lits):
1422
+ raise ValueError('PB encoding requires explicit weights for all literals')
1423
+
1424
+ # computing the key
1425
+ ltup = tuple(sorted(lits))
1426
+ wtup = tuple(weights[lit] for lit in ltup)
1427
+ key = (ltup, wtup, bound)
1428
+
1429
+ max_sum = sum(wtup)
1430
+ if bound >= max_sum:
1431
+ # the contraint is trivially satisfiable
1432
+ return None
1433
+
1434
+ # first, checking if we actually have an IDPool to work with!
1435
+ if self.vpool is None:
1436
+ self.vpool = Formula.export_vpool()
1437
+
1438
+ if (self, '_reif', key) not in self.vpool.obj2id:
1439
+ selv = self.vpool.id((self, '_reif', key))
1440
+
1441
+ if bound < 0:
1442
+ # constraint unsatisfiable => selv must be false
1443
+ self.clauses.append([-selv])
1444
+ if self._attached:
1445
+ self.solver.add_clause([-selv])
1446
+ return selv
1447
+
1448
+ # computing selector's weight and recording the literal
1449
+ wght = max_sum - bound
1450
+ rweights = dict(weights)
1451
+ rweights[selv] = wght
1452
+ rlits = list(lits) + [selv]
1453
+
1454
+ # creating the actual constraint
1455
+ rcons = ('linear', [rlits, bound + wght, rweights])
1456
+ self._record_linear(rcons)
1457
+ self._add_constraint_internal(rcons)
1458
+
1459
+ return self.vpool.id((self, '_reif', key))
1460
+
1461
+ def add_not_equal(self, left, right):
1462
+ """
1463
+ Add a not-equal constraint on two :class:`Integer` variables: left
1464
+ != right (for direct/coupled encodings only).
1465
+
1466
+ :param left: left variable
1467
+ :param right: right variable
1468
+ :type left: :class:`Integer`
1469
+ :type right: :class:`Integer`
1470
+ """
1471
+
1472
+ # common-sense checks first
1473
+ if left.vpool is not right.vpool:
1474
+ raise ValueError('NotEqual variables must share the same IDPool')
1475
+ if left.encoding not in ('direct', 'coupled'):
1476
+ raise ValueError('NotEqual requires direct or coupled encoding on left')
1477
+ if right.encoding not in ('direct', 'coupled'):
1478
+ raise ValueError('NotEqual requires direct or coupled encoding on right')
1479
+
1480
+ # adding CNF clauses for value inequality
1481
+ values = set(left.domain).intersection(right.domain)
1482
+ for v in values:
1483
+ cl = [-left.equals(v), -right.equals(v)]
1484
+ self.clauses.append(cl)
1485
+ if self._attached:
1486
+ self.solver.add_clause(cl)
1487
+
1488
+ def clausify(self, cardenc=CardEncType.seqcounter, pbenc=PBEncType.best):
1489
+ """
1490
+ Clausify all constraints and return a CNF encoding. All linear
1491
+ constraints are converted using the user-specified cardinality and
1492
+ pseudo-Boolean encoding. The former are utilized for unweighted
1493
+ constraints, while the latter are utilized for weighted
1494
+ constraints.
1495
+
1496
+ Note that parameters ``cardenc`` and ``pbenc`` default to
1497
+ :attr:`.card.EncType.seqcounter` and
1498
+ :class:`pysat.pb.EncType.best`, respectively.
1499
+
1500
+ :param cardenc: cardinality encoding for unweighted constraints
1501
+ :param pbenc: PB encoding for weighted constraints
1502
+ :type cardenc: int
1503
+ :type pbenc: int
1504
+
1505
+ :rtype: :class:`pysat.formula.CNF`
1506
+ """
1507
+
1508
+ # we are going to store the resulting formula here
1509
+ res = CNF()
1510
+
1511
+ # first, checking if there is work to do
1512
+ if not self.integers:
1513
+ if self._lcons:
1514
+ # it should not happen that we've got constraints but no vars
1515
+ raise ValueError('Cannot clausify linear constraints without integer variables')
1516
+
1517
+ # returning an empty formula
1518
+ return res
1519
+
1520
+ # making sure all variables share the same IDPool
1521
+ vpool = self.integers[0].vpool
1522
+ for i in range(1, len(self.integers)):
1523
+ if self.integers[i].vpool is not vpool:
1524
+ raise ValueError('All variables must share the same IDPool')
1525
+
1526
+ # extracting domain clauses for all the variables
1527
+ for var in self.integers:
1528
+ res.extend(var.domain_clauses())
1529
+
1530
+ # include any extra CNF clauses (e.g., reified disjunctions)
1531
+ res.extend(self.clauses)
1532
+
1533
+ # the main part is to encode all linear constraints
1534
+ for cs in self._lcons:
1535
+ lits, bound = cs[1][0], cs[1][1]
1536
+ weights = {} if len(cs[1]) == 2 else cs[1][2]
1537
+
1538
+ if not weights:
1539
+ wght = 1
1540
+ else:
1541
+ wvals = list(weights.values())
1542
+ if len(set(wvals)) == 1:
1543
+ wght = wvals[0]
1544
+ else:
1545
+ wght = None
1546
+
1547
+ # we have a common weight wght => cardinality constraint
1548
+ if wght is not None:
1549
+ new_bound = bound // wght
1550
+ if isinstance(new_bound, Decimal):
1551
+ new_bound = int(new_bound)
1552
+ elif isinstance(new_bound, float):
1553
+ new_bound = int(math.floor(new_bound))
1554
+ else:
1555
+ new_bound = int(new_bound)
1556
+
1557
+ if new_bound < 0:
1558
+ # the bound is negative => constraint is unsatisfiable
1559
+ res.append([])
1560
+ continue
1561
+ if new_bound >= len(lits):
1562
+ # the constraint is unfalsifiable
1563
+ continue
1564
+
1565
+ enc = CardEnc.atmost(lits=lits, bound=new_bound, vpool=vpool,
1566
+ encoding=cardenc)
1567
+ res.extend(enc.clauses)
1568
+ continue
1569
+
1570
+ # the weights are not all the same => PB constraint
1571
+ # first checking the bound and literal weights for being int-like
1572
+ if not self._intlike(bound):
1573
+ raise ValueError('PB encoding requires integer bound')
1574
+ if any(l not in weights for l in lits):
1575
+ raise ValueError('PB encoding requires explicit weights for all literals')
1576
+ wlist = [weights[l] for l in lits]
1577
+ if not all(self._intlike(w) for w in wlist):
1578
+ raise ValueError('PB encoding requires integer weights')
1579
+
1580
+ bound_int = int(bound) if not isinstance(bound, Decimal) else int(bound)
1581
+ wlist = [int(w) if not isinstance(w, Decimal) else int(w)
1582
+ for w in wlist]
1583
+
1584
+ enc = PBEnc.atmost(lits=lits, weights=wlist, bound=bound_int,
1585
+ vpool=vpool, encoding=pbenc)
1586
+ res.extend(enc.clauses)
1587
+
1588
+ return res
1589
+
1590
+ @staticmethod
1591
+ def _intlike(value):
1592
+ """
1593
+ Checking if the value can be converted to integer.
1594
+ """
1595
+
1596
+ # yes, nothing to do
1597
+ if isinstance(value, numbers.Integral):
1598
+ return True
1599
+
1600
+ # yes, it's a convertable decimal
1601
+ if isinstance(value, Decimal):
1602
+ return value == value.to_integral_value()
1603
+
1604
+ # yes, it's a convertable float
1605
+ if isinstance(value, float):
1606
+ return value.is_integer()
1607
+
1608
+ # no, we can't convert it
1609
+ return False
1610
+
1611
+ def _add_constraint_internal(self, constraint):
1612
+ """
1613
+ Add a constraint regardless of whether the solver is connected.
1614
+ """
1615
+
1616
+ if self.solver is None:
1617
+ # the solver does not exist yet, we can't do setup_observe()
1618
+ cs = self._add_constraint(constraint)
1619
+ cs.register_watched(self.wlst)
1620
+
1621
+ for lit in cs.lits:
1622
+ var = abs(lit)
1623
+ if var not in self.vset:
1624
+ self.vset.add(var)
1625
+ self.value[var] = None
1626
+ self.level[var] = None
1627
+ self.fixed[var] = False
1628
+
1629
+ # keep the observed variable list in sync before setup_observe()
1630
+ self.vars = sorted(self.vset)
1631
+
1632
+ cs.attach_values(self.value)
1633
+ else:
1634
+ # all good, resorting to BooleanEngine's add_constraint()
1635
+ super().add_constraint(constraint)
1636
+
1637
+ @staticmethod
1638
+ def pb_linear_leq(terms, bound):
1639
+ """
1640
+ Convenience wrapper for building a PySAT ``'linear'`` constraint
1641
+ (cardinality / pseudo-Boolean) from integer terms. Calls
1642
+ :meth:`.LinearExpr.pb_linear_leq`.
1643
+ """
1644
+
1645
+ return LinearExpr.pb_linear_leq(terms, bound)
1646
+
1647
+ def setup_observe(self, solver):
1648
+ """
1649
+ Inform the solver about observed variables and add domain clauses.
1650
+
1651
+ :param solver: SAT solver instance
1652
+ :type solver: :class:`pysat.solvers.Cadical195`
1653
+ """
1654
+
1655
+ # first, calling BooleanEngine's setup_observe()
1656
+ # this is required for registering more Boolean self.vars variables
1657
+ # if we managed to create them since last time the method was called
1658
+ # note that self.vars is populated by collecting
1659
+ # variables appearing in linear constraints only
1660
+ super().setup_observe(solver)
1661
+
1662
+ # next, adding domain clauses for all the known variables
1663
+ # setup_observe() is to be called once but more
1664
+ # variables can be added later through add_var()
1665
+ if not self._attached:
1666
+ for var in self._venc:
1667
+ if var in self._vatt:
1668
+ continue
1669
+
1670
+ for cl in var.domain_clauses():
1671
+ self.solver.add_clause(cl)
1672
+
1673
+ self._vatt.add(var)
1674
+
1675
+ for cl in self.clauses:
1676
+ self.solver.add_clause(cl)
1677
+
1678
+ # flagging that setup_observe() has been called
1679
+ self._attached = True
1680
+
1681
+ def decode_model(self, model, vars=None):
1682
+ """
1683
+ Given a SAT model (list of int identifiers), return a mapping
1684
+ ``{Integer: integer_value}`` for the engine's integer variables.
1685
+
1686
+ If ``vars`` is provided, decode only those variables.
1687
+
1688
+ Example:
1689
+
1690
+ .. code-block:: python
1691
+
1692
+ >>> model = solver.get_model()
1693
+ >>> values = eng.decode_model(model, vars=[X, Y])
1694
+ >>> print(values)
1695
+
1696
+ :param model: SAT model (list of literals)
1697
+ :param vars: subset of variables to decode (optional)
1698
+ :type model: list(int)
1699
+ :type vars: list(:class:`Integer`) or None
1700
+
1701
+ :rtype: dict
1702
+ """
1703
+
1704
+ res = {}
1705
+
1706
+ # resorting to variables' own ability to derive their values
1707
+ for var in vars if vars is not None else self.integers:
1708
+ res[var] = var.decode(model)
1709
+
1710
+ return res
1711
+
1712
+ def encode_model(self, model, vars=None):
1713
+ """
1714
+ Given an integer-level model, return a list of true literals.
1715
+
1716
+ ``model`` can be a dict mapping :class:`Integer` objects
1717
+ (preferred) or variable names to values, or a list/tuple of values
1718
+ corresponding to ``vars`` (or ``self.integers`` if ``vars`` is
1719
+ None).
1720
+
1721
+ This is the inverse of :meth:`.decode_model` at the integer level.
1722
+
1723
+ :param model: integer-level model
1724
+ :param vars: variable order for list/tuple models (optional)
1725
+ :type model: dict or list
1726
+ :type vars: list(:class:`Integer`) or None
1727
+
1728
+ :rtype: list(int)
1729
+ """
1730
+
1731
+ # if no target is given, using all variables
1732
+ if vars is None:
1733
+ vars = self.integers
1734
+
1735
+ # preparing the values list
1736
+ if isinstance(model, dict):
1737
+ values = []
1738
+ for var in vars:
1739
+ if var in model:
1740
+ values.append(model[var])
1741
+ else:
1742
+ values.append(model.get(var.name))
1743
+ else:
1744
+ values = list(model)
1745
+ if len(values) != len(vars):
1746
+ raise ValueError('Model length does not match variables list')
1747
+
1748
+ # the actual translation is done by variables themselves
1749
+ lits = []
1750
+ for var, value in zip(vars, values):
1751
+ lits.extend(var.encode(value))
1752
+
1753
+ return lits
1754
+
1755
+ def decode_assignment(self, lits, vars=None):
1756
+ """
1757
+ Given a list of assigned Boolean literals, return a mapping
1758
+ ``{Integer: integer_value}`` for those variables whose value is
1759
+ fixed by the assignment alone. Unlike :meth:`.decode_model`, this
1760
+ method does not require a full model and only looks at literals
1761
+ present in ``lits``. Also note that the method does not require
1762
+ the input literals to be in any particular order.
1763
+
1764
+ If multiple inconsistent *equality* literals are present for the
1765
+ same variable, the value is reported as a list of all observed
1766
+ values, which (if an issue) can be used to find the culprit in the
1767
+ encoding. For order-based literals, if the inferred bounds fix the
1768
+ value, the value is reported as an integer; otherwise, the value
1769
+ is reported as the list of all values within the inferred bounds.
1770
+ If order-based bounds are contradictory (lower bound exceeds upper
1771
+ bound), the value is reported as an empty list.
1772
+
1773
+ Example:
1774
+
1775
+ .. code-block:: python
1776
+
1777
+ >>> st, props = solver.propagate(assumptions=clues)
1778
+ >>> vals = eng.decode_assignment(props)
1779
+ >>> # vals[i] is an int if the value is fixed,
1780
+ >>> # a list if conflicting if multiple values are present,
1781
+ >>> # or [] if contradictory bounds were derived.
1782
+
1783
+ :param lits: literals in the assignment
1784
+ :param vars: subset of variables to decode (optional)
1785
+ :type lits: iterable(int)
1786
+ :type vars: list(:class:`Integer`) or None
1787
+
1788
+ :rtype: dict
1789
+ """
1790
+
1791
+ # don't anything about any variables!
1792
+ if self.vpool is None:
1793
+ return {}
1794
+
1795
+ # optional filtering by a subset of variables
1796
+ allowed = None if vars is None else set(vars)
1797
+
1798
+ # per-variable state tracked only when we see relevant literals
1799
+ eq_true, lbounds, ubounds = {}, {}, {}
1800
+
1801
+ seen = set()
1802
+ for lit in lits:
1803
+ if lit == 0:
1804
+ # literal '0' makes no sense
1805
+ raise ValueError('Literal 0 is not allowed')
1806
+
1807
+ if lit not in seen:
1808
+ seen.add(lit)
1809
+
1810
+ # getting the object given a literal
1811
+ obj = self.vpool.obj(abs(lit))
1812
+
1813
+ # we expect a tuple of (var, 'kind', value)
1814
+ if not obj or not isinstance(obj, tuple) or len(obj) != 3:
1815
+ # not a variable we know!
1816
+ continue
1817
+
1818
+ var, kind, value = obj
1819
+ if not isinstance(var, Integer) or kind not in ('eq', 'ge'):
1820
+ # still not our variable!
1821
+ continue
1822
+
1823
+ # checking if the variable is among those we are interested in
1824
+ if allowed is not None and var not in allowed:
1825
+ continue
1826
+
1827
+ if kind == 'eq':
1828
+ if lit > 0:
1829
+ eq_true.setdefault(var, set()).add(value)
1830
+ else: # kind == 'ge'
1831
+ if var not in lbounds:
1832
+ lbounds[var] = var.lb
1833
+ ubounds[var] = var.ub
1834
+
1835
+ # refining the bounds if we can
1836
+ if lit > 0:
1837
+ if value > lbounds[var]:
1838
+ lbounds[var] = value
1839
+ else:
1840
+ bound = value - 1
1841
+ if bound < ubounds[var]:
1842
+ ubounds[var] = bound
1843
+
1844
+ # now, we are going to collate the data and put it here
1845
+ res = {}
1846
+
1847
+ # direct/coupled encodings expose equality literals
1848
+ for var, values in eq_true.items():
1849
+ if len(values) == 1:
1850
+ # taking the concrete value we found
1851
+ res[var] = next(iter(values))
1852
+ else:
1853
+ # multiple values are found
1854
+ res[var] = sorted(values)
1855
+
1856
+ # order/coupled encodings may be fixed by bounds alone
1857
+ for var in lbounds:
1858
+ if var in res:
1859
+ # well, we already have a concrete value for this variable
1860
+ continue
1861
+
1862
+ if lbounds[var] == ubounds[var]:
1863
+ # concrete value defined from the bounds
1864
+ res[var] = lbounds[var]
1865
+
1866
+ elif lbounds[var] < ubounds[var]:
1867
+ # the bounds represent a valid range
1868
+ res[var] = list(range(lbounds[var], ubounds[var] + 1))
1869
+ else:
1870
+ # conflicting bounds detected; report as an empty list
1871
+ # to signal contradiction without raising
1872
+ res[var] = []
1873
+
1874
+ return res
1875
+
1876
+
1877
+ # example usage
1878
+ #==============================================================================
1879
+ if __name__ == '__main__':
1880
+ # example 1: satisfiable, mixed encodings, all constraint types, enumeration
1881
+ from decimal import Decimal
1882
+ from pysat.solvers import Solver
1883
+
1884
+ X = Integer('X', 0, 3) # direct
1885
+ Y = Integer('Y', 0, 3, encoding='direct') # direct
1886
+ Z = Integer('Z', 0, 3, encoding='coupled') # coupled
1887
+ D = Integer('D', 0, 3, numeric='decimal') # decimal
1888
+ E = Integer('E', 0, 3, numeric='decimal') # decimal
1889
+
1890
+ eng = IntegerEngine([X, Y, Z, D, E], adaptive=False)
1891
+
1892
+ # linear constraints
1893
+ eng.add_linear(X + Y <= 4 - Z)
1894
+ eng.add_linear(X + Y >= 3)
1895
+ eng.add_linear(Z - X <= 1)
1896
+ eng.add_linear(X - Z <= 1)
1897
+
1898
+ # not equal and AllDifferent (direct/coupled only)
1899
+ eng.add_linear(X != Z)
1900
+ eng.add_alldifferent([X, Y, Z])
1901
+
1902
+ # decimal linear constraint
1903
+ eng.add_linear(Decimal('1') * D + Decimal('2') * E <= Decimal('3'))
1904
+ eng.add_linear(Decimal('1') * D + Decimal('1') * E == Decimal('2'))
1905
+
1906
+ # model enumeration using integer-level blocking
1907
+ with Solver(name='cd19', bootstrap_with=None) as solver:
1908
+ solver.connect_propagator(eng)
1909
+ eng.setup_observe(solver)
1910
+ while solver.solve():
1911
+ model = solver.get_model()
1912
+ vals = eng.decode_model(model)
1913
+ print('ENG example:', {var.name: value for var, value in vals.items()})
1914
+ blits = eng.encode_model(vals)
1915
+ solver.add_clause([-l for l in blits])
1916
+
1917
+ print('')
1918
+
1919
+ # example 2: satisfiable, clausified (same model set as example 1)
1920
+ cnf = eng.clausify()
1921
+ with Solver(name='g3', bootstrap_with=cnf.clauses) as solver:
1922
+ while solver.solve():
1923
+ model = solver.get_model()
1924
+ vals = eng.decode_model(model)
1925
+ print('CNF example:', {var.name: value for var, value in vals.items()})
1926
+ blits = eng.encode_model(vals)
1927
+ solver.add_clause([-l for l in blits])
1928
+
1929
+ # example 3: should be unsatisfiable
1930
+ A = Integer('A', 0, 2)
1931
+ B = Integer('B', 0, 2)
1932
+ eng2 = IntegerEngine([A, B], adaptive=False)
1933
+ eng2.add_linear(A + B <= 1)
1934
+ eng2.add_linear(A - B >= 2)
1935
+
1936
+ with Solver(name='cd19', bootstrap_with=None) as solver:
1937
+ solver.connect_propagator(eng2)
1938
+ eng2.setup_observe(solver)
1939
+ sat = solver.solve()
1940
+ if sat:
1941
+ model = solver.get_model()
1942
+ vals = eng2.decode_model(model)
1943
+ print('UNSAT example (wrong!):', {var.name: value for var, value in vals.items()})
1944
+ else:
1945
+ print('UNSAT example: UNSAT')