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