python-constraint2 2.5.0__cp314-cp314-win_amd64.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- constraint/__init__.py +38 -0
- constraint/constraints.c +56770 -0
- constraint/constraints.py +1572 -0
- constraint/domain.c +9866 -0
- constraint/domain.py +102 -0
- constraint/parser.c +24324 -0
- constraint/parser.py +446 -0
- constraint/problem.c +18512 -0
- constraint/problem.py +305 -0
- constraint/solvers.c +30180 -0
- constraint/solvers.py +788 -0
- python_constraint2-2.5.0.dist-info/METADATA +252 -0
- python_constraint2-2.5.0.dist-info/RECORD +15 -0
- python_constraint2-2.5.0.dist-info/WHEEL +4 -0
- python_constraint2-2.5.0.dist-info/licenses/LICENSE +23 -0
|
@@ -0,0 +1,1572 @@
|
|
|
1
|
+
"""Module containing the code for constraint definitions."""
|
|
2
|
+
|
|
3
|
+
from constraint.domain import Unassigned
|
|
4
|
+
from typing import Callable, Union, Optional
|
|
5
|
+
from collections.abc import Sequence
|
|
6
|
+
from itertools import product
|
|
7
|
+
|
|
8
|
+
class Constraint:
|
|
9
|
+
"""Abstract base class for constraints."""
|
|
10
|
+
|
|
11
|
+
def __call__(self, variables: Sequence, domains: dict, assignments: dict, forwardcheck=False):
|
|
12
|
+
"""Perform the constraint checking.
|
|
13
|
+
|
|
14
|
+
If the forwardcheck parameter is not false, besides telling if
|
|
15
|
+
the constraint is currently broken or not, the constraint
|
|
16
|
+
implementation may choose to hide values from the domains of
|
|
17
|
+
unassigned variables to prevent them from being used, and thus
|
|
18
|
+
prune the search space.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
variables (sequence): :py:class:`Variables` affected by that constraint,
|
|
22
|
+
in the same order provided by the user
|
|
23
|
+
domains (dict): Dictionary mapping variables to their
|
|
24
|
+
domains
|
|
25
|
+
assignments (dict): Dictionary mapping assigned variables to
|
|
26
|
+
their current assumed value
|
|
27
|
+
forwardcheck: Boolean value stating whether forward checking
|
|
28
|
+
should be performed or not
|
|
29
|
+
|
|
30
|
+
Returns:
|
|
31
|
+
bool: Boolean value stating if this constraint is currently
|
|
32
|
+
broken or not
|
|
33
|
+
"""
|
|
34
|
+
return True
|
|
35
|
+
|
|
36
|
+
def preProcess(self, variables: Sequence, domains: dict, constraints: list[tuple], vconstraints: dict):
|
|
37
|
+
"""Preprocess variable domains.
|
|
38
|
+
|
|
39
|
+
This method is called before starting to look for solutions,
|
|
40
|
+
and is used to prune domains with specific constraint logic
|
|
41
|
+
when possible. For instance, any constraints with a single
|
|
42
|
+
variable may be applied on all possible values and removed,
|
|
43
|
+
since they may act on individual values even without further
|
|
44
|
+
knowledge about other assignments.
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
variables (sequence): Variables affected by that constraint,
|
|
48
|
+
in the same order provided by the user
|
|
49
|
+
domains (dict): Dictionary mapping variables to their
|
|
50
|
+
domains
|
|
51
|
+
constraints (list): List of pairs of (constraint, variables)
|
|
52
|
+
vconstraints (dict): Dictionary mapping variables to a list
|
|
53
|
+
of constraints affecting the given variables.
|
|
54
|
+
"""
|
|
55
|
+
if len(variables) == 1:
|
|
56
|
+
variable = variables[0]
|
|
57
|
+
domain = domains[variable]
|
|
58
|
+
for value in domain[:]:
|
|
59
|
+
if not self(variables, domains, {variable: value}):
|
|
60
|
+
domain.remove(value)
|
|
61
|
+
constraints.remove((self, variables))
|
|
62
|
+
vconstraints[variable].remove((self, variables))
|
|
63
|
+
|
|
64
|
+
def forwardCheck(self, variables: Sequence, domains: dict, assignments: dict, _unassigned=Unassigned):
|
|
65
|
+
"""Helper method for generic forward checking.
|
|
66
|
+
|
|
67
|
+
Currently, this method acts only when there's a single
|
|
68
|
+
unassigned variable.
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
variables (sequence): Variables affected by that constraint,
|
|
72
|
+
in the same order provided by the user
|
|
73
|
+
domains (dict): Dictionary mapping variables to their
|
|
74
|
+
domains
|
|
75
|
+
assignments (dict): Dictionary mapping assigned variables to
|
|
76
|
+
their current assumed value
|
|
77
|
+
|
|
78
|
+
Returns:
|
|
79
|
+
bool: Boolean value stating if this constraint is currently
|
|
80
|
+
broken or not
|
|
81
|
+
"""
|
|
82
|
+
unassignedvariable = _unassigned
|
|
83
|
+
for variable in variables:
|
|
84
|
+
if variable not in assignments:
|
|
85
|
+
if unassignedvariable is _unassigned:
|
|
86
|
+
unassignedvariable = variable
|
|
87
|
+
else:
|
|
88
|
+
break
|
|
89
|
+
else:
|
|
90
|
+
if unassignedvariable is not _unassigned:
|
|
91
|
+
# Remove from the unassigned variable domain's all
|
|
92
|
+
# values which break our variable's constraints.
|
|
93
|
+
domain = domains[unassignedvariable]
|
|
94
|
+
if domain:
|
|
95
|
+
for value in domain[:]:
|
|
96
|
+
assignments[unassignedvariable] = value
|
|
97
|
+
if not self(variables, domains, assignments):
|
|
98
|
+
domain.hideValue(value)
|
|
99
|
+
del assignments[unassignedvariable]
|
|
100
|
+
if not domain:
|
|
101
|
+
return False
|
|
102
|
+
return True
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
class FunctionConstraint(Constraint):
|
|
106
|
+
"""Constraint which wraps a function defining the constraint logic.
|
|
107
|
+
|
|
108
|
+
Examples:
|
|
109
|
+
>>> problem = Problem()
|
|
110
|
+
>>> problem.addVariables(["a", "b"], [1, 2])
|
|
111
|
+
>>> def func(a, b):
|
|
112
|
+
... return b > a
|
|
113
|
+
>>> problem.addConstraint(func, ["a", "b"])
|
|
114
|
+
>>> problem.getSolution()
|
|
115
|
+
{'a': 1, 'b': 2}
|
|
116
|
+
|
|
117
|
+
>>> problem = Problem()
|
|
118
|
+
>>> problem.addVariables(["a", "b"], [1, 2])
|
|
119
|
+
>>> def func(a, b):
|
|
120
|
+
... return b > a
|
|
121
|
+
>>> problem.addConstraint(FunctionConstraint(func), ["a", "b"])
|
|
122
|
+
>>> problem.getSolution()
|
|
123
|
+
{'a': 1, 'b': 2}
|
|
124
|
+
>>> problem = Problem()
|
|
125
|
+
>>> problem.addVariables([1, 2], ["a", "b"])
|
|
126
|
+
>>> def func(x, y):
|
|
127
|
+
... return x != y
|
|
128
|
+
>>> problem.addConstraint(FunctionConstraint(func), [1, 2])
|
|
129
|
+
>>> sorted(sorted(x.items()) for x in problem.getSolutions())
|
|
130
|
+
[[(1, 'a'), (2, 'b')], [(1, 'b'), (2, 'a')]]
|
|
131
|
+
"""
|
|
132
|
+
|
|
133
|
+
def __init__(self, func: Callable, assigned: bool = True):
|
|
134
|
+
"""Initialization method.
|
|
135
|
+
|
|
136
|
+
Args:
|
|
137
|
+
func (callable object): Function wrapped and queried for
|
|
138
|
+
constraint logic
|
|
139
|
+
assigned (bool): Whether the function may receive unassigned
|
|
140
|
+
variables or not
|
|
141
|
+
"""
|
|
142
|
+
self._func = func
|
|
143
|
+
self._assigned = assigned
|
|
144
|
+
|
|
145
|
+
def __call__( # noqa: D102
|
|
146
|
+
self,
|
|
147
|
+
variables: Sequence,
|
|
148
|
+
domains: dict,
|
|
149
|
+
assignments: dict,
|
|
150
|
+
forwardcheck=False,
|
|
151
|
+
_unassigned=Unassigned,
|
|
152
|
+
):
|
|
153
|
+
# # initial code: 0.94621 seconds, Cythonized: 0.92805 seconds
|
|
154
|
+
# parms = [assignments.get(x, _unassigned) for x in variables]
|
|
155
|
+
# missing = parms.count(_unassigned)
|
|
156
|
+
|
|
157
|
+
# # list comprehension and sum: 0.13744 seconds, Cythonized: 0.10059 seconds
|
|
158
|
+
# parms = [assignments.get(x, _unassigned) for x in variables]
|
|
159
|
+
# missing = sum(x not in assignments for x in variables)
|
|
160
|
+
|
|
161
|
+
# # sum check with fallback: , Cythonized: 0.10108 seconds
|
|
162
|
+
# missing = sum(x not in assignments for x in variables)
|
|
163
|
+
# parms = [assignments.get(x, _unassigned) for x in variables] if missing > 0 else [assignments[x] for x in var]
|
|
164
|
+
|
|
165
|
+
# # tuple list comprehension with unzipping: 0.14521 seconds, Cythonized: 0.12054 seconds
|
|
166
|
+
# lst = [(assignments[x], 0) if x in assignments else (_unassigned, 1) for x in variables]
|
|
167
|
+
# parms, missing_iter = zip(*lst)
|
|
168
|
+
# parms = list(parms)
|
|
169
|
+
# missing = sum(missing_iter)
|
|
170
|
+
|
|
171
|
+
# # single loop array: 0.11249 seconds, Cythonized: 0.09514 seconds
|
|
172
|
+
# parms = [None] * len(variables)
|
|
173
|
+
# missing = 0
|
|
174
|
+
# for i, x in enumerate(variables):
|
|
175
|
+
# if x in assignments:
|
|
176
|
+
# parms[i] = assignments[x]
|
|
177
|
+
# else:
|
|
178
|
+
# parms[i] = _unassigned
|
|
179
|
+
# missing += 1
|
|
180
|
+
|
|
181
|
+
# single loop list: 0.11462 seconds, Cythonized: 0.08686 seconds
|
|
182
|
+
parms = list()
|
|
183
|
+
missing = 0
|
|
184
|
+
for x in variables:
|
|
185
|
+
if x in assignments:
|
|
186
|
+
parms.append(assignments[x])
|
|
187
|
+
else:
|
|
188
|
+
parms.append(_unassigned)
|
|
189
|
+
missing += 1
|
|
190
|
+
|
|
191
|
+
# if there are unassigned variables, do a forward check before executing the restriction function
|
|
192
|
+
if missing > 0:
|
|
193
|
+
return (self._assigned or self._func(*parms)) and (
|
|
194
|
+
not forwardcheck or missing != 1 or self.forwardCheck(variables, domains, assignments)
|
|
195
|
+
)
|
|
196
|
+
return self._func(*parms)
|
|
197
|
+
|
|
198
|
+
class CompilableFunctionConstraint(Constraint):
|
|
199
|
+
"""Wrapper function for picklable string constraints that must be compiled into a FunctionConstraint later on."""
|
|
200
|
+
|
|
201
|
+
def __init__(self, func: str, assigned: bool = True): # noqa: D102, D107
|
|
202
|
+
self._func = func
|
|
203
|
+
self._assigned = assigned
|
|
204
|
+
|
|
205
|
+
def __call__(self, variables, domains, assignments, forwardcheck=False, _unassigned=Unassigned): # noqa: D102
|
|
206
|
+
raise NotImplementedError("CompilableFunctionConstraint can not be called directly")
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
class AllDifferentConstraint(Constraint):
|
|
210
|
+
"""Constraint enforcing that values of all given variables are different.
|
|
211
|
+
|
|
212
|
+
Example:
|
|
213
|
+
>>> problem = Problem()
|
|
214
|
+
>>> problem.addVariables(["a", "b"], [1, 2])
|
|
215
|
+
>>> problem.addConstraint(AllDifferentConstraint())
|
|
216
|
+
>>> sorted(sorted(x.items()) for x in problem.getSolutions())
|
|
217
|
+
[[('a', 1), ('b', 2)], [('a', 2), ('b', 1)]]
|
|
218
|
+
"""
|
|
219
|
+
|
|
220
|
+
def __call__( # noqa: D102
|
|
221
|
+
self,
|
|
222
|
+
variables: Sequence,
|
|
223
|
+
domains: dict,
|
|
224
|
+
assignments: dict,
|
|
225
|
+
forwardcheck=False,
|
|
226
|
+
_unassigned=Unassigned,
|
|
227
|
+
):
|
|
228
|
+
seen = {}
|
|
229
|
+
for variable in variables:
|
|
230
|
+
value = assignments.get(variable, _unassigned)
|
|
231
|
+
if value is not _unassigned:
|
|
232
|
+
if value in seen:
|
|
233
|
+
return False
|
|
234
|
+
seen[value] = True
|
|
235
|
+
if forwardcheck:
|
|
236
|
+
for variable in variables:
|
|
237
|
+
if variable not in assignments:
|
|
238
|
+
domain = domains[variable]
|
|
239
|
+
for value in seen:
|
|
240
|
+
if value in domain:
|
|
241
|
+
domain.hideValue(value)
|
|
242
|
+
if not domain:
|
|
243
|
+
return False
|
|
244
|
+
return True
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
class AllEqualConstraint(Constraint):
|
|
248
|
+
"""Constraint enforcing that values of all given variables are equal.
|
|
249
|
+
|
|
250
|
+
Example:
|
|
251
|
+
>>> problem = Problem()
|
|
252
|
+
>>> problem.addVariables(["a", "b"], [1, 2])
|
|
253
|
+
>>> problem.addConstraint(AllEqualConstraint())
|
|
254
|
+
>>> sorted(sorted(x.items()) for x in problem.getSolutions())
|
|
255
|
+
[[('a', 1), ('b', 1)], [('a', 2), ('b', 2)]]
|
|
256
|
+
"""
|
|
257
|
+
|
|
258
|
+
def __call__( # noqa: D102
|
|
259
|
+
self,
|
|
260
|
+
variables: Sequence,
|
|
261
|
+
domains: dict,
|
|
262
|
+
assignments: dict,
|
|
263
|
+
forwardcheck=False,
|
|
264
|
+
_unassigned=Unassigned,
|
|
265
|
+
):
|
|
266
|
+
singlevalue = _unassigned
|
|
267
|
+
for variable in variables:
|
|
268
|
+
value = assignments.get(variable, _unassigned)
|
|
269
|
+
if singlevalue is _unassigned:
|
|
270
|
+
singlevalue = value
|
|
271
|
+
elif value is not _unassigned and value != singlevalue:
|
|
272
|
+
return False
|
|
273
|
+
if forwardcheck and singlevalue is not _unassigned:
|
|
274
|
+
for variable in variables:
|
|
275
|
+
if variable not in assignments:
|
|
276
|
+
domain = domains[variable]
|
|
277
|
+
if singlevalue not in domain:
|
|
278
|
+
return False
|
|
279
|
+
for value in domain[:]:
|
|
280
|
+
if value != singlevalue:
|
|
281
|
+
domain.hideValue(value)
|
|
282
|
+
return True
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
class ExactSumConstraint(Constraint):
|
|
286
|
+
"""Constraint enforcing that values of given variables sum exactly to a given amount.
|
|
287
|
+
|
|
288
|
+
Example:
|
|
289
|
+
>>> problem = Problem()
|
|
290
|
+
>>> problem.addVariables(["a", "b"], [1, 2])
|
|
291
|
+
>>> problem.addConstraint(ExactSumConstraint(3))
|
|
292
|
+
>>> sorted(sorted(x.items()) for x in problem.getSolutions())
|
|
293
|
+
[[('a', 1), ('b', 2)], [('a', 2), ('b', 1)]]
|
|
294
|
+
>>> problem = Problem()
|
|
295
|
+
>>> problem.addVariables(["a", "b"], [-1, 0, 1])
|
|
296
|
+
>>> problem.addConstraint(ExactSumConstraint(0))
|
|
297
|
+
>>> sorted(sorted(x.items()) for x in problem.getSolutions())
|
|
298
|
+
[[('a', -1), ('b', 1)], [('a', 0), ('b', 0)], [('a', 1), ('b', -1)]]
|
|
299
|
+
"""
|
|
300
|
+
|
|
301
|
+
def __init__(self, exactsum: Union[int, float], multipliers: Optional[Sequence] = None):
|
|
302
|
+
"""Initialization method.
|
|
303
|
+
|
|
304
|
+
Args:
|
|
305
|
+
exactsum (number): Value to be considered as the exact sum
|
|
306
|
+
multipliers (sequence of numbers): If given, variable values
|
|
307
|
+
will be multiplied by the given factors before being
|
|
308
|
+
summed to be checked
|
|
309
|
+
"""
|
|
310
|
+
self._exactsum = exactsum
|
|
311
|
+
self._multipliers = multipliers
|
|
312
|
+
self._var_max = {}
|
|
313
|
+
self._var_min = {}
|
|
314
|
+
self._var_is_negative = {}
|
|
315
|
+
|
|
316
|
+
def preProcess(self, variables: Sequence, domains: dict, constraints: list[tuple], vconstraints: dict): # noqa: D102
|
|
317
|
+
Constraint.preProcess(self, variables, domains, constraints, vconstraints)
|
|
318
|
+
multipliers = self._multipliers if self._multipliers else [1] * len(variables)
|
|
319
|
+
exactsum = self._exactsum
|
|
320
|
+
self._var_min = { variable: min(domains[variable]) * multiplier for variable, multiplier in zip(variables, multipliers) } # noqa: E501
|
|
321
|
+
self._var_max = { variable: max(domains[variable]) * multiplier for variable, multiplier in zip(variables, multipliers) } # noqa: E501
|
|
322
|
+
|
|
323
|
+
# preprocess the domains to remove values that cannot contribute to the exact sum
|
|
324
|
+
for variable, multiplier in zip(variables, multipliers):
|
|
325
|
+
domain = domains[variable]
|
|
326
|
+
other_vars_min = sum_other_vars(variables, variable, self._var_min)
|
|
327
|
+
other_vars_max = sum_other_vars(variables, variable, self._var_max)
|
|
328
|
+
for value in domain[:]:
|
|
329
|
+
if value * multiplier + other_vars_min > exactsum:
|
|
330
|
+
domain.remove(value)
|
|
331
|
+
if value * multiplier + other_vars_max < exactsum:
|
|
332
|
+
domain.remove(value)
|
|
333
|
+
|
|
334
|
+
# recalculate the min and max after pruning
|
|
335
|
+
self._var_max = { variable: max(domains[variable]) * multiplier if len(domains[variable]) > 0 else 0 for variable, multiplier in zip(variables, multipliers) } # noqa: E501
|
|
336
|
+
self._var_min = { variable: min(domains[variable]) * multiplier if len(domains[variable]) > 0 else 0 for variable, multiplier in zip(variables, multipliers) } # noqa: E501
|
|
337
|
+
self._var_is_negative = { variable: self._var_min[variable] < 0 for variable in variables }
|
|
338
|
+
|
|
339
|
+
def __call__(self, variables: Sequence, domains: dict, assignments: dict, forwardcheck=False): # noqa: D102
|
|
340
|
+
multipliers = self._multipliers
|
|
341
|
+
exactsum = self._exactsum
|
|
342
|
+
sum = 0
|
|
343
|
+
min_sum_missing = 0
|
|
344
|
+
max_sum_missing = 0
|
|
345
|
+
missing = False
|
|
346
|
+
missing_negative = False
|
|
347
|
+
if multipliers:
|
|
348
|
+
for variable, multiplier in zip(variables, multipliers):
|
|
349
|
+
if variable in assignments:
|
|
350
|
+
sum += assignments[variable] * multiplier
|
|
351
|
+
else:
|
|
352
|
+
min_sum_missing += self._var_min[variable]
|
|
353
|
+
max_sum_missing += self._var_max[variable]
|
|
354
|
+
missing = True
|
|
355
|
+
if self._var_is_negative[variable]:
|
|
356
|
+
missing_negative = True
|
|
357
|
+
if isinstance(sum, float):
|
|
358
|
+
sum = round(sum, 10)
|
|
359
|
+
if sum + min_sum_missing > exactsum or sum + max_sum_missing < exactsum:
|
|
360
|
+
return False
|
|
361
|
+
if forwardcheck and missing and not missing_negative:
|
|
362
|
+
for variable, multiplier in zip(variables, multipliers):
|
|
363
|
+
if variable not in assignments:
|
|
364
|
+
domain = domains[variable]
|
|
365
|
+
for value in domain[:]:
|
|
366
|
+
if sum + value * multiplier > exactsum:
|
|
367
|
+
domain.hideValue(value)
|
|
368
|
+
if not domain:
|
|
369
|
+
return False
|
|
370
|
+
else:
|
|
371
|
+
for variable in variables:
|
|
372
|
+
if variable in assignments:
|
|
373
|
+
sum += assignments[variable]
|
|
374
|
+
else:
|
|
375
|
+
min_sum_missing += self._var_min[variable]
|
|
376
|
+
max_sum_missing += self._var_max[variable]
|
|
377
|
+
missing = True
|
|
378
|
+
if self._var_is_negative[variable]:
|
|
379
|
+
missing_negative = True
|
|
380
|
+
if isinstance(sum, float):
|
|
381
|
+
sum = round(sum, 10)
|
|
382
|
+
if sum + min_sum_missing > exactsum or sum + max_sum_missing < exactsum:
|
|
383
|
+
return False
|
|
384
|
+
if forwardcheck and missing and not missing_negative:
|
|
385
|
+
for variable in variables:
|
|
386
|
+
if variable not in assignments:
|
|
387
|
+
domain = domains[variable]
|
|
388
|
+
for value in domain[:]:
|
|
389
|
+
if sum + value > exactsum:
|
|
390
|
+
domain.hideValue(value)
|
|
391
|
+
if not domain:
|
|
392
|
+
return False
|
|
393
|
+
if missing:
|
|
394
|
+
return sum + min_sum_missing <= exactsum and sum + max_sum_missing >= exactsum
|
|
395
|
+
else:
|
|
396
|
+
return sum == exactsum
|
|
397
|
+
|
|
398
|
+
class VariableExactSumConstraint(Constraint):
|
|
399
|
+
"""Constraint enforcing that the sum of variables equals the value of another variable.
|
|
400
|
+
|
|
401
|
+
Example:
|
|
402
|
+
>>> problem = Problem()
|
|
403
|
+
>>> problem.addVariables(["a", "b", "c"], [1, 2, 3])
|
|
404
|
+
>>> problem.addConstraint(VariableExactSumConstraint('c', ['a', 'b']))
|
|
405
|
+
>>> sorted(sorted(x.items()) for x in problem.getSolutions())
|
|
406
|
+
[[('a', 1), ('b', 1), ('c', 2)], [('a', 1), ('b', 2), ('c', 3)], [('a', 2), ('b', 1), ('c', 3)]]
|
|
407
|
+
>>> problem = Problem()
|
|
408
|
+
>>> problem.addVariable('a', [-1,0,1])
|
|
409
|
+
>>> problem.addVariable('b', [-1,0,1])
|
|
410
|
+
>>> problem.addVariable('c', [0, 2])
|
|
411
|
+
>>> problem.addConstraint(VariableExactSumConstraint('c', ['a', 'b']))
|
|
412
|
+
>>> sorted(sorted(x.items()) for x in problem.getSolutions())
|
|
413
|
+
[[('a', -1), ('b', 1), ('c', 0)], [('a', 0), ('b', 0), ('c', 0)], [('a', 1), ('b', -1), ('c', 0)], [('a', 1), ('b', 1), ('c', 2)]]
|
|
414
|
+
""" # noqa: E501
|
|
415
|
+
|
|
416
|
+
def __init__(self, target_var: str, sum_vars: Sequence[str], multipliers: Optional[Sequence] = None):
|
|
417
|
+
"""Initialization method.
|
|
418
|
+
|
|
419
|
+
Args:
|
|
420
|
+
target_var (Variable): The target variable to sum to.
|
|
421
|
+
sum_vars (sequence of Variables): The variables to sum up.
|
|
422
|
+
multipliers (sequence of numbers): If given, variable values
|
|
423
|
+
(except the last) will be multiplied by the given factors before being
|
|
424
|
+
summed to match the last variable.
|
|
425
|
+
"""
|
|
426
|
+
self.target_var = target_var
|
|
427
|
+
self.sum_vars = sum_vars
|
|
428
|
+
self._multipliers = multipliers
|
|
429
|
+
|
|
430
|
+
if multipliers:
|
|
431
|
+
assert len(multipliers) == len(sum_vars) + 1, "Multipliers must match sum variables and +1 for target."
|
|
432
|
+
assert all(isinstance(m, (int, float)) for m in multipliers), "Multipliers must be numbers."
|
|
433
|
+
assert multipliers[-1] == 1, "Last multiplier must be 1, as it is the target variable."
|
|
434
|
+
|
|
435
|
+
def preProcess(self, variables: Sequence, domains: dict, constraints: list[tuple], vconstraints: dict): # noqa: D102
|
|
436
|
+
Constraint.preProcess(self, variables, domains, constraints, vconstraints)
|
|
437
|
+
|
|
438
|
+
multipliers = self._multipliers
|
|
439
|
+
|
|
440
|
+
if multipliers:
|
|
441
|
+
for var, multiplier in zip(self.sum_vars, multipliers):
|
|
442
|
+
domain = domains[var]
|
|
443
|
+
for value in domain[:]:
|
|
444
|
+
if value * multiplier > max(domains[self.target_var]):
|
|
445
|
+
domain.remove(value)
|
|
446
|
+
else:
|
|
447
|
+
for var in self.sum_vars:
|
|
448
|
+
domain = domains[var]
|
|
449
|
+
others_min = sum(min(domains[v]) for v in self.sum_vars if v != var)
|
|
450
|
+
others_max = sum(max(domains[v]) for v in self.sum_vars if v != var)
|
|
451
|
+
for value in domain[:]:
|
|
452
|
+
if value + others_min > max(domains[self.target_var]):
|
|
453
|
+
domain.remove(value)
|
|
454
|
+
if value + others_max < min(domains[self.target_var]):
|
|
455
|
+
domain.remove(value)
|
|
456
|
+
|
|
457
|
+
def __call__(self, variables: Sequence, domains: dict, assignments: dict, forwardcheck=False): # noqa: D102
|
|
458
|
+
multipliers = self._multipliers
|
|
459
|
+
|
|
460
|
+
if self.target_var not in assignments:
|
|
461
|
+
return True # can't evaluate without target, defer to later
|
|
462
|
+
|
|
463
|
+
target_value = assignments[self.target_var]
|
|
464
|
+
sum_value = 0
|
|
465
|
+
missing = False
|
|
466
|
+
|
|
467
|
+
if multipliers:
|
|
468
|
+
for var, multiplier in zip(self.sum_vars, multipliers):
|
|
469
|
+
if var in assignments:
|
|
470
|
+
sum_value += assignments[var] * multiplier
|
|
471
|
+
else:
|
|
472
|
+
missing = True
|
|
473
|
+
else:
|
|
474
|
+
for var in self.sum_vars:
|
|
475
|
+
if var in assignments:
|
|
476
|
+
sum_value += assignments[var]
|
|
477
|
+
else:
|
|
478
|
+
sum_value += min(domains[var]) # use min value if not assigned
|
|
479
|
+
missing = True
|
|
480
|
+
|
|
481
|
+
if isinstance(sum_value, float):
|
|
482
|
+
sum_value = round(sum_value, 10)
|
|
483
|
+
|
|
484
|
+
if missing:
|
|
485
|
+
# Partial assignments: only check feasibility
|
|
486
|
+
if sum_value > target_value:
|
|
487
|
+
return False
|
|
488
|
+
if forwardcheck:
|
|
489
|
+
for var in self.sum_vars:
|
|
490
|
+
if var not in assignments:
|
|
491
|
+
domain = domains[var]
|
|
492
|
+
if multipliers:
|
|
493
|
+
for value in domain[:]:
|
|
494
|
+
temp_sum = sum_value + (value * multipliers[self.sum_vars.index(var)])
|
|
495
|
+
if temp_sum > target_value:
|
|
496
|
+
domain.hideValue(value)
|
|
497
|
+
else:
|
|
498
|
+
for value in domain[:]:
|
|
499
|
+
temp_sum = sum_value + value
|
|
500
|
+
if temp_sum > target_value:
|
|
501
|
+
domain.hideValue(value)
|
|
502
|
+
if not domain:
|
|
503
|
+
return False
|
|
504
|
+
return True
|
|
505
|
+
else:
|
|
506
|
+
return sum_value == target_value
|
|
507
|
+
|
|
508
|
+
|
|
509
|
+
class MinSumConstraint(Constraint):
|
|
510
|
+
"""Constraint enforcing that values of given variables sum at least to a given amount.
|
|
511
|
+
|
|
512
|
+
Example:
|
|
513
|
+
>>> problem = Problem()
|
|
514
|
+
>>> problem.addVariables(["a", "b"], [1, 2])
|
|
515
|
+
>>> problem.addConstraint(MinSumConstraint(3))
|
|
516
|
+
>>> sorted(sorted(x.items()) for x in problem.getSolutions())
|
|
517
|
+
[[('a', 1), ('b', 2)], [('a', 2), ('b', 1)], [('a', 2), ('b', 2)]]
|
|
518
|
+
>>> problem = Problem()
|
|
519
|
+
>>> problem.addVariables(["a", "b"], [-3, 1])
|
|
520
|
+
>>> problem.addConstraint(MinSumConstraint(-2))
|
|
521
|
+
>>> sorted(sorted(x.items()) for x in problem.getSolutions())
|
|
522
|
+
[[('a', -3), ('b', 1)], [('a', 1), ('b', -3)], [('a', 1), ('b', 1)]]
|
|
523
|
+
"""
|
|
524
|
+
|
|
525
|
+
def __init__(self, minsum: Union[int, float], multipliers: Optional[Sequence] = None):
|
|
526
|
+
"""Initialization method.
|
|
527
|
+
|
|
528
|
+
Args:
|
|
529
|
+
minsum (number): Value to be considered as the minimum sum
|
|
530
|
+
multipliers (sequence of numbers): If given, variable values
|
|
531
|
+
will be multiplied by the given factors before being
|
|
532
|
+
summed to be checked
|
|
533
|
+
"""
|
|
534
|
+
self._minsum = minsum
|
|
535
|
+
self._multipliers = multipliers
|
|
536
|
+
self._var_max = {}
|
|
537
|
+
|
|
538
|
+
def preProcess(self, variables: Sequence, domains: dict, constraints: list[tuple], vconstraints: dict): # noqa: D102
|
|
539
|
+
Constraint.preProcess(self, variables, domains, constraints, vconstraints)
|
|
540
|
+
multipliers = self._multipliers if self._multipliers else [1] * len(variables)
|
|
541
|
+
self._var_max = { variable: max(domains[variable]) * multiplier for variable, multiplier in zip(variables, multipliers) } # noqa: E501
|
|
542
|
+
|
|
543
|
+
# preprocess the domains to remove values that cannot contribute to the minimum sum
|
|
544
|
+
for variable, multiplier in zip(variables, multipliers):
|
|
545
|
+
domain = domains[variable]
|
|
546
|
+
others_max = sum_other_vars(variables, variable, self._var_max)
|
|
547
|
+
for value in domain[:]:
|
|
548
|
+
if value * multiplier + others_max < self._minsum:
|
|
549
|
+
domain.remove(value)
|
|
550
|
+
|
|
551
|
+
# recalculate the max after pruning
|
|
552
|
+
self._var_max = { variable: max(domains[variable]) * multiplier if len(domains[variable]) > 0 else 0 for variable, multiplier in zip(variables, multipliers) } # noqa: E501
|
|
553
|
+
|
|
554
|
+
def __call__(self, variables: Sequence, domains: dict, assignments: dict, forwardcheck=False): # noqa: D102
|
|
555
|
+
multipliers = self._multipliers
|
|
556
|
+
minsum = self._minsum
|
|
557
|
+
sum = 0
|
|
558
|
+
missing = False
|
|
559
|
+
max_sum_missing = 0
|
|
560
|
+
if multipliers:
|
|
561
|
+
for variable, multiplier in zip(variables, multipliers):
|
|
562
|
+
if variable in assignments:
|
|
563
|
+
sum += assignments[variable] * multiplier
|
|
564
|
+
else:
|
|
565
|
+
max_sum_missing += self._var_max[variable]
|
|
566
|
+
missing = True
|
|
567
|
+
else:
|
|
568
|
+
for variable in variables:
|
|
569
|
+
if variable in assignments:
|
|
570
|
+
sum += assignments[variable]
|
|
571
|
+
else:
|
|
572
|
+
max_sum_missing += self._var_max[variable]
|
|
573
|
+
missing = True
|
|
574
|
+
|
|
575
|
+
if isinstance(sum, float):
|
|
576
|
+
sum = round(sum, 10)
|
|
577
|
+
if sum + max_sum_missing < minsum:
|
|
578
|
+
return False
|
|
579
|
+
return sum >= minsum or missing
|
|
580
|
+
|
|
581
|
+
class VariableMinSumConstraint(Constraint):
|
|
582
|
+
"""Constraint enforcing that the sum of variables sum at least to the value of another variable.
|
|
583
|
+
|
|
584
|
+
Example:
|
|
585
|
+
>>> problem = Problem()
|
|
586
|
+
>>> problem.addVariables(["a", "b", "c"], [1, 4])
|
|
587
|
+
>>> problem.addConstraint(VariableMinSumConstraint('c', ['a', 'b']))
|
|
588
|
+
>>> sorted(sorted(x.items()) for x in problem.getSolutions())
|
|
589
|
+
[[('a', 1), ('b', 1), ('c', 1)], [('a', 1), ('b', 4), ('c', 1)], [('a', 1), ('b', 4), ('c', 4)], [('a', 4), ('b', 1), ('c', 1)], [('a', 4), ('b', 1), ('c', 4)], [('a', 4), ('b', 4), ('c', 1)], [('a', 4), ('b', 4), ('c', 4)]]
|
|
590
|
+
>>> problem = Problem()
|
|
591
|
+
>>> problem.addVariables(["a", "b"], [-3, 1])
|
|
592
|
+
>>> problem.addVariable('c', [-2, 2])
|
|
593
|
+
>>> problem.addConstraint(VariableMinSumConstraint('c', ['a', 'b']))
|
|
594
|
+
>>> sorted(sorted(x.items()) for x in problem.getSolutions())
|
|
595
|
+
[[('a', -3), ('b', 1), ('c', -2)], [('a', 1), ('b', -3), ('c', -2)], [('a', 1), ('b', 1), ('c', -2)], [('a', 1), ('b', 1), ('c', 2)]]
|
|
596
|
+
""" # noqa: E501
|
|
597
|
+
|
|
598
|
+
def __init__(self, target_var: str, sum_vars: Sequence[str], multipliers: Optional[Sequence] = None):
|
|
599
|
+
"""Initialization method.
|
|
600
|
+
|
|
601
|
+
Args:
|
|
602
|
+
target_var (Variable): The target variable to sum to.
|
|
603
|
+
sum_vars (sequence of Variables): The variables to sum up.
|
|
604
|
+
multipliers (sequence of numbers): If given, variable values
|
|
605
|
+
(except the last) will be multiplied by the given factors before being
|
|
606
|
+
summed to match the last variable.
|
|
607
|
+
"""
|
|
608
|
+
self.target_var = target_var
|
|
609
|
+
self.sum_vars = sum_vars
|
|
610
|
+
self._multipliers = multipliers
|
|
611
|
+
|
|
612
|
+
if multipliers:
|
|
613
|
+
assert len(multipliers) == len(sum_vars) + 1, "Multipliers must match sum variables and +1 for target."
|
|
614
|
+
assert all(isinstance(m, (int, float)) for m in multipliers), "Multipliers must be numbers."
|
|
615
|
+
assert multipliers[-1] == 1, "Last multiplier must be 1, as it is the target variable."
|
|
616
|
+
|
|
617
|
+
def preProcess(self, variables: Sequence, domains: dict, constraints: list[tuple], vconstraints: dict): # noqa: D102
|
|
618
|
+
Constraint.preProcess(self, variables, domains, constraints, vconstraints)
|
|
619
|
+
|
|
620
|
+
multipliers = self._multipliers
|
|
621
|
+
|
|
622
|
+
if not multipliers:
|
|
623
|
+
for var in self.sum_vars:
|
|
624
|
+
domain = domains[var]
|
|
625
|
+
others_max = sum(max(domains[v]) for v in self.sum_vars if v != var)
|
|
626
|
+
for value in domain[:]:
|
|
627
|
+
if value + others_max < min(domains[self.target_var]):
|
|
628
|
+
domain.remove(value)
|
|
629
|
+
|
|
630
|
+
def __call__(self, variables: Sequence, domains: dict, assignments: dict, forwardcheck=False): # noqa: D102
|
|
631
|
+
multipliers = self._multipliers
|
|
632
|
+
|
|
633
|
+
if self.target_var not in assignments:
|
|
634
|
+
return True # can't evaluate without target, defer to later
|
|
635
|
+
|
|
636
|
+
target_value = assignments[self.target_var]
|
|
637
|
+
sum_value = 0
|
|
638
|
+
|
|
639
|
+
if multipliers:
|
|
640
|
+
for var, multiplier in zip(self.sum_vars, multipliers):
|
|
641
|
+
if var in assignments:
|
|
642
|
+
sum_value += assignments[var] * multiplier
|
|
643
|
+
else:
|
|
644
|
+
sum_value += max(domains[var] * multiplier) # use max value if not assigned
|
|
645
|
+
else:
|
|
646
|
+
for var in self.sum_vars:
|
|
647
|
+
if var in assignments:
|
|
648
|
+
sum_value += assignments[var]
|
|
649
|
+
else:
|
|
650
|
+
sum_value += max(domains[var]) # use max value if not assigned
|
|
651
|
+
|
|
652
|
+
if isinstance(sum_value, float):
|
|
653
|
+
sum_value = round(sum_value, 10)
|
|
654
|
+
|
|
655
|
+
return sum_value >= target_value
|
|
656
|
+
|
|
657
|
+
|
|
658
|
+
class MaxSumConstraint(Constraint):
|
|
659
|
+
"""Constraint enforcing that values of given variables sum up to a given amount.
|
|
660
|
+
|
|
661
|
+
Example:
|
|
662
|
+
>>> problem = Problem()
|
|
663
|
+
>>> problem.addVariables(["a", "b"], [1, 2])
|
|
664
|
+
>>> problem.addConstraint(MaxSumConstraint(3))
|
|
665
|
+
>>> sorted(sorted(x.items()) for x in problem.getSolutions())
|
|
666
|
+
[[('a', 1), ('b', 1)], [('a', 1), ('b', 2)], [('a', 2), ('b', 1)]]
|
|
667
|
+
>>> problem = Problem()
|
|
668
|
+
>>> problem.addVariables(["a", "b"], [-3, 1])
|
|
669
|
+
>>> problem.addConstraint(MaxSumConstraint(-2))
|
|
670
|
+
>>> sorted(sorted(x.items()) for x in problem.getSolutions())
|
|
671
|
+
[[('a', -3), ('b', -3)], [('a', -3), ('b', 1)], [('a', 1), ('b', -3)]]
|
|
672
|
+
"""
|
|
673
|
+
|
|
674
|
+
def __init__(self, maxsum: Union[int, float], multipliers: Optional[Sequence] = None):
|
|
675
|
+
"""Initialization method.
|
|
676
|
+
|
|
677
|
+
Args:
|
|
678
|
+
maxsum (number): Value to be considered as the maximum sum
|
|
679
|
+
multipliers (sequence of numbers): If given, variable values
|
|
680
|
+
will be multiplied by the given factors before being
|
|
681
|
+
summed to be checked
|
|
682
|
+
"""
|
|
683
|
+
self._maxsum = maxsum
|
|
684
|
+
self._multipliers = multipliers
|
|
685
|
+
self._var_min = {}
|
|
686
|
+
self._var_is_negative = {}
|
|
687
|
+
|
|
688
|
+
def preProcess(self, variables: Sequence, domains: dict, constraints: list[tuple], vconstraints: dict): # noqa: D102
|
|
689
|
+
Constraint.preProcess(self, variables, domains, constraints, vconstraints)
|
|
690
|
+
multipliers = self._multipliers if self._multipliers else [1] * len(variables)
|
|
691
|
+
maxsum = self._maxsum
|
|
692
|
+
self._var_min = { variable: min(domains[variable]) * multiplier for variable, multiplier in zip(variables, multipliers) } # noqa: E501
|
|
693
|
+
|
|
694
|
+
# preprocess the domains to remove values that cannot contribute to the sum
|
|
695
|
+
for variable, multiplier in zip(variables, multipliers):
|
|
696
|
+
domain = domains[variable]
|
|
697
|
+
other_vars_min = sum_other_vars(variables, variable, self._var_min)
|
|
698
|
+
for value in domain[:]:
|
|
699
|
+
if value * multiplier + other_vars_min > maxsum:
|
|
700
|
+
domain.remove(value)
|
|
701
|
+
|
|
702
|
+
# recalculate the min after pruning
|
|
703
|
+
self._var_min = { variable: min(domains[variable]) * multiplier if len(domains[variable]) > 0 else 0 for variable, multiplier in zip(variables, multipliers) } # noqa: E501
|
|
704
|
+
self._var_is_negative = { variable: self._var_min[variable] < 0 for variable in variables }
|
|
705
|
+
|
|
706
|
+
def __call__(self, variables: Sequence, domains: dict, assignments: dict, forwardcheck=False): # noqa: D102
|
|
707
|
+
multipliers = self._multipliers
|
|
708
|
+
maxsum = self._maxsum
|
|
709
|
+
sum = 0
|
|
710
|
+
min_sum_missing = 0
|
|
711
|
+
missing = False
|
|
712
|
+
missing_negative = False
|
|
713
|
+
if multipliers:
|
|
714
|
+
for variable, multiplier in zip(variables, multipliers):
|
|
715
|
+
if variable in assignments:
|
|
716
|
+
sum += assignments[variable] * multiplier
|
|
717
|
+
else:
|
|
718
|
+
min_sum_missing += self._var_min[variable]
|
|
719
|
+
missing = True
|
|
720
|
+
if self._var_is_negative[variable]:
|
|
721
|
+
missing_negative = True
|
|
722
|
+
if isinstance(sum, float):
|
|
723
|
+
sum = round(sum, 10)
|
|
724
|
+
if sum + min_sum_missing > maxsum:
|
|
725
|
+
return False
|
|
726
|
+
if forwardcheck and missing and not missing_negative:
|
|
727
|
+
for variable, multiplier in zip(variables, multipliers):
|
|
728
|
+
if variable not in assignments:
|
|
729
|
+
domain = domains[variable]
|
|
730
|
+
for value in domain[:]:
|
|
731
|
+
if sum + value * multiplier > maxsum:
|
|
732
|
+
domain.hideValue(value)
|
|
733
|
+
if not domain:
|
|
734
|
+
return False
|
|
735
|
+
else:
|
|
736
|
+
for variable in variables:
|
|
737
|
+
if variable in assignments:
|
|
738
|
+
sum += assignments[variable]
|
|
739
|
+
else:
|
|
740
|
+
min_sum_missing += self._var_min[variable]
|
|
741
|
+
missing = True
|
|
742
|
+
if self._var_is_negative[variable]:
|
|
743
|
+
missing_negative = True
|
|
744
|
+
if isinstance(sum, float):
|
|
745
|
+
sum = round(sum, 10)
|
|
746
|
+
if sum + min_sum_missing > maxsum:
|
|
747
|
+
return False
|
|
748
|
+
if forwardcheck and missing and not missing_negative:
|
|
749
|
+
for variable in variables:
|
|
750
|
+
if variable not in assignments:
|
|
751
|
+
domain = domains[variable]
|
|
752
|
+
for value in domain[:]:
|
|
753
|
+
if sum + value > maxsum:
|
|
754
|
+
domain.hideValue(value)
|
|
755
|
+
if not domain:
|
|
756
|
+
return False
|
|
757
|
+
return True
|
|
758
|
+
|
|
759
|
+
|
|
760
|
+
class VariableMaxSumConstraint(Constraint):
|
|
761
|
+
"""Constraint enforcing that the sum of variables sum at most to the value of another variable.
|
|
762
|
+
|
|
763
|
+
Example:
|
|
764
|
+
>>> problem = Problem()
|
|
765
|
+
>>> problem.addVariables(["a", "b", "c"], [1, 3, 4])
|
|
766
|
+
>>> problem.addConstraint(VariableMaxSumConstraint('c', ['a', 'b']))
|
|
767
|
+
>>> sorted(sorted(x.items()) for x in problem.getSolutions())
|
|
768
|
+
[[('a', 1), ('b', 1), ('c', 3)], [('a', 1), ('b', 1), ('c', 4)], [('a', 1), ('b', 3), ('c', 4)], [('a', 3), ('b', 1), ('c', 4)]]
|
|
769
|
+
>>> problem = Problem()
|
|
770
|
+
>>> problem.addVariables(["a", "b"], [-2, 1])
|
|
771
|
+
>>> problem.addVariable('c', [-3, -1])
|
|
772
|
+
>>> problem.addConstraint(VariableMaxSumConstraint('c', ['a', 'b']))
|
|
773
|
+
>>> sorted(sorted(x.items()) for x in problem.getSolutions())
|
|
774
|
+
[[('a', -2), ('b', -2), ('c', -3)], [('a', -2), ('b', -2), ('c', -1)], [('a', -2), ('b', 1), ('c', -1)], [('a', 1), ('b', -2), ('c', -1)]]
|
|
775
|
+
""" # noqa: E501
|
|
776
|
+
|
|
777
|
+
def __init__(self, target_var: str, sum_vars: Sequence[str], multipliers: Optional[Sequence] = None):
|
|
778
|
+
"""Initialization method.
|
|
779
|
+
|
|
780
|
+
Args:
|
|
781
|
+
target_var (Variable): The target variable to sum to.
|
|
782
|
+
sum_vars (sequence of Variables): The variables to sum up.
|
|
783
|
+
multipliers (sequence of numbers): If given, variable values
|
|
784
|
+
(except the last) will be multiplied by the given factors before being
|
|
785
|
+
summed to match the last variable.
|
|
786
|
+
"""
|
|
787
|
+
self.target_var = target_var
|
|
788
|
+
self.sum_vars = sum_vars
|
|
789
|
+
self._multipliers = multipliers
|
|
790
|
+
|
|
791
|
+
if multipliers:
|
|
792
|
+
assert len(multipliers) == len(sum_vars) + 1, "Multipliers must match sum variables and +1 for target."
|
|
793
|
+
assert all(isinstance(m, (int, float)) for m in multipliers), "Multipliers must be numbers."
|
|
794
|
+
assert multipliers[-1] == 1, "Last multiplier must be 1, as it is the target variable."
|
|
795
|
+
|
|
796
|
+
def preProcess(self, variables: Sequence, domains: dict, constraints: list[tuple], vconstraints: dict): # noqa: D102
|
|
797
|
+
Constraint.preProcess(self, variables, domains, constraints, vconstraints)
|
|
798
|
+
|
|
799
|
+
multipliers = self._multipliers
|
|
800
|
+
|
|
801
|
+
if not multipliers:
|
|
802
|
+
for var in self.sum_vars:
|
|
803
|
+
domain = domains[var]
|
|
804
|
+
others_min = sum(min(domains[v]) for v in self.sum_vars if v != var)
|
|
805
|
+
for value in domain[:]:
|
|
806
|
+
if value + others_min > max(domains[self.target_var]):
|
|
807
|
+
domain.remove(value)
|
|
808
|
+
|
|
809
|
+
def __call__(self, variables: Sequence, domains: dict, assignments: dict, forwardcheck=False): # noqa: D102
|
|
810
|
+
multipliers = self._multipliers
|
|
811
|
+
|
|
812
|
+
if self.target_var not in assignments:
|
|
813
|
+
return True # can't evaluate without target, defer to later
|
|
814
|
+
|
|
815
|
+
target_value = assignments[self.target_var]
|
|
816
|
+
sum_value = 0
|
|
817
|
+
|
|
818
|
+
if multipliers:
|
|
819
|
+
for var, multiplier in zip(self.sum_vars, multipliers):
|
|
820
|
+
if var in assignments:
|
|
821
|
+
sum_value += assignments[var] * multiplier
|
|
822
|
+
else:
|
|
823
|
+
sum_value += min(domains[var] * multiplier) # use min value if not assigned
|
|
824
|
+
else:
|
|
825
|
+
for var in self.sum_vars:
|
|
826
|
+
if var in assignments:
|
|
827
|
+
sum_value += assignments[var]
|
|
828
|
+
else:
|
|
829
|
+
sum_value += min(domains[var]) # use min value if not assigned
|
|
830
|
+
|
|
831
|
+
if isinstance(sum_value, float):
|
|
832
|
+
sum_value = round(sum_value, 10)
|
|
833
|
+
|
|
834
|
+
return sum_value <= target_value
|
|
835
|
+
|
|
836
|
+
|
|
837
|
+
class ExactProdConstraint(Constraint):
|
|
838
|
+
"""Constraint enforcing that values of given variables create a product of exactly a given amount.
|
|
839
|
+
|
|
840
|
+
Example:
|
|
841
|
+
>>> problem = Problem()
|
|
842
|
+
>>> problem.addVariables(["a", "b"], [1, 2])
|
|
843
|
+
>>> problem.addConstraint(ExactProdConstraint(2))
|
|
844
|
+
>>> sorted(sorted(x.items()) for x in problem.getSolutions())
|
|
845
|
+
[[('a', 1), ('b', 2)], [('a', 2), ('b', 1)]]
|
|
846
|
+
>>> problem = Problem()
|
|
847
|
+
>>> problem.addVariables(["a", "b"], [-2, -1, 1, 2])
|
|
848
|
+
>>> problem.addConstraint(ExactProdConstraint(-2))
|
|
849
|
+
>>> sorted(sorted(x.items()) for x in problem.getSolutions())
|
|
850
|
+
[[('a', -2), ('b', 1)], [('a', -1), ('b', 2)], [('a', 1), ('b', -2)], [('a', 2), ('b', -1)]]
|
|
851
|
+
"""
|
|
852
|
+
|
|
853
|
+
def __init__(self, exactprod: Union[int, float]):
|
|
854
|
+
"""Instantiate an ExactProdConstraint.
|
|
855
|
+
|
|
856
|
+
Args:
|
|
857
|
+
exactprod: Value to be considered as the product
|
|
858
|
+
"""
|
|
859
|
+
self._exactprod = exactprod
|
|
860
|
+
self._variable_contains_lt1: list[bool] = list()
|
|
861
|
+
|
|
862
|
+
def preProcess(self, variables: Sequence, domains: dict, constraints: list[tuple], vconstraints: dict): # noqa: D102
|
|
863
|
+
Constraint.preProcess(self, variables, domains, constraints, vconstraints)
|
|
864
|
+
|
|
865
|
+
# check if there are any values less than 1 in the associated variables
|
|
866
|
+
self._variable_contains_lt1: list[bool] = list()
|
|
867
|
+
variable_with_lt1 = None
|
|
868
|
+
for variable in variables:
|
|
869
|
+
contains_lt1 = any(value < 1 for value in domains[variable])
|
|
870
|
+
self._variable_contains_lt1.append(contains_lt1)
|
|
871
|
+
for variable, contains_lt1 in zip(variables, self._variable_contains_lt1):
|
|
872
|
+
if contains_lt1 is True:
|
|
873
|
+
if variable_with_lt1 is not None:
|
|
874
|
+
# if more than one associated variables contain less than 1, we can't prune
|
|
875
|
+
return
|
|
876
|
+
variable_with_lt1 = variable
|
|
877
|
+
|
|
878
|
+
# prune the associated variables of values > exactprod
|
|
879
|
+
exactprod = self._exactprod
|
|
880
|
+
for variable in variables:
|
|
881
|
+
if variable_with_lt1 is not None and variable_with_lt1 != variable:
|
|
882
|
+
continue
|
|
883
|
+
domain = domains[variable]
|
|
884
|
+
for value in domain[:]:
|
|
885
|
+
if value > exactprod:
|
|
886
|
+
domain.remove(value)
|
|
887
|
+
elif value == 0 and exactprod != 0:
|
|
888
|
+
domain.remove(value)
|
|
889
|
+
|
|
890
|
+
def __call__(self, variables: Sequence, domains: dict, assignments: dict, forwardcheck=False): # noqa: D102
|
|
891
|
+
exactprod = self._exactprod
|
|
892
|
+
prod = 1
|
|
893
|
+
missing = False
|
|
894
|
+
missing_lt1 = []
|
|
895
|
+
# find out which variables contain values less than 1 if not preprocessed
|
|
896
|
+
if len(self._variable_contains_lt1) != len(variables):
|
|
897
|
+
for variable in variables:
|
|
898
|
+
self._variable_contains_lt1.append(any(value < 1 for value in domains[variable]))
|
|
899
|
+
for variable, contains_lt1 in zip(variables, self._variable_contains_lt1):
|
|
900
|
+
if variable in assignments:
|
|
901
|
+
prod *= assignments[variable]
|
|
902
|
+
else:
|
|
903
|
+
missing = True
|
|
904
|
+
if contains_lt1:
|
|
905
|
+
missing_lt1.append(variable)
|
|
906
|
+
if isinstance(prod, float):
|
|
907
|
+
prod = round(prod, 10)
|
|
908
|
+
if (not missing and prod != exactprod) or (len(missing_lt1) == 0 and prod > exactprod):
|
|
909
|
+
return False
|
|
910
|
+
if forwardcheck:
|
|
911
|
+
for variable in variables:
|
|
912
|
+
if variable not in assignments and (variable not in missing_lt1 or len(missing_lt1) == 1):
|
|
913
|
+
domain = domains[variable]
|
|
914
|
+
for value in domain[:]:
|
|
915
|
+
if prod * value > exactprod:
|
|
916
|
+
domain.hideValue(value)
|
|
917
|
+
if not domain:
|
|
918
|
+
return False
|
|
919
|
+
return True
|
|
920
|
+
|
|
921
|
+
class VariableExactProdConstraint(Constraint):
|
|
922
|
+
"""Constraint enforcing that the product of variables equals the value of another variable.
|
|
923
|
+
|
|
924
|
+
Example:
|
|
925
|
+
>>> problem = Problem()
|
|
926
|
+
>>> problem.addVariables(["a", "b", "c"], [1, 2])
|
|
927
|
+
>>> problem.addConstraint(VariableExactProdConstraint('c', ['a', 'b']))
|
|
928
|
+
>>> sorted(sorted(x.items()) for x in problem.getSolutions())
|
|
929
|
+
[[('a', 1), ('b', 1), ('c', 1)], [('a', 1), ('b', 2), ('c', 2)], [('a', 2), ('b', 1), ('c', 2)]]
|
|
930
|
+
>>> problem = Problem()
|
|
931
|
+
>>> problem.addVariables(["a", "b"], [-2, -1, 2])
|
|
932
|
+
>>> problem.addVariable('c', [-2, 1])
|
|
933
|
+
>>> problem.addConstraint(VariableExactProdConstraint('c', ['a', 'b']))
|
|
934
|
+
>>> sorted(sorted(x.items()) for x in problem.getSolutions())
|
|
935
|
+
[[('a', -1), ('b', -1), ('c', 1)], [('a', -1), ('b', 2), ('c', -2)], [('a', 2), ('b', -1), ('c', -2)]]
|
|
936
|
+
"""
|
|
937
|
+
|
|
938
|
+
def __init__(self, target_var: str, product_vars: Sequence[str]):
|
|
939
|
+
"""Instantiate a VariableExactProdConstraint.
|
|
940
|
+
|
|
941
|
+
Args:
|
|
942
|
+
target_var (Variable): The target variable to match.
|
|
943
|
+
product_vars (sequence of Variables): The variables to calculate the product of.
|
|
944
|
+
"""
|
|
945
|
+
self.target_var = target_var
|
|
946
|
+
self.product_vars = product_vars
|
|
947
|
+
|
|
948
|
+
def _get_product_bounds(self, domain_dict, exclude_var=None):
|
|
949
|
+
"""Return min and max product of domains of product_vars (excluding `exclude_var` if given)."""
|
|
950
|
+
bounds = []
|
|
951
|
+
for var in self.product_vars:
|
|
952
|
+
if var == exclude_var:
|
|
953
|
+
continue
|
|
954
|
+
dom = domain_dict[var]
|
|
955
|
+
if not dom:
|
|
956
|
+
continue
|
|
957
|
+
bounds.append((min(dom), max(dom)))
|
|
958
|
+
|
|
959
|
+
all_bounds = [b for b in bounds]
|
|
960
|
+
if not all_bounds:
|
|
961
|
+
return 1, 1
|
|
962
|
+
|
|
963
|
+
# Get all combinations of min/max to find global min/max product
|
|
964
|
+
candidates = [b for b in product(*[(lo, hi) for lo, hi in all_bounds])]
|
|
965
|
+
products = [self._safe_product(p) for p in candidates]
|
|
966
|
+
return min(products), max(products)
|
|
967
|
+
|
|
968
|
+
def _safe_product(self, values):
|
|
969
|
+
prod = 1
|
|
970
|
+
for v in values:
|
|
971
|
+
prod *= v
|
|
972
|
+
return prod
|
|
973
|
+
|
|
974
|
+
def preProcess(self, variables: Sequence, domains: dict, constraints: list[tuple], vconstraints: dict): # noqa: D102
|
|
975
|
+
Constraint.preProcess(self, variables, domains, constraints, vconstraints)
|
|
976
|
+
|
|
977
|
+
target_domain = domains[self.target_var]
|
|
978
|
+
target_min = min(target_domain)
|
|
979
|
+
target_max = max(target_domain)
|
|
980
|
+
for var in self.product_vars:
|
|
981
|
+
other_min, other_max = self._get_product_bounds(domains, exclude_var=var)
|
|
982
|
+
domain = domains[var]
|
|
983
|
+
for value in domain[:]:
|
|
984
|
+
candidates = [value * other_min, value * other_max]
|
|
985
|
+
minval, maxval = min(candidates), max(candidates)
|
|
986
|
+
if maxval < target_min or minval > target_max:
|
|
987
|
+
domain.remove(value)
|
|
988
|
+
|
|
989
|
+
def __call__(self, variables: Sequence, domains: dict, assignments: dict, forwardcheck=False): # noqa: D102
|
|
990
|
+
if self.target_var not in assignments:
|
|
991
|
+
return True
|
|
992
|
+
|
|
993
|
+
target_value = assignments[self.target_var]
|
|
994
|
+
assigned_product = 1
|
|
995
|
+
unassigned_vars = []
|
|
996
|
+
|
|
997
|
+
for var in self.product_vars:
|
|
998
|
+
if var in assignments:
|
|
999
|
+
assigned_product *= assignments[var]
|
|
1000
|
+
else:
|
|
1001
|
+
unassigned_vars.append(var)
|
|
1002
|
+
|
|
1003
|
+
if isinstance(assigned_product, float):
|
|
1004
|
+
assigned_product = round(assigned_product, 10)
|
|
1005
|
+
|
|
1006
|
+
if not unassigned_vars:
|
|
1007
|
+
return assigned_product == target_value
|
|
1008
|
+
|
|
1009
|
+
# Partial assignment – check feasibility
|
|
1010
|
+
domain_bounds = [(min(domains[v]), max(domains[v])) for v in unassigned_vars]
|
|
1011
|
+
candidates = [self._safe_product(p) for p in product(*[(lo, hi) for lo, hi in domain_bounds])]
|
|
1012
|
+
possible_min = min(assigned_product * c for c in candidates)
|
|
1013
|
+
possible_max = max(assigned_product * c for c in candidates)
|
|
1014
|
+
|
|
1015
|
+
if target_value < possible_min or target_value > possible_max:
|
|
1016
|
+
return False
|
|
1017
|
+
|
|
1018
|
+
# the below forwardcheck is incorrect for mixes of negative and positive values
|
|
1019
|
+
# if forwardcheck:
|
|
1020
|
+
# for var in unassigned_vars:
|
|
1021
|
+
# others = [v for v in unassigned_vars if v != var]
|
|
1022
|
+
# others_bounds = [(min(domains[v]), max(domains[v])) for v in others] or [(1, 1)]
|
|
1023
|
+
# other_products = [self._safe_product(p) for p in product(*[(lo, hi) for lo, hi in others_bounds])]
|
|
1024
|
+
|
|
1025
|
+
# domain = domains[var]
|
|
1026
|
+
# for value in domain[:]:
|
|
1027
|
+
# candidates = [assigned_product * value * p for p in other_products]
|
|
1028
|
+
# if all(c != target_value for c in candidates):
|
|
1029
|
+
# domain.hideValue(value)
|
|
1030
|
+
# if not domain:
|
|
1031
|
+
# return False
|
|
1032
|
+
|
|
1033
|
+
return True
|
|
1034
|
+
|
|
1035
|
+
|
|
1036
|
+
class MinProdConstraint(Constraint):
|
|
1037
|
+
"""Constraint enforcing that values of given variables create a product up to at least a given amount.
|
|
1038
|
+
|
|
1039
|
+
Example:
|
|
1040
|
+
>>> problem = Problem()
|
|
1041
|
+
>>> problem.addVariables(["a", "b"], [1, 2])
|
|
1042
|
+
>>> problem.addConstraint(MinProdConstraint(2))
|
|
1043
|
+
>>> sorted(sorted(x.items()) for x in problem.getSolutions())
|
|
1044
|
+
[[('a', 1), ('b', 2)], [('a', 2), ('b', 1)], [('a', 2), ('b', 2)]]
|
|
1045
|
+
>>> problem = Problem()
|
|
1046
|
+
>>> problem.addVariables(["a", "b"], [-2, -1, 1])
|
|
1047
|
+
>>> problem.addConstraint(MinProdConstraint(1))
|
|
1048
|
+
>>> sorted(sorted(x.items()) for x in problem.getSolutions())
|
|
1049
|
+
[[('a', -2), ('b', -2)], [('a', -2), ('b', -1)], [('a', -1), ('b', -2)], [('a', -1), ('b', -1)], [('a', 1), ('b', 1)]]
|
|
1050
|
+
""" # noqa: E501
|
|
1051
|
+
|
|
1052
|
+
def __init__(self, minprod: Union[int, float]):
|
|
1053
|
+
"""Instantiate a MinProdConstraint.
|
|
1054
|
+
|
|
1055
|
+
Args:
|
|
1056
|
+
minprod: Value to be considered as the maximum product
|
|
1057
|
+
"""
|
|
1058
|
+
self._minprod = minprod
|
|
1059
|
+
|
|
1060
|
+
def preProcess(self, variables: Sequence, domains: dict, constraints: list[tuple], vconstraints: dict): # noqa: D102
|
|
1061
|
+
Constraint.preProcess(self, variables, domains, constraints, vconstraints)
|
|
1062
|
+
|
|
1063
|
+
# prune the associated variables of values > minprod
|
|
1064
|
+
minprod = self._minprod
|
|
1065
|
+
for variable in variables:
|
|
1066
|
+
domain = domains[variable]
|
|
1067
|
+
for value in domain[:]:
|
|
1068
|
+
if value == 0 and minprod > 0:
|
|
1069
|
+
domain.remove(value)
|
|
1070
|
+
|
|
1071
|
+
def __call__(self, variables: Sequence, domains: dict, assignments: dict, forwardcheck=False): # noqa: D102
|
|
1072
|
+
# check if each variable is in the assignments
|
|
1073
|
+
for variable in variables:
|
|
1074
|
+
if variable not in assignments:
|
|
1075
|
+
return True
|
|
1076
|
+
|
|
1077
|
+
# with each variable assigned, sum the values
|
|
1078
|
+
minprod = self._minprod
|
|
1079
|
+
prod = 1
|
|
1080
|
+
for variable in variables:
|
|
1081
|
+
prod *= assignments[variable]
|
|
1082
|
+
if isinstance(prod, float):
|
|
1083
|
+
prod = round(prod, 10)
|
|
1084
|
+
return prod >= minprod
|
|
1085
|
+
|
|
1086
|
+
|
|
1087
|
+
class VariableMinProdConstraint(Constraint):
|
|
1088
|
+
"""Constraint enforcing that the product of variables is at least the value of another variable.
|
|
1089
|
+
|
|
1090
|
+
Example:
|
|
1091
|
+
>>> problem = Problem()
|
|
1092
|
+
>>> problem.addVariables(["a", "b", "c"], [-1, 2])
|
|
1093
|
+
>>> problem.addConstraint(VariableMinProdConstraint('c', ['a', 'b']))
|
|
1094
|
+
>>> sorted(sorted(x.items()) for x in problem.getSolutions())
|
|
1095
|
+
[[('a', -1), ('b', -1), ('c', -1)], [('a', 2), ('b', 2), ('c', -1)], [('a', 2), ('b', 2), ('c', 2)]]
|
|
1096
|
+
>>> problem = Problem()
|
|
1097
|
+
>>> problem.addVariables(["a", "b"], [-2, -1, 1])
|
|
1098
|
+
>>> problem.addVariable('c', [2, 5])
|
|
1099
|
+
>>> problem.addConstraint(VariableMinProdConstraint('c', ['a', 'b']))
|
|
1100
|
+
>>> sorted(sorted(x.items()) for x in problem.getSolutions())
|
|
1101
|
+
[[('a', -2), ('b', -2), ('c', 2)], [('a', -2), ('b', -1), ('c', 2)], [('a', -1), ('b', -2), ('c', 2)]]
|
|
1102
|
+
"""
|
|
1103
|
+
|
|
1104
|
+
def __init__(self, target_var: str, product_vars: Sequence[str]): # noqa: D107
|
|
1105
|
+
self.target_var = target_var
|
|
1106
|
+
self.product_vars = product_vars
|
|
1107
|
+
|
|
1108
|
+
def _get_product_bounds(self, domain_dict, exclude_var=None):
|
|
1109
|
+
bounds = []
|
|
1110
|
+
for var in self.product_vars:
|
|
1111
|
+
if var == exclude_var:
|
|
1112
|
+
continue
|
|
1113
|
+
dom = domain_dict[var]
|
|
1114
|
+
if not dom:
|
|
1115
|
+
continue
|
|
1116
|
+
bounds.append((min(dom), max(dom)))
|
|
1117
|
+
|
|
1118
|
+
if not bounds:
|
|
1119
|
+
return 1, 1
|
|
1120
|
+
|
|
1121
|
+
# Try all corner combinations
|
|
1122
|
+
candidates = [p for p in product(*[(lo, hi) for lo, hi in bounds])]
|
|
1123
|
+
products = [self._safe_product(p) for p in candidates]
|
|
1124
|
+
return min(products), max(products)
|
|
1125
|
+
|
|
1126
|
+
def _safe_product(self, values):
|
|
1127
|
+
prod = 1
|
|
1128
|
+
for v in values:
|
|
1129
|
+
prod *= v
|
|
1130
|
+
return prod
|
|
1131
|
+
|
|
1132
|
+
def preProcess(self, variables: Sequence, domains: dict, constraints: list[tuple], vconstraints: dict): # noqa: D102
|
|
1133
|
+
Constraint.preProcess(self, variables, domains, constraints, vconstraints)
|
|
1134
|
+
target_dom = domains[self.target_var]
|
|
1135
|
+
t_min = min(target_dom)
|
|
1136
|
+
|
|
1137
|
+
for var in self.product_vars:
|
|
1138
|
+
min_others, max_others = self._get_product_bounds(domains, exclude_var=var)
|
|
1139
|
+
dom = domains[var]
|
|
1140
|
+
for val in dom[:]:
|
|
1141
|
+
possible_prods = [val * min_others, val * max_others]
|
|
1142
|
+
if max(possible_prods) < t_min:
|
|
1143
|
+
dom.remove(val)
|
|
1144
|
+
|
|
1145
|
+
def __call__(self, variables: Sequence, domains: dict, assignments: dict, forwardcheck=False): # noqa: D102
|
|
1146
|
+
if self.target_var not in assignments:
|
|
1147
|
+
return True # Can't evaluate yet
|
|
1148
|
+
|
|
1149
|
+
target_value = assignments[self.target_var]
|
|
1150
|
+
assigned_prod = 1
|
|
1151
|
+
unassigned = []
|
|
1152
|
+
|
|
1153
|
+
for var in self.product_vars:
|
|
1154
|
+
if var in assignments:
|
|
1155
|
+
assigned_prod *= assignments[var]
|
|
1156
|
+
else:
|
|
1157
|
+
unassigned.append(var)
|
|
1158
|
+
|
|
1159
|
+
if not unassigned:
|
|
1160
|
+
return assigned_prod >= target_value
|
|
1161
|
+
|
|
1162
|
+
# Estimate min possible value of full product
|
|
1163
|
+
domain_bounds = [(min(domains[v]), max(domains[v])) for v in unassigned]
|
|
1164
|
+
candidates = [self._safe_product(p) for p in product(*[(lo, hi) for lo, hi in domain_bounds])]
|
|
1165
|
+
possible_prods = [assigned_prod * c for c in candidates]
|
|
1166
|
+
if max(possible_prods) < target_value:
|
|
1167
|
+
return False
|
|
1168
|
+
|
|
1169
|
+
if forwardcheck:
|
|
1170
|
+
for var in unassigned:
|
|
1171
|
+
other_unassigned = [v for v in unassigned if v != var]
|
|
1172
|
+
if other_unassigned:
|
|
1173
|
+
bounds = [(min(domains[v]), max(domains[v])) for v in other_unassigned]
|
|
1174
|
+
other_products = [self._safe_product(p) for p in product(*[(lo, hi) for lo, hi in bounds])]
|
|
1175
|
+
else:
|
|
1176
|
+
other_products = [1]
|
|
1177
|
+
|
|
1178
|
+
domain = domains[var]
|
|
1179
|
+
for val in domain[:]:
|
|
1180
|
+
prods = [assigned_prod * val * o for o in other_products]
|
|
1181
|
+
if all(p < target_value for p in prods):
|
|
1182
|
+
domain.hideValue(val)
|
|
1183
|
+
if not domain:
|
|
1184
|
+
return False
|
|
1185
|
+
|
|
1186
|
+
return True
|
|
1187
|
+
|
|
1188
|
+
|
|
1189
|
+
class MaxProdConstraint(Constraint):
|
|
1190
|
+
"""Constraint enforcing that values of given variables create a product up to at most a given amount.
|
|
1191
|
+
|
|
1192
|
+
Example:
|
|
1193
|
+
>>> problem = Problem()
|
|
1194
|
+
>>> problem.addVariables(["a", "b"], [1, 2])
|
|
1195
|
+
>>> problem.addConstraint(MaxProdConstraint(2))
|
|
1196
|
+
>>> sorted(sorted(x.items()) for x in problem.getSolutions())
|
|
1197
|
+
[[('a', 1), ('b', 1)], [('a', 1), ('b', 2)], [('a', 2), ('b', 1)]]
|
|
1198
|
+
>>> problem = Problem()
|
|
1199
|
+
>>> problem.addVariables(["a", "b"], [-2, -1, 1])
|
|
1200
|
+
>>> problem.addConstraint(MaxProdConstraint(-1))
|
|
1201
|
+
>>> sorted(sorted(x.items()) for x in problem.getSolutions())
|
|
1202
|
+
[[('a', -2), ('b', 1)], [('a', -1), ('b', 1)], [('a', 1), ('b', -2)], [('a', 1), ('b', -1)]]
|
|
1203
|
+
"""
|
|
1204
|
+
|
|
1205
|
+
def __init__(self, maxprod: Union[int, float]):
|
|
1206
|
+
"""Instantiate a MaxProdConstraint.
|
|
1207
|
+
|
|
1208
|
+
Args:
|
|
1209
|
+
maxprod: Value to be considered as the maximum product
|
|
1210
|
+
"""
|
|
1211
|
+
self._maxprod = maxprod
|
|
1212
|
+
self._variable_contains_lt1: list[bool] = list()
|
|
1213
|
+
|
|
1214
|
+
def preProcess(self, variables: Sequence, domains: dict, constraints: list[tuple], vconstraints: dict): # noqa: D102
|
|
1215
|
+
Constraint.preProcess(self, variables, domains, constraints, vconstraints)
|
|
1216
|
+
|
|
1217
|
+
# check if there are any values less than 1 in the associated variables
|
|
1218
|
+
self._variable_contains_lt1: list[bool] = list()
|
|
1219
|
+
variable_with_lt1 = None
|
|
1220
|
+
for variable in variables:
|
|
1221
|
+
contains_lt1 = any(value < 1 for value in domains[variable])
|
|
1222
|
+
self._variable_contains_lt1.append(contains_lt1)
|
|
1223
|
+
for variable, contains_lt1 in zip(variables, self._variable_contains_lt1):
|
|
1224
|
+
if contains_lt1 is True:
|
|
1225
|
+
if variable_with_lt1 is not None:
|
|
1226
|
+
# if more than one associated variables contain less than 1, we can't prune
|
|
1227
|
+
return
|
|
1228
|
+
variable_with_lt1 = variable
|
|
1229
|
+
|
|
1230
|
+
# prune the associated variables of values > maxprod
|
|
1231
|
+
maxprod = self._maxprod
|
|
1232
|
+
for variable in variables:
|
|
1233
|
+
if variable_with_lt1 is not None and variable_with_lt1 != variable:
|
|
1234
|
+
continue
|
|
1235
|
+
domain = domains[variable]
|
|
1236
|
+
for value in domain[:]:
|
|
1237
|
+
if value > maxprod:
|
|
1238
|
+
domain.remove(value)
|
|
1239
|
+
elif value == 0 and maxprod < 0:
|
|
1240
|
+
domain.remove(value)
|
|
1241
|
+
|
|
1242
|
+
def __call__(self, variables: Sequence, domains: dict, assignments: dict, forwardcheck=False): # noqa: D102
|
|
1243
|
+
maxprod = self._maxprod
|
|
1244
|
+
prod = 1
|
|
1245
|
+
missing = False
|
|
1246
|
+
missing_lt1 = []
|
|
1247
|
+
# find out which variables contain values less than 1 if not preprocessed
|
|
1248
|
+
if len(self._variable_contains_lt1) != len(variables):
|
|
1249
|
+
for variable in variables:
|
|
1250
|
+
self._variable_contains_lt1.append(any(value < 1 for value in domains[variable]))
|
|
1251
|
+
for variable, contains_lt1 in zip(variables, self._variable_contains_lt1):
|
|
1252
|
+
if variable in assignments:
|
|
1253
|
+
prod *= assignments[variable]
|
|
1254
|
+
else:
|
|
1255
|
+
missing = True
|
|
1256
|
+
if contains_lt1:
|
|
1257
|
+
missing_lt1.append(variable)
|
|
1258
|
+
if isinstance(prod, float):
|
|
1259
|
+
prod = round(prod, 10)
|
|
1260
|
+
if (not missing or len(missing_lt1) == 0) and prod > maxprod:
|
|
1261
|
+
return False
|
|
1262
|
+
if forwardcheck:
|
|
1263
|
+
for variable in variables:
|
|
1264
|
+
if variable not in assignments and (variable not in missing_lt1 or len(missing_lt1) == 1):
|
|
1265
|
+
domain = domains[variable]
|
|
1266
|
+
for value in domain[:]:
|
|
1267
|
+
if prod * value > maxprod:
|
|
1268
|
+
domain.hideValue(value)
|
|
1269
|
+
if not domain:
|
|
1270
|
+
return False
|
|
1271
|
+
return True
|
|
1272
|
+
|
|
1273
|
+
|
|
1274
|
+
class VariableMaxProdConstraint(Constraint):
|
|
1275
|
+
"""Constraint enforcing that the product of variables is at most the value of another variable.
|
|
1276
|
+
|
|
1277
|
+
Example:
|
|
1278
|
+
>>> problem = Problem()
|
|
1279
|
+
>>> problem.addVariables(["a", "b", "c"], [-1, 2])
|
|
1280
|
+
>>> problem.addConstraint(VariableMaxProdConstraint('c', ['a', 'b']))
|
|
1281
|
+
>>> sorted(sorted(x.items()) for x in problem.getSolutions())
|
|
1282
|
+
[[('a', -1), ('b', -1), ('c', 2)], [('a', -1), ('b', 2), ('c', -1)], [('a', -1), ('b', 2), ('c', 2)], [('a', 2), ('b', -1), ('c', -1)], [('a', 2), ('b', -1), ('c', 2)]]
|
|
1283
|
+
>>> problem = Problem()
|
|
1284
|
+
>>> problem.addVariables(["a", "b"], [-2, -1, 1])
|
|
1285
|
+
>>> problem.addVariable('c', [-2, -3])
|
|
1286
|
+
>>> problem.addConstraint(VariableMaxProdConstraint('c', ['a', 'b']))
|
|
1287
|
+
>>> sorted(sorted(x.items()) for x in problem.getSolutions())
|
|
1288
|
+
[[('a', -2), ('b', 1), ('c', -2)], [('a', 1), ('b', -2), ('c', -2)]]
|
|
1289
|
+
""" # noqa: E501
|
|
1290
|
+
|
|
1291
|
+
def __init__(self, target_var: str, product_vars: Sequence[str]): # noqa: D107
|
|
1292
|
+
self.target_var = target_var
|
|
1293
|
+
self.product_vars = product_vars
|
|
1294
|
+
|
|
1295
|
+
def _get_product_bounds(self, domain_dict, exclude_var=None):
|
|
1296
|
+
bounds = []
|
|
1297
|
+
for var in self.product_vars:
|
|
1298
|
+
if var == exclude_var:
|
|
1299
|
+
continue
|
|
1300
|
+
dom = domain_dict[var]
|
|
1301
|
+
if not dom:
|
|
1302
|
+
continue
|
|
1303
|
+
bounds.append((min(dom), max(dom)))
|
|
1304
|
+
|
|
1305
|
+
if not bounds:
|
|
1306
|
+
return 1, 1
|
|
1307
|
+
|
|
1308
|
+
# Try all corner combinations
|
|
1309
|
+
candidates = [p for p in product(*[(lo, hi) for lo, hi in bounds])]
|
|
1310
|
+
products = [self._safe_product(p) for p in candidates]
|
|
1311
|
+
return min(products), max(products)
|
|
1312
|
+
|
|
1313
|
+
def _safe_product(self, values):
|
|
1314
|
+
prod = 1
|
|
1315
|
+
for v in values:
|
|
1316
|
+
prod *= v
|
|
1317
|
+
return prod
|
|
1318
|
+
|
|
1319
|
+
def preProcess(self, variables: Sequence, domains: dict, constraints: list[tuple], vconstraints: dict): # noqa: D102
|
|
1320
|
+
Constraint.preProcess(self, variables, domains, constraints, vconstraints)
|
|
1321
|
+
target_dom = domains[self.target_var]
|
|
1322
|
+
t_max = max(target_dom)
|
|
1323
|
+
|
|
1324
|
+
for var in self.product_vars:
|
|
1325
|
+
min_others, max_others = self._get_product_bounds(domains, exclude_var=var)
|
|
1326
|
+
dom = domains[var]
|
|
1327
|
+
for val in dom[:]:
|
|
1328
|
+
possible_prods = [val * min_others, val * max_others]
|
|
1329
|
+
if min(possible_prods) > t_max:
|
|
1330
|
+
dom.remove(val)
|
|
1331
|
+
|
|
1332
|
+
def __call__(self, variables: Sequence, domains: dict, assignments: dict, forwardcheck=False): # noqa: D102
|
|
1333
|
+
if self.target_var not in assignments:
|
|
1334
|
+
return True # Can't evaluate yet
|
|
1335
|
+
|
|
1336
|
+
target_value = assignments[self.target_var]
|
|
1337
|
+
assigned_prod = 1
|
|
1338
|
+
unassigned = []
|
|
1339
|
+
|
|
1340
|
+
for var in self.product_vars:
|
|
1341
|
+
if var in assignments:
|
|
1342
|
+
assigned_prod *= assignments[var]
|
|
1343
|
+
else:
|
|
1344
|
+
unassigned.append(var)
|
|
1345
|
+
|
|
1346
|
+
if not unassigned:
|
|
1347
|
+
return assigned_prod <= target_value
|
|
1348
|
+
|
|
1349
|
+
# Estimate max possible value of full product
|
|
1350
|
+
domain_bounds = [(min(domains[v]), max(domains[v])) for v in unassigned]
|
|
1351
|
+
candidates = [self._safe_product(p) for p in product(*[(lo, hi) for lo, hi in domain_bounds])]
|
|
1352
|
+
possible_prods = [assigned_prod * c for c in candidates]
|
|
1353
|
+
if min(possible_prods) > target_value:
|
|
1354
|
+
return False
|
|
1355
|
+
|
|
1356
|
+
if forwardcheck:
|
|
1357
|
+
for var in unassigned:
|
|
1358
|
+
other_unassigned = [v for v in unassigned if v != var]
|
|
1359
|
+
if other_unassigned:
|
|
1360
|
+
bounds = [(min(domains[v]), max(domains[v])) for v in other_unassigned]
|
|
1361
|
+
other_products = [self._safe_product(p) for p in product(*[(lo, hi) for lo, hi in bounds])]
|
|
1362
|
+
else:
|
|
1363
|
+
other_products = [1]
|
|
1364
|
+
|
|
1365
|
+
domain = domains[var]
|
|
1366
|
+
for val in domain[:]:
|
|
1367
|
+
prods = [assigned_prod * val * o for o in other_products]
|
|
1368
|
+
if all(p > target_value for p in prods):
|
|
1369
|
+
domain.hideValue(val)
|
|
1370
|
+
if not domain:
|
|
1371
|
+
return False
|
|
1372
|
+
|
|
1373
|
+
return True
|
|
1374
|
+
|
|
1375
|
+
|
|
1376
|
+
class InSetConstraint(Constraint):
|
|
1377
|
+
"""Constraint enforcing that values of given variables are present in the given set.
|
|
1378
|
+
|
|
1379
|
+
Example:
|
|
1380
|
+
>>> problem = Problem()
|
|
1381
|
+
>>> problem.addVariables(["a", "b"], [1, 2])
|
|
1382
|
+
>>> problem.addConstraint(InSetConstraint([1]))
|
|
1383
|
+
>>> sorted(sorted(x.items()) for x in problem.getSolutions())
|
|
1384
|
+
[[('a', 1), ('b', 1)]]
|
|
1385
|
+
"""
|
|
1386
|
+
|
|
1387
|
+
def __init__(self, set):
|
|
1388
|
+
"""Initialization method.
|
|
1389
|
+
|
|
1390
|
+
Args:
|
|
1391
|
+
set (set): Set of allowed values
|
|
1392
|
+
"""
|
|
1393
|
+
self._set = set
|
|
1394
|
+
|
|
1395
|
+
def __call__(self, variables, domains, assignments, forwardcheck=False): # noqa: D102
|
|
1396
|
+
# preProcess() will remove it.
|
|
1397
|
+
raise RuntimeError("Can't happen")
|
|
1398
|
+
|
|
1399
|
+
def preProcess(self, variables: Sequence, domains: dict, constraints: list[tuple], vconstraints: dict): # noqa: D102
|
|
1400
|
+
set = self._set
|
|
1401
|
+
for variable in variables:
|
|
1402
|
+
domain = domains[variable]
|
|
1403
|
+
for value in domain[:]:
|
|
1404
|
+
if value not in set:
|
|
1405
|
+
domain.remove(value)
|
|
1406
|
+
vconstraints[variable].remove((self, variables))
|
|
1407
|
+
constraints.remove((self, variables))
|
|
1408
|
+
|
|
1409
|
+
|
|
1410
|
+
class NotInSetConstraint(Constraint):
|
|
1411
|
+
"""Constraint enforcing that values of given variables are not present in the given set.
|
|
1412
|
+
|
|
1413
|
+
Example:
|
|
1414
|
+
>>> problem = Problem()
|
|
1415
|
+
>>> problem.addVariables(["a", "b"], [1, 2])
|
|
1416
|
+
>>> problem.addConstraint(NotInSetConstraint([1]))
|
|
1417
|
+
>>> sorted(sorted(x.items()) for x in problem.getSolutions())
|
|
1418
|
+
[[('a', 2), ('b', 2)]]
|
|
1419
|
+
"""
|
|
1420
|
+
|
|
1421
|
+
def __init__(self, set):
|
|
1422
|
+
"""Initialization method.
|
|
1423
|
+
|
|
1424
|
+
Args:
|
|
1425
|
+
set (set): Set of disallowed values
|
|
1426
|
+
"""
|
|
1427
|
+
self._set = set
|
|
1428
|
+
|
|
1429
|
+
def __call__(self, variables, domains, assignments, forwardcheck=False): # noqa: D102
|
|
1430
|
+
# preProcess() will remove it.
|
|
1431
|
+
raise RuntimeError("Can't happen")
|
|
1432
|
+
|
|
1433
|
+
def preProcess(self, variables: Sequence, domains: dict, constraints: list[tuple], vconstraints: dict): # noqa: D102
|
|
1434
|
+
set = self._set
|
|
1435
|
+
for variable in variables:
|
|
1436
|
+
domain = domains[variable]
|
|
1437
|
+
for value in domain[:]:
|
|
1438
|
+
if value in set:
|
|
1439
|
+
domain.remove(value)
|
|
1440
|
+
vconstraints[variable].remove((self, variables))
|
|
1441
|
+
constraints.remove((self, variables))
|
|
1442
|
+
|
|
1443
|
+
|
|
1444
|
+
class SomeInSetConstraint(Constraint):
|
|
1445
|
+
"""Constraint enforcing that at least some of the values of given variables must be present in a given set.
|
|
1446
|
+
|
|
1447
|
+
Example:
|
|
1448
|
+
>>> problem = Problem()
|
|
1449
|
+
>>> problem.addVariables(["a", "b"], [1, 2])
|
|
1450
|
+
>>> problem.addConstraint(SomeInSetConstraint([1]))
|
|
1451
|
+
>>> sorted(sorted(x.items()) for x in problem.getSolutions())
|
|
1452
|
+
[[('a', 1), ('b', 1)], [('a', 1), ('b', 2)], [('a', 2), ('b', 1)]]
|
|
1453
|
+
"""
|
|
1454
|
+
|
|
1455
|
+
def __init__(self, set, n=1, exact=False):
|
|
1456
|
+
"""Initialization method.
|
|
1457
|
+
|
|
1458
|
+
Args:
|
|
1459
|
+
set (set): Set of values to be checked
|
|
1460
|
+
n (int): Minimum number of assigned values that should be
|
|
1461
|
+
present in set (default is 1)
|
|
1462
|
+
exact (bool): Whether the number of assigned values which
|
|
1463
|
+
are present in set must be exactly `n`
|
|
1464
|
+
"""
|
|
1465
|
+
self._set = set
|
|
1466
|
+
self._n = n
|
|
1467
|
+
self._exact = exact
|
|
1468
|
+
|
|
1469
|
+
def __call__(self, variables: Sequence, domains: dict, assignments: dict, forwardcheck=False): # noqa: D102
|
|
1470
|
+
set = self._set
|
|
1471
|
+
missing = 0
|
|
1472
|
+
found = 0
|
|
1473
|
+
for variable in variables:
|
|
1474
|
+
if variable in assignments:
|
|
1475
|
+
found += assignments[variable] in set
|
|
1476
|
+
else:
|
|
1477
|
+
missing += 1
|
|
1478
|
+
if missing:
|
|
1479
|
+
if self._exact:
|
|
1480
|
+
if not (found <= self._n <= missing + found):
|
|
1481
|
+
return False
|
|
1482
|
+
else:
|
|
1483
|
+
if self._n > missing + found:
|
|
1484
|
+
return False
|
|
1485
|
+
if forwardcheck and self._n - found == missing:
|
|
1486
|
+
# All unassigned variables must be assigned to
|
|
1487
|
+
# values in the set.
|
|
1488
|
+
for variable in variables:
|
|
1489
|
+
if variable not in assignments:
|
|
1490
|
+
domain = domains[variable]
|
|
1491
|
+
for value in domain[:]:
|
|
1492
|
+
if value not in set:
|
|
1493
|
+
domain.hideValue(value)
|
|
1494
|
+
if not domain:
|
|
1495
|
+
return False
|
|
1496
|
+
else:
|
|
1497
|
+
if self._exact:
|
|
1498
|
+
if found != self._n:
|
|
1499
|
+
return False
|
|
1500
|
+
else:
|
|
1501
|
+
if found < self._n:
|
|
1502
|
+
return False
|
|
1503
|
+
return True
|
|
1504
|
+
|
|
1505
|
+
|
|
1506
|
+
class SomeNotInSetConstraint(Constraint):
|
|
1507
|
+
"""Constraint enforcing that at least some of the values of given variables must not be present in a given set.
|
|
1508
|
+
|
|
1509
|
+
Example:
|
|
1510
|
+
>>> problem = Problem()
|
|
1511
|
+
>>> problem.addVariables(["a", "b"], [1, 2])
|
|
1512
|
+
>>> problem.addConstraint(SomeNotInSetConstraint([1]))
|
|
1513
|
+
>>> sorted(sorted(x.items()) for x in problem.getSolutions())
|
|
1514
|
+
[[('a', 1), ('b', 2)], [('a', 2), ('b', 1)], [('a', 2), ('b', 2)]]
|
|
1515
|
+
"""
|
|
1516
|
+
|
|
1517
|
+
def __init__(self, set, n=1, exact=False):
|
|
1518
|
+
"""Initialization method.
|
|
1519
|
+
|
|
1520
|
+
Args:
|
|
1521
|
+
set (set): Set of values to be checked
|
|
1522
|
+
n (int): Minimum number of assigned values that should not
|
|
1523
|
+
be present in set (default is 1)
|
|
1524
|
+
exact (bool): Whether the number of assigned values which
|
|
1525
|
+
are not present in set must be exactly `n`
|
|
1526
|
+
"""
|
|
1527
|
+
self._set = set
|
|
1528
|
+
self._n = n
|
|
1529
|
+
self._exact = exact
|
|
1530
|
+
|
|
1531
|
+
def __call__(self, variables: Sequence, domains: dict, assignments: dict, forwardcheck=False): # noqa: D102
|
|
1532
|
+
set = self._set
|
|
1533
|
+
missing = 0
|
|
1534
|
+
found = 0
|
|
1535
|
+
for variable in variables:
|
|
1536
|
+
if variable in assignments:
|
|
1537
|
+
found += assignments[variable] not in set
|
|
1538
|
+
else:
|
|
1539
|
+
missing += 1
|
|
1540
|
+
if missing:
|
|
1541
|
+
if self._exact:
|
|
1542
|
+
if not (found <= self._n <= missing + found):
|
|
1543
|
+
return False
|
|
1544
|
+
else:
|
|
1545
|
+
if self._n > missing + found:
|
|
1546
|
+
return False
|
|
1547
|
+
if forwardcheck and self._n - found == missing:
|
|
1548
|
+
# All unassigned variables must be assigned to
|
|
1549
|
+
# values not in the set.
|
|
1550
|
+
for variable in variables:
|
|
1551
|
+
if variable not in assignments:
|
|
1552
|
+
domain = domains[variable]
|
|
1553
|
+
for value in domain[:]:
|
|
1554
|
+
if value in set:
|
|
1555
|
+
domain.hideValue(value)
|
|
1556
|
+
if not domain:
|
|
1557
|
+
return False
|
|
1558
|
+
else:
|
|
1559
|
+
if self._exact:
|
|
1560
|
+
if found != self._n:
|
|
1561
|
+
return False
|
|
1562
|
+
else:
|
|
1563
|
+
if found < self._n:
|
|
1564
|
+
return False
|
|
1565
|
+
return True
|
|
1566
|
+
|
|
1567
|
+
|
|
1568
|
+
# Utility functions
|
|
1569
|
+
|
|
1570
|
+
def sum_other_vars(variables: Sequence, variable, values: dict):
|
|
1571
|
+
"""Calculate the sum of the given values of all other variables."""
|
|
1572
|
+
return sum(values[v] for v in variables if v != variable)
|