python-constraint2 2.0.0__cp313-cp313-win_amd64.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- constraint/__init__.py +37 -0
- constraint/constraints.c +29807 -0
- constraint/constraints.py +786 -0
- constraint/domain.c +9306 -0
- constraint/domain.py +102 -0
- constraint/problem.c +16329 -0
- constraint/problem.py +256 -0
- constraint/solvers.c +23963 -0
- constraint/solvers.py +592 -0
- python_constraint2-2.0.0.dist-info/LICENSE +23 -0
- python_constraint2-2.0.0.dist-info/METADATA +238 -0
- python_constraint2-2.0.0.dist-info/RECORD +22 -0
- python_constraint2-2.0.0.dist-info/WHEEL +4 -0
- tests/__init__.py +0 -0
- tests/setup_teardown.py +40 -0
- tests/test_compilation.py +17 -0
- tests/test_constraint.py +127 -0
- tests/test_doctests.py +10 -0
- tests/test_problem.py +27 -0
- tests/test_solvers.py +72 -0
- tests/test_some_not_in_set.py +99 -0
- tests/test_toml_file.py +72 -0
|
@@ -0,0 +1,786 @@
|
|
|
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
|
+
|
|
7
|
+
class Constraint:
|
|
8
|
+
"""Abstract base class for constraints."""
|
|
9
|
+
|
|
10
|
+
def __call__(self, variables: Sequence, domains: dict, assignments: dict, forwardcheck=False):
|
|
11
|
+
"""Perform the constraint checking.
|
|
12
|
+
|
|
13
|
+
If the forwardcheck parameter is not false, besides telling if
|
|
14
|
+
the constraint is currently broken or not, the constraint
|
|
15
|
+
implementation may choose to hide values from the domains of
|
|
16
|
+
unassigned variables to prevent them from being used, and thus
|
|
17
|
+
prune the search space.
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
variables (sequence): :py:class:`Variables` affected by that constraint,
|
|
21
|
+
in the same order provided by the user
|
|
22
|
+
domains (dict): Dictionary mapping variables to their
|
|
23
|
+
domains
|
|
24
|
+
assignments (dict): Dictionary mapping assigned variables to
|
|
25
|
+
their current assumed value
|
|
26
|
+
forwardcheck: Boolean value stating whether forward checking
|
|
27
|
+
should be performed or not
|
|
28
|
+
|
|
29
|
+
Returns:
|
|
30
|
+
bool: Boolean value stating if this constraint is currently
|
|
31
|
+
broken or not
|
|
32
|
+
"""
|
|
33
|
+
return True
|
|
34
|
+
|
|
35
|
+
def preProcess(self, variables: Sequence, domains: dict, constraints: list[tuple], vconstraints: dict):
|
|
36
|
+
"""Preprocess variable domains.
|
|
37
|
+
|
|
38
|
+
This method is called before starting to look for solutions,
|
|
39
|
+
and is used to prune domains with specific constraint logic
|
|
40
|
+
when possible. For instance, any constraints with a single
|
|
41
|
+
variable may be applied on all possible values and removed,
|
|
42
|
+
since they may act on individual values even without further
|
|
43
|
+
knowledge about other assignments.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
variables (sequence): Variables affected by that constraint,
|
|
47
|
+
in the same order provided by the user
|
|
48
|
+
domains (dict): Dictionary mapping variables to their
|
|
49
|
+
domains
|
|
50
|
+
constraints (list): List of pairs of (constraint, variables)
|
|
51
|
+
vconstraints (dict): Dictionary mapping variables to a list
|
|
52
|
+
of constraints affecting the given variables.
|
|
53
|
+
"""
|
|
54
|
+
if len(variables) == 1:
|
|
55
|
+
variable = variables[0]
|
|
56
|
+
domain = domains[variable]
|
|
57
|
+
for value in domain[:]:
|
|
58
|
+
if not self(variables, domains, {variable: value}):
|
|
59
|
+
domain.remove(value)
|
|
60
|
+
constraints.remove((self, variables))
|
|
61
|
+
vconstraints[variable].remove((self, variables))
|
|
62
|
+
|
|
63
|
+
def forwardCheck(self, variables: Sequence, domains: dict, assignments: dict, _unassigned=Unassigned):
|
|
64
|
+
"""Helper method for generic forward checking.
|
|
65
|
+
|
|
66
|
+
Currently, this method acts only when there's a single
|
|
67
|
+
unassigned variable.
|
|
68
|
+
|
|
69
|
+
Args:
|
|
70
|
+
variables (sequence): Variables affected by that constraint,
|
|
71
|
+
in the same order provided by the user
|
|
72
|
+
domains (dict): Dictionary mapping variables to their
|
|
73
|
+
domains
|
|
74
|
+
assignments (dict): Dictionary mapping assigned variables to
|
|
75
|
+
their current assumed value
|
|
76
|
+
|
|
77
|
+
Returns:
|
|
78
|
+
bool: Boolean value stating if this constraint is currently
|
|
79
|
+
broken or not
|
|
80
|
+
"""
|
|
81
|
+
unassignedvariable = _unassigned
|
|
82
|
+
for variable in variables:
|
|
83
|
+
if variable not in assignments:
|
|
84
|
+
if unassignedvariable is _unassigned:
|
|
85
|
+
unassignedvariable = variable
|
|
86
|
+
else:
|
|
87
|
+
break
|
|
88
|
+
else:
|
|
89
|
+
if unassignedvariable is not _unassigned:
|
|
90
|
+
# Remove from the unassigned variable domain's all
|
|
91
|
+
# values which break our variable's constraints.
|
|
92
|
+
domain = domains[unassignedvariable]
|
|
93
|
+
if domain:
|
|
94
|
+
for value in domain[:]:
|
|
95
|
+
assignments[unassignedvariable] = value
|
|
96
|
+
if not self(variables, domains, assignments):
|
|
97
|
+
domain.hideValue(value)
|
|
98
|
+
del assignments[unassignedvariable]
|
|
99
|
+
if not domain:
|
|
100
|
+
return False
|
|
101
|
+
return True
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
class FunctionConstraint(Constraint):
|
|
105
|
+
"""Constraint which wraps a function defining the constraint logic.
|
|
106
|
+
|
|
107
|
+
Examples:
|
|
108
|
+
>>> problem = Problem()
|
|
109
|
+
>>> problem.addVariables(["a", "b"], [1, 2])
|
|
110
|
+
>>> def func(a, b):
|
|
111
|
+
... return b > a
|
|
112
|
+
>>> problem.addConstraint(func, ["a", "b"])
|
|
113
|
+
>>> problem.getSolution()
|
|
114
|
+
{'a': 1, 'b': 2}
|
|
115
|
+
|
|
116
|
+
>>> problem = Problem()
|
|
117
|
+
>>> problem.addVariables(["a", "b"], [1, 2])
|
|
118
|
+
>>> def func(a, b):
|
|
119
|
+
... return b > a
|
|
120
|
+
>>> problem.addConstraint(FunctionConstraint(func), ["a", "b"])
|
|
121
|
+
>>> problem.getSolution()
|
|
122
|
+
{'a': 1, 'b': 2}
|
|
123
|
+
"""
|
|
124
|
+
|
|
125
|
+
def __init__(self, func: Callable, assigned: bool = True):
|
|
126
|
+
"""Initialization method.
|
|
127
|
+
|
|
128
|
+
Args:
|
|
129
|
+
func (callable object): Function wrapped and queried for
|
|
130
|
+
constraint logic
|
|
131
|
+
assigned (bool): Whether the function may receive unassigned
|
|
132
|
+
variables or not
|
|
133
|
+
"""
|
|
134
|
+
self._func = func
|
|
135
|
+
self._assigned = assigned
|
|
136
|
+
|
|
137
|
+
def __call__( # noqa: D102
|
|
138
|
+
self,
|
|
139
|
+
variables: Sequence,
|
|
140
|
+
domains: dict,
|
|
141
|
+
assignments: dict,
|
|
142
|
+
forwardcheck=False,
|
|
143
|
+
_unassigned=Unassigned,
|
|
144
|
+
):
|
|
145
|
+
# # initial code: 0.94621 seconds, Cythonized: 0.92805 seconds
|
|
146
|
+
# parms = [assignments.get(x, _unassigned) for x in variables]
|
|
147
|
+
# missing = parms.count(_unassigned)
|
|
148
|
+
|
|
149
|
+
# # list comprehension and sum: 0.13744 seconds, Cythonized: 0.10059 seconds
|
|
150
|
+
# parms = [assignments.get(x, _unassigned) for x in variables]
|
|
151
|
+
# missing = sum(x not in assignments for x in variables)
|
|
152
|
+
|
|
153
|
+
# # sum check with fallback: , Cythonized: 0.10108 seconds
|
|
154
|
+
# missing = sum(x not in assignments for x in variables)
|
|
155
|
+
# parms = [assignments.get(x, _unassigned) for x in variables] if missing > 0 else [assignments[x] for x in var]
|
|
156
|
+
|
|
157
|
+
# # tuple list comprehension with unzipping: 0.14521 seconds, Cythonized: 0.12054 seconds
|
|
158
|
+
# lst = [(assignments[x], 0) if x in assignments else (_unassigned, 1) for x in variables]
|
|
159
|
+
# parms, missing_iter = zip(*lst)
|
|
160
|
+
# parms = list(parms)
|
|
161
|
+
# missing = sum(missing_iter)
|
|
162
|
+
|
|
163
|
+
# # single loop array: 0.11249 seconds, Cythonized: 0.09514 seconds
|
|
164
|
+
# parms = [None] * len(variables)
|
|
165
|
+
# missing = 0
|
|
166
|
+
# for i, x in enumerate(variables):
|
|
167
|
+
# if x in assignments:
|
|
168
|
+
# parms[i] = assignments[x]
|
|
169
|
+
# else:
|
|
170
|
+
# parms[i] = _unassigned
|
|
171
|
+
# missing += 1
|
|
172
|
+
|
|
173
|
+
# single loop list: 0.11462 seconds, Cythonized: 0.08686 seconds
|
|
174
|
+
parms = list()
|
|
175
|
+
missing = 0
|
|
176
|
+
for x in variables:
|
|
177
|
+
if x in assignments:
|
|
178
|
+
parms.append(assignments[x])
|
|
179
|
+
else:
|
|
180
|
+
parms.append(_unassigned)
|
|
181
|
+
missing += 1
|
|
182
|
+
|
|
183
|
+
# if there are unassigned variables, do a forward check before executing the restriction function
|
|
184
|
+
if missing > 0:
|
|
185
|
+
return (self._assigned or self._func(*parms)) and (
|
|
186
|
+
not forwardcheck or missing != 1 or self.forwardCheck(variables, domains, assignments)
|
|
187
|
+
)
|
|
188
|
+
return self._func(*parms)
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
class AllDifferentConstraint(Constraint):
|
|
192
|
+
"""Constraint enforcing that values of all given variables are different.
|
|
193
|
+
|
|
194
|
+
Example:
|
|
195
|
+
>>> problem = Problem()
|
|
196
|
+
>>> problem.addVariables(["a", "b"], [1, 2])
|
|
197
|
+
>>> problem.addConstraint(AllDifferentConstraint())
|
|
198
|
+
>>> sorted(sorted(x.items()) for x in problem.getSolutions())
|
|
199
|
+
[[('a', 1), ('b', 2)], [('a', 2), ('b', 1)]]
|
|
200
|
+
"""
|
|
201
|
+
|
|
202
|
+
def __call__( # noqa: D102
|
|
203
|
+
self,
|
|
204
|
+
variables: Sequence,
|
|
205
|
+
domains: dict,
|
|
206
|
+
assignments: dict,
|
|
207
|
+
forwardcheck=False,
|
|
208
|
+
_unassigned=Unassigned,
|
|
209
|
+
):
|
|
210
|
+
seen = {}
|
|
211
|
+
for variable in variables:
|
|
212
|
+
value = assignments.get(variable, _unassigned)
|
|
213
|
+
if value is not _unassigned:
|
|
214
|
+
if value in seen:
|
|
215
|
+
return False
|
|
216
|
+
seen[value] = True
|
|
217
|
+
if forwardcheck:
|
|
218
|
+
for variable in variables:
|
|
219
|
+
if variable not in assignments:
|
|
220
|
+
domain = domains[variable]
|
|
221
|
+
for value in seen:
|
|
222
|
+
if value in domain:
|
|
223
|
+
domain.hideValue(value)
|
|
224
|
+
if not domain:
|
|
225
|
+
return False
|
|
226
|
+
return True
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
class AllEqualConstraint(Constraint):
|
|
230
|
+
"""Constraint enforcing that values of all given variables are equal.
|
|
231
|
+
|
|
232
|
+
Example:
|
|
233
|
+
>>> problem = Problem()
|
|
234
|
+
>>> problem.addVariables(["a", "b"], [1, 2])
|
|
235
|
+
>>> problem.addConstraint(AllEqualConstraint())
|
|
236
|
+
>>> sorted(sorted(x.items()) for x in problem.getSolutions())
|
|
237
|
+
[[('a', 1), ('b', 1)], [('a', 2), ('b', 2)]]
|
|
238
|
+
"""
|
|
239
|
+
|
|
240
|
+
def __call__( # noqa: D102
|
|
241
|
+
self,
|
|
242
|
+
variables: Sequence,
|
|
243
|
+
domains: dict,
|
|
244
|
+
assignments: dict,
|
|
245
|
+
forwardcheck=False,
|
|
246
|
+
_unassigned=Unassigned,
|
|
247
|
+
):
|
|
248
|
+
singlevalue = _unassigned
|
|
249
|
+
for variable in variables:
|
|
250
|
+
value = assignments.get(variable, _unassigned)
|
|
251
|
+
if singlevalue is _unassigned:
|
|
252
|
+
singlevalue = value
|
|
253
|
+
elif value is not _unassigned and value != singlevalue:
|
|
254
|
+
return False
|
|
255
|
+
if forwardcheck and singlevalue is not _unassigned:
|
|
256
|
+
for variable in variables:
|
|
257
|
+
if variable not in assignments:
|
|
258
|
+
domain = domains[variable]
|
|
259
|
+
if singlevalue not in domain:
|
|
260
|
+
return False
|
|
261
|
+
for value in domain[:]:
|
|
262
|
+
if value != singlevalue:
|
|
263
|
+
domain.hideValue(value)
|
|
264
|
+
return True
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
class MaxSumConstraint(Constraint):
|
|
268
|
+
"""Constraint enforcing that values of given variables sum up to a given amount.
|
|
269
|
+
|
|
270
|
+
Example:
|
|
271
|
+
>>> problem = Problem()
|
|
272
|
+
>>> problem.addVariables(["a", "b"], [1, 2])
|
|
273
|
+
>>> problem.addConstraint(MaxSumConstraint(3))
|
|
274
|
+
>>> sorted(sorted(x.items()) for x in problem.getSolutions())
|
|
275
|
+
[[('a', 1), ('b', 1)], [('a', 1), ('b', 2)], [('a', 2), ('b', 1)]]
|
|
276
|
+
"""
|
|
277
|
+
|
|
278
|
+
def __init__(self, maxsum: Union[int, float], multipliers: Optional[Sequence] = None):
|
|
279
|
+
"""Initialization method.
|
|
280
|
+
|
|
281
|
+
Args:
|
|
282
|
+
maxsum (number): Value to be considered as the maximum sum
|
|
283
|
+
multipliers (sequence of numbers): If given, variable values
|
|
284
|
+
will be multiplied by the given factors before being
|
|
285
|
+
summed to be checked
|
|
286
|
+
"""
|
|
287
|
+
self._maxsum = maxsum
|
|
288
|
+
self._multipliers = multipliers
|
|
289
|
+
|
|
290
|
+
def preProcess(self, variables: Sequence, domains: dict, constraints: list[tuple], vconstraints: dict): # noqa: D102
|
|
291
|
+
Constraint.preProcess(self, variables, domains, constraints, vconstraints)
|
|
292
|
+
|
|
293
|
+
# check if there are any negative values in the associated variables
|
|
294
|
+
variable_contains_negative: list[bool] = list()
|
|
295
|
+
variable_with_negative = None
|
|
296
|
+
for variable in variables:
|
|
297
|
+
contains_negative = any(value < 0 for value in domains[variable])
|
|
298
|
+
variable_contains_negative.append(contains_negative)
|
|
299
|
+
if contains_negative:
|
|
300
|
+
if variable_with_negative is not None:
|
|
301
|
+
# if more than one associated variables contain negative, we can't prune
|
|
302
|
+
return
|
|
303
|
+
variable_with_negative = variable
|
|
304
|
+
|
|
305
|
+
# prune the associated variables of values > maxsum
|
|
306
|
+
multipliers = self._multipliers
|
|
307
|
+
maxsum = self._maxsum
|
|
308
|
+
if multipliers:
|
|
309
|
+
for variable, multiplier in zip(variables, multipliers):
|
|
310
|
+
if variable_with_negative is not None and variable_with_negative != variable:
|
|
311
|
+
continue
|
|
312
|
+
domain = domains[variable]
|
|
313
|
+
for value in domain[:]:
|
|
314
|
+
if value * multiplier > maxsum:
|
|
315
|
+
domain.remove(value)
|
|
316
|
+
else:
|
|
317
|
+
for variable in variables:
|
|
318
|
+
if variable_with_negative is not None and variable_with_negative != variable:
|
|
319
|
+
continue
|
|
320
|
+
domain = domains[variable]
|
|
321
|
+
for value in domain[:]:
|
|
322
|
+
if value > maxsum:
|
|
323
|
+
domain.remove(value)
|
|
324
|
+
|
|
325
|
+
def __call__(self, variables: Sequence, domains: dict, assignments: dict, forwardcheck=False): # noqa: D102
|
|
326
|
+
multipliers = self._multipliers
|
|
327
|
+
maxsum = self._maxsum
|
|
328
|
+
sum = 0
|
|
329
|
+
if multipliers:
|
|
330
|
+
for variable, multiplier in zip(variables, multipliers):
|
|
331
|
+
if variable in assignments:
|
|
332
|
+
sum += assignments[variable] * multiplier
|
|
333
|
+
if isinstance(sum, float):
|
|
334
|
+
sum = round(sum, 10)
|
|
335
|
+
if sum > maxsum:
|
|
336
|
+
return False
|
|
337
|
+
if forwardcheck:
|
|
338
|
+
for variable, multiplier in zip(variables, multipliers):
|
|
339
|
+
if variable not in assignments:
|
|
340
|
+
domain = domains[variable]
|
|
341
|
+
for value in domain[:]:
|
|
342
|
+
if sum + value * multiplier > maxsum:
|
|
343
|
+
domain.hideValue(value)
|
|
344
|
+
if not domain:
|
|
345
|
+
return False
|
|
346
|
+
else:
|
|
347
|
+
for variable in variables:
|
|
348
|
+
if variable in assignments:
|
|
349
|
+
sum += assignments[variable]
|
|
350
|
+
if isinstance(sum, float):
|
|
351
|
+
sum = round(sum, 10)
|
|
352
|
+
if sum > maxsum:
|
|
353
|
+
return False
|
|
354
|
+
if forwardcheck:
|
|
355
|
+
for variable in variables:
|
|
356
|
+
if variable not in assignments:
|
|
357
|
+
domain = domains[variable]
|
|
358
|
+
for value in domain[:]:
|
|
359
|
+
if sum + value > maxsum:
|
|
360
|
+
domain.hideValue(value)
|
|
361
|
+
if not domain:
|
|
362
|
+
return False
|
|
363
|
+
return True
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
class ExactSumConstraint(Constraint):
|
|
367
|
+
"""Constraint enforcing that values of given variables sum exactly to a given amount.
|
|
368
|
+
|
|
369
|
+
Example:
|
|
370
|
+
>>> problem = Problem()
|
|
371
|
+
>>> problem.addVariables(["a", "b"], [1, 2])
|
|
372
|
+
>>> problem.addConstraint(ExactSumConstraint(3))
|
|
373
|
+
>>> sorted(sorted(x.items()) for x in problem.getSolutions())
|
|
374
|
+
[[('a', 1), ('b', 2)], [('a', 2), ('b', 1)]]
|
|
375
|
+
"""
|
|
376
|
+
|
|
377
|
+
def __init__(self, exactsum: Union[int, float], multipliers: Optional[Sequence] = None):
|
|
378
|
+
"""Initialization method.
|
|
379
|
+
|
|
380
|
+
Args:
|
|
381
|
+
exactsum (number): Value to be considered as the exact sum
|
|
382
|
+
multipliers (sequence of numbers): If given, variable values
|
|
383
|
+
will be multiplied by the given factors before being
|
|
384
|
+
summed to be checked
|
|
385
|
+
"""
|
|
386
|
+
self._exactsum = exactsum
|
|
387
|
+
self._multipliers = multipliers
|
|
388
|
+
|
|
389
|
+
def preProcess(self, variables: Sequence, domains: dict, constraints: list[tuple], vconstraints: dict): # noqa: D102
|
|
390
|
+
Constraint.preProcess(self, variables, domains, constraints, vconstraints)
|
|
391
|
+
multipliers = self._multipliers
|
|
392
|
+
exactsum = self._exactsum
|
|
393
|
+
if multipliers:
|
|
394
|
+
for variable, multiplier in zip(variables, multipliers):
|
|
395
|
+
domain = domains[variable]
|
|
396
|
+
for value in domain[:]:
|
|
397
|
+
if value * multiplier > exactsum:
|
|
398
|
+
domain.remove(value)
|
|
399
|
+
else:
|
|
400
|
+
for variable in variables:
|
|
401
|
+
domain = domains[variable]
|
|
402
|
+
for value in domain[:]:
|
|
403
|
+
if value > exactsum:
|
|
404
|
+
domain.remove(value)
|
|
405
|
+
|
|
406
|
+
def __call__(self, variables: Sequence, domains: dict, assignments: dict, forwardcheck=False): # noqa: D102
|
|
407
|
+
multipliers = self._multipliers
|
|
408
|
+
exactsum = self._exactsum
|
|
409
|
+
sum = 0
|
|
410
|
+
missing = False
|
|
411
|
+
if multipliers:
|
|
412
|
+
for variable, multiplier in zip(variables, multipliers):
|
|
413
|
+
if variable in assignments:
|
|
414
|
+
sum += assignments[variable] * multiplier
|
|
415
|
+
else:
|
|
416
|
+
missing = True
|
|
417
|
+
if isinstance(sum, float):
|
|
418
|
+
sum = round(sum, 10)
|
|
419
|
+
if sum > exactsum:
|
|
420
|
+
return False
|
|
421
|
+
if forwardcheck and missing:
|
|
422
|
+
for variable, multiplier in zip(variables, multipliers):
|
|
423
|
+
if variable not in assignments:
|
|
424
|
+
domain = domains[variable]
|
|
425
|
+
for value in domain[:]:
|
|
426
|
+
if sum + value * multiplier > exactsum:
|
|
427
|
+
domain.hideValue(value)
|
|
428
|
+
if not domain:
|
|
429
|
+
return False
|
|
430
|
+
else:
|
|
431
|
+
for variable in variables:
|
|
432
|
+
if variable in assignments:
|
|
433
|
+
sum += assignments[variable]
|
|
434
|
+
else:
|
|
435
|
+
missing = True
|
|
436
|
+
if isinstance(sum, float):
|
|
437
|
+
sum = round(sum, 10)
|
|
438
|
+
if sum > exactsum:
|
|
439
|
+
return False
|
|
440
|
+
if forwardcheck and missing:
|
|
441
|
+
for variable in variables:
|
|
442
|
+
if variable not in assignments:
|
|
443
|
+
domain = domains[variable]
|
|
444
|
+
for value in domain[:]:
|
|
445
|
+
if sum + value > exactsum:
|
|
446
|
+
domain.hideValue(value)
|
|
447
|
+
if not domain:
|
|
448
|
+
return False
|
|
449
|
+
if missing:
|
|
450
|
+
return sum <= exactsum
|
|
451
|
+
else:
|
|
452
|
+
return sum == exactsum
|
|
453
|
+
|
|
454
|
+
|
|
455
|
+
class MinSumConstraint(Constraint):
|
|
456
|
+
"""Constraint enforcing that values of given variables sum at least to a given amount.
|
|
457
|
+
|
|
458
|
+
Example:
|
|
459
|
+
>>> problem = Problem()
|
|
460
|
+
>>> problem.addVariables(["a", "b"], [1, 2])
|
|
461
|
+
>>> problem.addConstraint(MinSumConstraint(3))
|
|
462
|
+
>>> sorted(sorted(x.items()) for x in problem.getSolutions())
|
|
463
|
+
[[('a', 1), ('b', 2)], [('a', 2), ('b', 1)], [('a', 2), ('b', 2)]]
|
|
464
|
+
"""
|
|
465
|
+
|
|
466
|
+
def __init__(self, minsum: Union[int, float], multipliers: Optional[Sequence] = None):
|
|
467
|
+
"""Initialization method.
|
|
468
|
+
|
|
469
|
+
Args:
|
|
470
|
+
minsum (number): Value to be considered as the minimum sum
|
|
471
|
+
multipliers (sequence of numbers): If given, variable values
|
|
472
|
+
will be multiplied by the given factors before being
|
|
473
|
+
summed to be checked
|
|
474
|
+
"""
|
|
475
|
+
self._minsum = minsum
|
|
476
|
+
self._multipliers = multipliers
|
|
477
|
+
|
|
478
|
+
def __call__(self, variables: Sequence, domains: dict, assignments: dict, forwardcheck=False): # noqa: D102
|
|
479
|
+
# check if each variable is in the assignments
|
|
480
|
+
for variable in variables:
|
|
481
|
+
if variable not in assignments:
|
|
482
|
+
return True
|
|
483
|
+
|
|
484
|
+
# with each variable assigned, sum the values
|
|
485
|
+
multipliers = self._multipliers
|
|
486
|
+
minsum = self._minsum
|
|
487
|
+
sum = 0
|
|
488
|
+
if multipliers:
|
|
489
|
+
for variable, multiplier in zip(variables, multipliers):
|
|
490
|
+
sum += assignments[variable] * multiplier
|
|
491
|
+
else:
|
|
492
|
+
for variable in variables:
|
|
493
|
+
sum += assignments[variable]
|
|
494
|
+
if isinstance(sum, float):
|
|
495
|
+
sum = round(sum, 10)
|
|
496
|
+
return sum >= minsum
|
|
497
|
+
|
|
498
|
+
|
|
499
|
+
class MaxProdConstraint(Constraint):
|
|
500
|
+
"""Constraint enforcing that values of given variables create a product up to at most a given amount."""
|
|
501
|
+
|
|
502
|
+
def __init__(self, maxprod: Union[int, float]):
|
|
503
|
+
"""Instantiate a MaxProdConstraint.
|
|
504
|
+
|
|
505
|
+
Args:
|
|
506
|
+
maxprod: Value to be considered as the maximum product
|
|
507
|
+
"""
|
|
508
|
+
self._maxprod = maxprod
|
|
509
|
+
|
|
510
|
+
def preProcess(self, variables: Sequence, domains: dict, constraints: list[tuple], vconstraints: dict): # noqa: D102
|
|
511
|
+
Constraint.preProcess(self, variables, domains, constraints, vconstraints)
|
|
512
|
+
|
|
513
|
+
# check if there are any values less than 1 in the associated variables
|
|
514
|
+
variable_contains_lt1: list[bool] = list()
|
|
515
|
+
variable_with_lt1 = None
|
|
516
|
+
for variable in variables:
|
|
517
|
+
contains_lt1 = any(value < 1 for value in domains[variable])
|
|
518
|
+
variable_contains_lt1.append(contains_lt1)
|
|
519
|
+
if contains_lt1 is True:
|
|
520
|
+
if variable_with_lt1 is not None:
|
|
521
|
+
# if more than one associated variables contain less than 1, we can't prune
|
|
522
|
+
return
|
|
523
|
+
variable_with_lt1 = variable
|
|
524
|
+
|
|
525
|
+
# prune the associated variables of values > maxprod
|
|
526
|
+
maxprod = self._maxprod
|
|
527
|
+
for variable in variables:
|
|
528
|
+
if variable_with_lt1 is not None and variable_with_lt1 != variable:
|
|
529
|
+
continue
|
|
530
|
+
domain = domains[variable]
|
|
531
|
+
for value in domain[:]:
|
|
532
|
+
if value > maxprod:
|
|
533
|
+
domain.remove(value)
|
|
534
|
+
elif value == 0 and maxprod < 0:
|
|
535
|
+
domain.remove(value)
|
|
536
|
+
|
|
537
|
+
def __call__(self, variables: Sequence, domains: dict, assignments: dict, forwardcheck=False): # noqa: D102
|
|
538
|
+
maxprod = self._maxprod
|
|
539
|
+
prod = 1
|
|
540
|
+
for variable in variables:
|
|
541
|
+
if variable in assignments:
|
|
542
|
+
prod *= assignments[variable]
|
|
543
|
+
if isinstance(prod, float):
|
|
544
|
+
prod = round(prod, 10)
|
|
545
|
+
if prod > maxprod:
|
|
546
|
+
return False
|
|
547
|
+
if forwardcheck:
|
|
548
|
+
for variable in variables:
|
|
549
|
+
if variable not in assignments:
|
|
550
|
+
domain = domains[variable]
|
|
551
|
+
for value in domain[:]:
|
|
552
|
+
if prod * value > maxprod:
|
|
553
|
+
domain.hideValue(value)
|
|
554
|
+
if not domain:
|
|
555
|
+
return False
|
|
556
|
+
return True
|
|
557
|
+
|
|
558
|
+
|
|
559
|
+
class MinProdConstraint(Constraint):
|
|
560
|
+
"""Constraint enforcing that values of given variables create a product up to at least a given amount."""
|
|
561
|
+
|
|
562
|
+
def __init__(self, minprod: Union[int, float]):
|
|
563
|
+
"""Instantiate a MinProdConstraint.
|
|
564
|
+
|
|
565
|
+
Args:
|
|
566
|
+
minprod: Value to be considered as the maximum product
|
|
567
|
+
"""
|
|
568
|
+
self._minprod = minprod
|
|
569
|
+
|
|
570
|
+
def preProcess(self, variables: Sequence, domains: dict, constraints: list[tuple], vconstraints: dict): # noqa: D102
|
|
571
|
+
Constraint.preProcess(self, variables, domains, constraints, vconstraints)
|
|
572
|
+
|
|
573
|
+
# prune the associated variables of values > maxprod
|
|
574
|
+
minprod = self._minprod
|
|
575
|
+
for variable in variables:
|
|
576
|
+
domain = domains[variable]
|
|
577
|
+
for value in domain[:]:
|
|
578
|
+
if value == 0 and minprod > 0:
|
|
579
|
+
domain.remove(value)
|
|
580
|
+
|
|
581
|
+
def __call__(self, variables: Sequence, domains: dict, assignments: dict, forwardcheck=False): # noqa: D102
|
|
582
|
+
# check if each variable is in the assignments
|
|
583
|
+
for variable in variables:
|
|
584
|
+
if variable not in assignments:
|
|
585
|
+
return True
|
|
586
|
+
|
|
587
|
+
# with each variable assigned, sum the values
|
|
588
|
+
minprod = self._minprod
|
|
589
|
+
prod = 1
|
|
590
|
+
for variable in variables:
|
|
591
|
+
prod *= assignments[variable]
|
|
592
|
+
if isinstance(prod, float):
|
|
593
|
+
prod = round(prod, 10)
|
|
594
|
+
return prod >= minprod
|
|
595
|
+
|
|
596
|
+
|
|
597
|
+
class InSetConstraint(Constraint):
|
|
598
|
+
"""Constraint enforcing that values of given variables are present in the given set.
|
|
599
|
+
|
|
600
|
+
Example:
|
|
601
|
+
>>> problem = Problem()
|
|
602
|
+
>>> problem.addVariables(["a", "b"], [1, 2])
|
|
603
|
+
>>> problem.addConstraint(InSetConstraint([1]))
|
|
604
|
+
>>> sorted(sorted(x.items()) for x in problem.getSolutions())
|
|
605
|
+
[[('a', 1), ('b', 1)]]
|
|
606
|
+
"""
|
|
607
|
+
|
|
608
|
+
def __init__(self, set):
|
|
609
|
+
"""Initialization method.
|
|
610
|
+
|
|
611
|
+
Args:
|
|
612
|
+
set (set): Set of allowed values
|
|
613
|
+
"""
|
|
614
|
+
self._set = set
|
|
615
|
+
|
|
616
|
+
def __call__(self, variables, domains, assignments, forwardcheck=False): # noqa: D102
|
|
617
|
+
# preProcess() will remove it.
|
|
618
|
+
raise RuntimeError("Can't happen")
|
|
619
|
+
|
|
620
|
+
def preProcess(self, variables: Sequence, domains: dict, constraints: list[tuple], vconstraints: dict): # noqa: D102
|
|
621
|
+
set = self._set
|
|
622
|
+
for variable in variables:
|
|
623
|
+
domain = domains[variable]
|
|
624
|
+
for value in domain[:]:
|
|
625
|
+
if value not in set:
|
|
626
|
+
domain.remove(value)
|
|
627
|
+
vconstraints[variable].remove((self, variables))
|
|
628
|
+
constraints.remove((self, variables))
|
|
629
|
+
|
|
630
|
+
|
|
631
|
+
class NotInSetConstraint(Constraint):
|
|
632
|
+
"""Constraint enforcing that values of given variables are not present in the given set.
|
|
633
|
+
|
|
634
|
+
Example:
|
|
635
|
+
>>> problem = Problem()
|
|
636
|
+
>>> problem.addVariables(["a", "b"], [1, 2])
|
|
637
|
+
>>> problem.addConstraint(NotInSetConstraint([1]))
|
|
638
|
+
>>> sorted(sorted(x.items()) for x in problem.getSolutions())
|
|
639
|
+
[[('a', 2), ('b', 2)]]
|
|
640
|
+
"""
|
|
641
|
+
|
|
642
|
+
def __init__(self, set):
|
|
643
|
+
"""Initialization method.
|
|
644
|
+
|
|
645
|
+
Args:
|
|
646
|
+
set (set): Set of disallowed values
|
|
647
|
+
"""
|
|
648
|
+
self._set = set
|
|
649
|
+
|
|
650
|
+
def __call__(self, variables, domains, assignments, forwardcheck=False): # noqa: D102
|
|
651
|
+
# preProcess() will remove it.
|
|
652
|
+
raise RuntimeError("Can't happen")
|
|
653
|
+
|
|
654
|
+
def preProcess(self, variables: Sequence, domains: dict, constraints: list[tuple], vconstraints: dict): # noqa: D102
|
|
655
|
+
set = self._set
|
|
656
|
+
for variable in variables:
|
|
657
|
+
domain = domains[variable]
|
|
658
|
+
for value in domain[:]:
|
|
659
|
+
if value in set:
|
|
660
|
+
domain.remove(value)
|
|
661
|
+
vconstraints[variable].remove((self, variables))
|
|
662
|
+
constraints.remove((self, variables))
|
|
663
|
+
|
|
664
|
+
|
|
665
|
+
class SomeInSetConstraint(Constraint):
|
|
666
|
+
"""Constraint enforcing that at least some of the values of given variables must be present in a given set.
|
|
667
|
+
|
|
668
|
+
Example:
|
|
669
|
+
>>> problem = Problem()
|
|
670
|
+
>>> problem.addVariables(["a", "b"], [1, 2])
|
|
671
|
+
>>> problem.addConstraint(SomeInSetConstraint([1]))
|
|
672
|
+
>>> sorted(sorted(x.items()) for x in problem.getSolutions())
|
|
673
|
+
[[('a', 1), ('b', 1)], [('a', 1), ('b', 2)], [('a', 2), ('b', 1)]]
|
|
674
|
+
"""
|
|
675
|
+
|
|
676
|
+
def __init__(self, set, n=1, exact=False):
|
|
677
|
+
"""Initialization method.
|
|
678
|
+
|
|
679
|
+
Args:
|
|
680
|
+
set (set): Set of values to be checked
|
|
681
|
+
n (int): Minimum number of assigned values that should be
|
|
682
|
+
present in set (default is 1)
|
|
683
|
+
exact (bool): Whether the number of assigned values which
|
|
684
|
+
are present in set must be exactly `n`
|
|
685
|
+
"""
|
|
686
|
+
self._set = set
|
|
687
|
+
self._n = n
|
|
688
|
+
self._exact = exact
|
|
689
|
+
|
|
690
|
+
def __call__(self, variables: Sequence, domains: dict, assignments: dict, forwardcheck=False): # noqa: D102
|
|
691
|
+
set = self._set
|
|
692
|
+
missing = 0
|
|
693
|
+
found = 0
|
|
694
|
+
for variable in variables:
|
|
695
|
+
if variable in assignments:
|
|
696
|
+
found += assignments[variable] in set
|
|
697
|
+
else:
|
|
698
|
+
missing += 1
|
|
699
|
+
if missing:
|
|
700
|
+
if self._exact:
|
|
701
|
+
if not (found <= self._n <= missing + found):
|
|
702
|
+
return False
|
|
703
|
+
else:
|
|
704
|
+
if self._n > missing + found:
|
|
705
|
+
return False
|
|
706
|
+
if forwardcheck and self._n - found == missing:
|
|
707
|
+
# All unassigned variables must be assigned to
|
|
708
|
+
# values in the set.
|
|
709
|
+
for variable in variables:
|
|
710
|
+
if variable not in assignments:
|
|
711
|
+
domain = domains[variable]
|
|
712
|
+
for value in domain[:]:
|
|
713
|
+
if value not in set:
|
|
714
|
+
domain.hideValue(value)
|
|
715
|
+
if not domain:
|
|
716
|
+
return False
|
|
717
|
+
else:
|
|
718
|
+
if self._exact:
|
|
719
|
+
if found != self._n:
|
|
720
|
+
return False
|
|
721
|
+
else:
|
|
722
|
+
if found < self._n:
|
|
723
|
+
return False
|
|
724
|
+
return True
|
|
725
|
+
|
|
726
|
+
|
|
727
|
+
class SomeNotInSetConstraint(Constraint):
|
|
728
|
+
"""Constraint enforcing that at least some of the values of given variables must not be present in a given set.
|
|
729
|
+
|
|
730
|
+
Example:
|
|
731
|
+
>>> problem = Problem()
|
|
732
|
+
>>> problem.addVariables(["a", "b"], [1, 2])
|
|
733
|
+
>>> problem.addConstraint(SomeNotInSetConstraint([1]))
|
|
734
|
+
>>> sorted(sorted(x.items()) for x in problem.getSolutions())
|
|
735
|
+
[[('a', 1), ('b', 2)], [('a', 2), ('b', 1)], [('a', 2), ('b', 2)]]
|
|
736
|
+
"""
|
|
737
|
+
|
|
738
|
+
def __init__(self, set, n=1, exact=False):
|
|
739
|
+
"""Initialization method.
|
|
740
|
+
|
|
741
|
+
Args:
|
|
742
|
+
set (set): Set of values to be checked
|
|
743
|
+
n (int): Minimum number of assigned values that should not
|
|
744
|
+
be present in set (default is 1)
|
|
745
|
+
exact (bool): Whether the number of assigned values which
|
|
746
|
+
are not present in set must be exactly `n`
|
|
747
|
+
"""
|
|
748
|
+
self._set = set
|
|
749
|
+
self._n = n
|
|
750
|
+
self._exact = exact
|
|
751
|
+
|
|
752
|
+
def __call__(self, variables: Sequence, domains: dict, assignments: dict, forwardcheck=False): # noqa: D102
|
|
753
|
+
set = self._set
|
|
754
|
+
missing = 0
|
|
755
|
+
found = 0
|
|
756
|
+
for variable in variables:
|
|
757
|
+
if variable in assignments:
|
|
758
|
+
found += assignments[variable] not in set
|
|
759
|
+
else:
|
|
760
|
+
missing += 1
|
|
761
|
+
if missing:
|
|
762
|
+
if self._exact:
|
|
763
|
+
if not (found <= self._n <= missing + found):
|
|
764
|
+
return False
|
|
765
|
+
else:
|
|
766
|
+
if self._n > missing + found:
|
|
767
|
+
return False
|
|
768
|
+
if forwardcheck and self._n - found == missing:
|
|
769
|
+
# All unassigned variables must be assigned to
|
|
770
|
+
# values not in the set.
|
|
771
|
+
for variable in variables:
|
|
772
|
+
if variable not in assignments:
|
|
773
|
+
domain = domains[variable]
|
|
774
|
+
for value in domain[:]:
|
|
775
|
+
if value in set:
|
|
776
|
+
domain.hideValue(value)
|
|
777
|
+
if not domain:
|
|
778
|
+
return False
|
|
779
|
+
else:
|
|
780
|
+
if self._exact:
|
|
781
|
+
if found != self._n:
|
|
782
|
+
return False
|
|
783
|
+
else:
|
|
784
|
+
if found < self._n:
|
|
785
|
+
return False
|
|
786
|
+
return True
|