python-sat 1.8.dev28__cp313-cp313-win_amd64.whl → 1.8.dev30__cp313-cp313-win_amd64.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pycard.cp313-win_amd64.pyd +0 -0
- pysat/__init__.py +2 -2
- pysat/card.py +1 -1
- pysat/integer.py +1945 -0
- pysolvers.cp313-win_amd64.pyd +0 -0
- {python_sat-1.8.dev28.dist-info → python_sat-1.8.dev30.dist-info}/METADATA +1 -1
- {python_sat-1.8.dev28.dist-info → python_sat-1.8.dev30.dist-info}/RECORD +24 -23
- {python_sat-1.8.dev28.data → python_sat-1.8.dev30.data}/scripts/approxmc.py +0 -0
- {python_sat-1.8.dev28.data → python_sat-1.8.dev30.data}/scripts/bbscan.py +0 -0
- {python_sat-1.8.dev28.data → python_sat-1.8.dev30.data}/scripts/bica.py +0 -0
- {python_sat-1.8.dev28.data → python_sat-1.8.dev30.data}/scripts/fm.py +0 -0
- {python_sat-1.8.dev28.data → python_sat-1.8.dev30.data}/scripts/genhard.py +0 -0
- {python_sat-1.8.dev28.data → python_sat-1.8.dev30.data}/scripts/lbx.py +0 -0
- {python_sat-1.8.dev28.data → python_sat-1.8.dev30.data}/scripts/lsu.py +0 -0
- {python_sat-1.8.dev28.data → python_sat-1.8.dev30.data}/scripts/mcsls.py +0 -0
- {python_sat-1.8.dev28.data → python_sat-1.8.dev30.data}/scripts/models.py +0 -0
- {python_sat-1.8.dev28.data → python_sat-1.8.dev30.data}/scripts/musx.py +0 -0
- {python_sat-1.8.dev28.data → python_sat-1.8.dev30.data}/scripts/optux.py +0 -0
- {python_sat-1.8.dev28.data → python_sat-1.8.dev30.data}/scripts/primer.py +0 -0
- {python_sat-1.8.dev28.data → python_sat-1.8.dev30.data}/scripts/rc2.py +0 -0
- {python_sat-1.8.dev28.data → python_sat-1.8.dev30.data}/scripts/unigen.py +0 -0
- {python_sat-1.8.dev28.dist-info → python_sat-1.8.dev30.dist-info}/WHEEL +0 -0
- {python_sat-1.8.dev28.dist-info → python_sat-1.8.dev30.dist-info}/licenses/LICENSE.txt +0 -0
- {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')
|