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
constraint/solvers.py
ADDED
|
@@ -0,0 +1,788 @@
|
|
|
1
|
+
"""Module containing the code for the problem solvers."""
|
|
2
|
+
|
|
3
|
+
import random
|
|
4
|
+
from types import FunctionType
|
|
5
|
+
from constraint.domain import Domain
|
|
6
|
+
from constraint.constraints import Constraint, FunctionConstraint, CompilableFunctionConstraint
|
|
7
|
+
from collections.abc import Hashable
|
|
8
|
+
|
|
9
|
+
# for parallel solver
|
|
10
|
+
from concurrent.futures import ProcessPoolExecutor, ThreadPoolExecutor
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def getArcs(domains: dict, constraints: list[tuple]) -> dict:
|
|
14
|
+
"""Return a dictionary mapping pairs (arcs) of constrained variables.
|
|
15
|
+
|
|
16
|
+
@attention: Currently unused.
|
|
17
|
+
"""
|
|
18
|
+
arcs = {}
|
|
19
|
+
for x in constraints:
|
|
20
|
+
constraint, variables = x
|
|
21
|
+
if len(variables) == 2:
|
|
22
|
+
variable1, variable2 = variables
|
|
23
|
+
arcs.setdefault(variable1, {}).setdefault(variable2, []).append(x)
|
|
24
|
+
arcs.setdefault(variable2, {}).setdefault(variable1, []).append(x)
|
|
25
|
+
return arcs
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def doArc8(arcs: dict, domains: dict, assignments: dict) -> bool:
|
|
29
|
+
"""Perform the ARC-8 arc checking algorithm and prune domains.
|
|
30
|
+
|
|
31
|
+
@attention: Currently unused.
|
|
32
|
+
"""
|
|
33
|
+
check = dict.fromkeys(domains, True)
|
|
34
|
+
while check:
|
|
35
|
+
variable, _ = check.popitem()
|
|
36
|
+
if variable not in arcs or variable in assignments:
|
|
37
|
+
continue
|
|
38
|
+
domain = domains[variable]
|
|
39
|
+
arcsvariable = arcs[variable]
|
|
40
|
+
for othervariable in arcsvariable:
|
|
41
|
+
arcconstraints = arcsvariable[othervariable]
|
|
42
|
+
if othervariable in assignments:
|
|
43
|
+
otherdomain = [assignments[othervariable]]
|
|
44
|
+
else:
|
|
45
|
+
otherdomain = domains[othervariable]
|
|
46
|
+
if domain:
|
|
47
|
+
# changed = False
|
|
48
|
+
for value in domain[:]:
|
|
49
|
+
assignments[variable] = value
|
|
50
|
+
if otherdomain:
|
|
51
|
+
for othervalue in otherdomain:
|
|
52
|
+
assignments[othervariable] = othervalue
|
|
53
|
+
for constraint, variables in arcconstraints:
|
|
54
|
+
if not constraint(variables, domains, assignments, True):
|
|
55
|
+
break
|
|
56
|
+
else:
|
|
57
|
+
# All constraints passed. Value is safe.
|
|
58
|
+
break
|
|
59
|
+
else:
|
|
60
|
+
# All othervalues failed. Kill value.
|
|
61
|
+
domain.hideValue(value)
|
|
62
|
+
# changed = True
|
|
63
|
+
del assignments[othervariable]
|
|
64
|
+
del assignments[variable]
|
|
65
|
+
# if changed:
|
|
66
|
+
# check.update(dict.fromkeys(arcsvariable))
|
|
67
|
+
if not domain:
|
|
68
|
+
return False
|
|
69
|
+
return True
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class Solver:
|
|
73
|
+
"""Abstract base class for solvers."""
|
|
74
|
+
|
|
75
|
+
requires_pickling = False
|
|
76
|
+
|
|
77
|
+
def getSolution(self, domains: dict, constraints: list[tuple], vconstraints: dict):
|
|
78
|
+
"""Return one solution for the given problem.
|
|
79
|
+
|
|
80
|
+
Args:
|
|
81
|
+
domains (dict): Dictionary mapping variables to their domains
|
|
82
|
+
constraints (list): List of pairs of (constraint, variables)
|
|
83
|
+
vconstraints (dict): Dictionary mapping variables to a list
|
|
84
|
+
of constraints affecting the given variables.
|
|
85
|
+
"""
|
|
86
|
+
msg = f"{self.__class__.__name__} is an abstract class"
|
|
87
|
+
raise NotImplementedError(msg)
|
|
88
|
+
|
|
89
|
+
def getSolutions(self, domains: dict, constraints: list[tuple], vconstraints: dict):
|
|
90
|
+
"""Return all solutions for the given problem.
|
|
91
|
+
|
|
92
|
+
Args:
|
|
93
|
+
domains (dict): Dictionary mapping variables to domains
|
|
94
|
+
constraints (list): List of pairs of (constraint, variables)
|
|
95
|
+
vconstraints (dict): Dictionary mapping variables to a list
|
|
96
|
+
of constraints affecting the given variables.
|
|
97
|
+
"""
|
|
98
|
+
msg = f"{self.__class__.__name__} provides only a single solution"
|
|
99
|
+
raise NotImplementedError(msg)
|
|
100
|
+
|
|
101
|
+
def getSolutionIter(self, domains: dict, constraints: list[tuple], vconstraints: dict):
|
|
102
|
+
"""Return an iterator for the solutions of the given problem.
|
|
103
|
+
|
|
104
|
+
Args:
|
|
105
|
+
domains (dict): Dictionary mapping variables to domains
|
|
106
|
+
constraints (list): List of pairs of (constraint, variables)
|
|
107
|
+
vconstraints (dict): Dictionary mapping variables to a list
|
|
108
|
+
of constraints affecting the given variables.
|
|
109
|
+
"""
|
|
110
|
+
msg = f"{self.__class__.__name__} doesn't provide iteration"
|
|
111
|
+
raise NotImplementedError(msg)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
class BacktrackingSolver(Solver):
|
|
115
|
+
"""Problem solver with backtracking capabilities.
|
|
116
|
+
|
|
117
|
+
Examples:
|
|
118
|
+
>>> result = [[('a', 1), ('b', 2)],
|
|
119
|
+
... [('a', 1), ('b', 3)],
|
|
120
|
+
... [('a', 2), ('b', 3)]]
|
|
121
|
+
|
|
122
|
+
>>> problem = Problem(BacktrackingSolver())
|
|
123
|
+
>>> problem.addVariables(["a", "b"], [1, 2, 3])
|
|
124
|
+
>>> problem.addConstraint(lambda a, b: b > a, ["a", "b"])
|
|
125
|
+
|
|
126
|
+
>>> solution = problem.getSolution()
|
|
127
|
+
>>> sorted(solution.items()) in result
|
|
128
|
+
True
|
|
129
|
+
|
|
130
|
+
>>> for solution in problem.getSolutionIter():
|
|
131
|
+
... sorted(solution.items()) in result
|
|
132
|
+
True
|
|
133
|
+
True
|
|
134
|
+
True
|
|
135
|
+
|
|
136
|
+
>>> for solution in problem.getSolutions():
|
|
137
|
+
... sorted(solution.items()) in result
|
|
138
|
+
True
|
|
139
|
+
True
|
|
140
|
+
True
|
|
141
|
+
"""
|
|
142
|
+
|
|
143
|
+
def __init__(self, forwardcheck=True):
|
|
144
|
+
"""Initialization method.
|
|
145
|
+
|
|
146
|
+
Args:
|
|
147
|
+
forwardcheck (bool): If false forward checking will not be
|
|
148
|
+
requested to constraints while looking for solutions
|
|
149
|
+
(default is true)
|
|
150
|
+
"""
|
|
151
|
+
self._forwardcheck = forwardcheck
|
|
152
|
+
|
|
153
|
+
def getSolutionIter(self, domains: dict, constraints: list[tuple], vconstraints: dict): # noqa: D102
|
|
154
|
+
forwardcheck = self._forwardcheck
|
|
155
|
+
assignments = {}
|
|
156
|
+
|
|
157
|
+
queue = []
|
|
158
|
+
|
|
159
|
+
while True:
|
|
160
|
+
# Mix the Degree and Minimum Remaing Values (MRV) heuristics
|
|
161
|
+
lst = [(-len(vconstraints[variable]), len(domains[variable]), variable) for variable in domains]
|
|
162
|
+
lst.sort(key=lambda x: (x[0], x[1]))
|
|
163
|
+
for item in lst:
|
|
164
|
+
if item[-1] not in assignments:
|
|
165
|
+
# Found unassigned variable
|
|
166
|
+
variable = item[-1]
|
|
167
|
+
values = domains[variable][:]
|
|
168
|
+
if forwardcheck:
|
|
169
|
+
pushdomains = [domains[x] for x in domains if x not in assignments and x != variable]
|
|
170
|
+
else:
|
|
171
|
+
pushdomains = None
|
|
172
|
+
break
|
|
173
|
+
else:
|
|
174
|
+
# No unassigned variables. We've got a solution. Go back
|
|
175
|
+
# to last variable, if there's one.
|
|
176
|
+
yield assignments.copy()
|
|
177
|
+
if not queue:
|
|
178
|
+
return
|
|
179
|
+
variable, values, pushdomains = queue.pop()
|
|
180
|
+
if pushdomains:
|
|
181
|
+
for domain in pushdomains:
|
|
182
|
+
domain.popState()
|
|
183
|
+
|
|
184
|
+
while True:
|
|
185
|
+
# We have a variable. Do we have any values left?
|
|
186
|
+
if not values:
|
|
187
|
+
# No. Go back to last variable, if there's one.
|
|
188
|
+
del assignments[variable]
|
|
189
|
+
while queue:
|
|
190
|
+
variable, values, pushdomains = queue.pop()
|
|
191
|
+
if pushdomains:
|
|
192
|
+
for domain in pushdomains:
|
|
193
|
+
domain.popState()
|
|
194
|
+
if values:
|
|
195
|
+
break
|
|
196
|
+
del assignments[variable]
|
|
197
|
+
else:
|
|
198
|
+
return
|
|
199
|
+
|
|
200
|
+
# Got a value. Check it.
|
|
201
|
+
assignments[variable] = values.pop()
|
|
202
|
+
|
|
203
|
+
if pushdomains:
|
|
204
|
+
for domain in pushdomains:
|
|
205
|
+
domain.pushState()
|
|
206
|
+
|
|
207
|
+
for constraint, variables in vconstraints[variable]:
|
|
208
|
+
if not constraint(variables, domains, assignments, pushdomains):
|
|
209
|
+
# Value is not good.
|
|
210
|
+
break
|
|
211
|
+
else:
|
|
212
|
+
break
|
|
213
|
+
|
|
214
|
+
if pushdomains:
|
|
215
|
+
for domain in pushdomains:
|
|
216
|
+
domain.popState()
|
|
217
|
+
|
|
218
|
+
# Push state before looking for next variable.
|
|
219
|
+
queue.append((variable, values, pushdomains))
|
|
220
|
+
|
|
221
|
+
raise RuntimeError("Can't happen")
|
|
222
|
+
|
|
223
|
+
def getSolution(self, domains: dict, constraints: list[tuple], vconstraints: dict): # noqa: D102
|
|
224
|
+
iter = self.getSolutionIter(domains, constraints, vconstraints)
|
|
225
|
+
try:
|
|
226
|
+
return next(iter)
|
|
227
|
+
except StopIteration:
|
|
228
|
+
return None
|
|
229
|
+
|
|
230
|
+
def getSolutions(self, domains: dict, constraints: list[tuple], vconstraints: dict): # noqa: D102
|
|
231
|
+
return list(self.getSolutionIter(domains, constraints, vconstraints))
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
class OptimizedBacktrackingSolver(Solver):
|
|
235
|
+
"""Problem solver with backtracking capabilities, implementing several optimizations for increased performance.
|
|
236
|
+
|
|
237
|
+
Optimizations are especially in obtaining all solutions.
|
|
238
|
+
View https://github.com/python-constraint/python-constraint/pull/76 for more details.
|
|
239
|
+
|
|
240
|
+
Examples:
|
|
241
|
+
>>> result = [[('a', 1), ('b', 2)],
|
|
242
|
+
... [('a', 1), ('b', 3)],
|
|
243
|
+
... [('a', 2), ('b', 3)]]
|
|
244
|
+
|
|
245
|
+
>>> problem = Problem(OptimizedBacktrackingSolver())
|
|
246
|
+
>>> problem.addVariables(["a", "b"], [1, 2, 3])
|
|
247
|
+
>>> problem.addConstraint(lambda a, b: b > a, ["a", "b"])
|
|
248
|
+
|
|
249
|
+
>>> solution = problem.getSolution()
|
|
250
|
+
>>> sorted(solution.items()) in result
|
|
251
|
+
True
|
|
252
|
+
|
|
253
|
+
>>> for solution in problem.getSolutionIter():
|
|
254
|
+
... sorted(solution.items()) in result
|
|
255
|
+
True
|
|
256
|
+
True
|
|
257
|
+
True
|
|
258
|
+
|
|
259
|
+
>>> for solution in problem.getSolutions():
|
|
260
|
+
... sorted(solution.items()) in result
|
|
261
|
+
True
|
|
262
|
+
True
|
|
263
|
+
True
|
|
264
|
+
"""
|
|
265
|
+
|
|
266
|
+
def __init__(self, forwardcheck=True):
|
|
267
|
+
"""Initialization method.
|
|
268
|
+
|
|
269
|
+
Args:
|
|
270
|
+
forwardcheck (bool): If false forward checking will not be
|
|
271
|
+
requested to constraints while looking for solutions
|
|
272
|
+
(default is true)
|
|
273
|
+
"""
|
|
274
|
+
self._forwardcheck = forwardcheck
|
|
275
|
+
|
|
276
|
+
def getSolutionIter(self, domains: dict, constraints: list[tuple], vconstraints: dict): # noqa: D102
|
|
277
|
+
forwardcheck = self._forwardcheck
|
|
278
|
+
assignments = {}
|
|
279
|
+
sorted_variables = self.getSortedVariables(domains, vconstraints)
|
|
280
|
+
|
|
281
|
+
queue = []
|
|
282
|
+
|
|
283
|
+
while True:
|
|
284
|
+
# Mix the Degree and Minimum Remaing Values (MRV) heuristics
|
|
285
|
+
for variable in sorted_variables:
|
|
286
|
+
if variable not in assignments:
|
|
287
|
+
# Found unassigned variable
|
|
288
|
+
values = domains[variable][:]
|
|
289
|
+
if forwardcheck:
|
|
290
|
+
pushdomains = [domains[x] for x in domains if x not in assignments and x != variable]
|
|
291
|
+
else:
|
|
292
|
+
pushdomains = None
|
|
293
|
+
break
|
|
294
|
+
else:
|
|
295
|
+
# No unassigned variables. We've got a solution. Go back
|
|
296
|
+
# to last variable, if there's one.
|
|
297
|
+
yield assignments.copy()
|
|
298
|
+
if not queue:
|
|
299
|
+
return
|
|
300
|
+
variable, values, pushdomains = queue.pop()
|
|
301
|
+
if pushdomains:
|
|
302
|
+
for domain in pushdomains:
|
|
303
|
+
domain.popState()
|
|
304
|
+
|
|
305
|
+
while True:
|
|
306
|
+
# We have a variable. Do we have any values left?
|
|
307
|
+
if not values:
|
|
308
|
+
# No. Go back to last variable, if there's one.
|
|
309
|
+
del assignments[variable]
|
|
310
|
+
while queue:
|
|
311
|
+
variable, values, pushdomains = queue.pop()
|
|
312
|
+
if pushdomains:
|
|
313
|
+
for domain in pushdomains:
|
|
314
|
+
domain.popState()
|
|
315
|
+
if values:
|
|
316
|
+
break
|
|
317
|
+
del assignments[variable]
|
|
318
|
+
else:
|
|
319
|
+
return
|
|
320
|
+
|
|
321
|
+
# Got a value. Check it.
|
|
322
|
+
assignments[variable] = values.pop()
|
|
323
|
+
|
|
324
|
+
if pushdomains:
|
|
325
|
+
for domain in pushdomains:
|
|
326
|
+
domain.pushState()
|
|
327
|
+
|
|
328
|
+
for constraint, variables in vconstraints[variable]:
|
|
329
|
+
if not constraint(variables, domains, assignments, pushdomains):
|
|
330
|
+
# Value is not good.
|
|
331
|
+
break
|
|
332
|
+
else:
|
|
333
|
+
break
|
|
334
|
+
|
|
335
|
+
if pushdomains:
|
|
336
|
+
for domain in pushdomains:
|
|
337
|
+
domain.popState()
|
|
338
|
+
|
|
339
|
+
# Push state before looking for next variable.
|
|
340
|
+
queue.append((variable, values, pushdomains))
|
|
341
|
+
|
|
342
|
+
raise RuntimeError("Can't happen")
|
|
343
|
+
|
|
344
|
+
def getSolutionsList(self, domains: dict[Hashable, Domain], vconstraints: dict[Hashable, list[tuple[Constraint, Hashable]]]) -> list[dict[Hashable, any]]: # noqa: D102, E501
|
|
345
|
+
"""Optimized all-solutions finder that skips forwardchecking and returns the solutions in a list.
|
|
346
|
+
|
|
347
|
+
Args:
|
|
348
|
+
domains: Dictionary mapping variables to domains
|
|
349
|
+
vconstraints: Dictionary mapping variables to a list of constraints affecting the given variables.
|
|
350
|
+
|
|
351
|
+
Returns:
|
|
352
|
+
the list of solutions as a dictionary.
|
|
353
|
+
"""
|
|
354
|
+
# Does not do forwardcheck for simplicity
|
|
355
|
+
|
|
356
|
+
assignments: dict = {}
|
|
357
|
+
queue: list[tuple] = []
|
|
358
|
+
solutions: list[dict] = list()
|
|
359
|
+
sorted_variables = self.getSortedVariables(domains, vconstraints)
|
|
360
|
+
|
|
361
|
+
while True:
|
|
362
|
+
# Mix the Degree and Minimum Remaing Values (MRV) heuristics
|
|
363
|
+
for variable in sorted_variables:
|
|
364
|
+
if variable not in assignments:
|
|
365
|
+
# Found unassigned variable
|
|
366
|
+
values = domains[variable][:]
|
|
367
|
+
break
|
|
368
|
+
else:
|
|
369
|
+
# No unassigned variables. We've got a solution. Go back
|
|
370
|
+
# to last variable, if there's one.
|
|
371
|
+
solutions.append(assignments.copy())
|
|
372
|
+
if not queue:
|
|
373
|
+
return solutions
|
|
374
|
+
variable, values = queue.pop()
|
|
375
|
+
|
|
376
|
+
while True:
|
|
377
|
+
# We have a variable. Do we have any values left?
|
|
378
|
+
if not values:
|
|
379
|
+
# No. Go back to last variable, if there's one.
|
|
380
|
+
del assignments[variable]
|
|
381
|
+
while queue:
|
|
382
|
+
variable, values = queue.pop()
|
|
383
|
+
if values:
|
|
384
|
+
break
|
|
385
|
+
del assignments[variable]
|
|
386
|
+
else:
|
|
387
|
+
return solutions
|
|
388
|
+
|
|
389
|
+
# Got a value. Check it.
|
|
390
|
+
assignments[variable] = values.pop()
|
|
391
|
+
for constraint, variables in vconstraints[variable]:
|
|
392
|
+
if not constraint(variables, domains, assignments, None):
|
|
393
|
+
# Value is not good.
|
|
394
|
+
break
|
|
395
|
+
else:
|
|
396
|
+
break
|
|
397
|
+
|
|
398
|
+
# Push state before looking for next variable.
|
|
399
|
+
queue.append((variable, values))
|
|
400
|
+
|
|
401
|
+
def getSolutions(self, domains: dict, constraints: list[tuple], vconstraints: dict): # noqa: D102
|
|
402
|
+
if self._forwardcheck:
|
|
403
|
+
return list(self.getSolutionIter(domains, constraints, vconstraints))
|
|
404
|
+
return self.getSolutionsList(domains, vconstraints)
|
|
405
|
+
|
|
406
|
+
def getSolution(self, domains: dict, constraints: list[tuple], vconstraints: dict): # noqa: D102
|
|
407
|
+
iter = self.getSolutionIter(domains, constraints, vconstraints)
|
|
408
|
+
try:
|
|
409
|
+
return next(iter)
|
|
410
|
+
except StopIteration:
|
|
411
|
+
return None
|
|
412
|
+
|
|
413
|
+
def getSortedVariables(self, domains: dict, vconstraints: dict) -> list:
|
|
414
|
+
"""Sorts the list of variables on number of vconstraints to find unassigned variables quicker.
|
|
415
|
+
|
|
416
|
+
Args:
|
|
417
|
+
domains: Dictionary mapping variables to their domains
|
|
418
|
+
vconstraints: Dictionary mapping variables to a list
|
|
419
|
+
of constraints affecting the given variables.
|
|
420
|
+
|
|
421
|
+
Returns:
|
|
422
|
+
the list of variables, sorted from highest number of vconstraints to lowest.
|
|
423
|
+
"""
|
|
424
|
+
lst = [(-len(vconstraints[variable]), len(domains[variable]), variable) for variable in domains]
|
|
425
|
+
lst.sort(key=lambda x: (x[0], x[1]))
|
|
426
|
+
return [c for _, _, c in lst]
|
|
427
|
+
|
|
428
|
+
class RecursiveBacktrackingSolver(Solver):
|
|
429
|
+
"""Recursive problem solver with backtracking capabilities.
|
|
430
|
+
|
|
431
|
+
Examples:
|
|
432
|
+
>>> result = [[('a', 1), ('b', 2)],
|
|
433
|
+
... [('a', 1), ('b', 3)],
|
|
434
|
+
... [('a', 2), ('b', 3)]]
|
|
435
|
+
|
|
436
|
+
>>> problem = Problem(RecursiveBacktrackingSolver())
|
|
437
|
+
>>> problem.addVariables(["a", "b"], [1, 2, 3])
|
|
438
|
+
>>> problem.addConstraint(lambda a, b: b > a, ["a", "b"])
|
|
439
|
+
|
|
440
|
+
>>> solution = problem.getSolution()
|
|
441
|
+
>>> sorted(solution.items()) in result
|
|
442
|
+
True
|
|
443
|
+
|
|
444
|
+
>>> for solution in problem.getSolutions():
|
|
445
|
+
... sorted(solution.items()) in result
|
|
446
|
+
True
|
|
447
|
+
True
|
|
448
|
+
True
|
|
449
|
+
|
|
450
|
+
>>> problem.getSolutionIter()
|
|
451
|
+
Traceback (most recent call last):
|
|
452
|
+
...
|
|
453
|
+
NotImplementedError: RecursiveBacktrackingSolver doesn't provide iteration
|
|
454
|
+
"""
|
|
455
|
+
|
|
456
|
+
def __init__(self, forwardcheck=True):
|
|
457
|
+
"""Initialization method.
|
|
458
|
+
|
|
459
|
+
Args:
|
|
460
|
+
forwardcheck (bool): If false forward checking will not be
|
|
461
|
+
requested to constraints while looking for solutions
|
|
462
|
+
(default is true)
|
|
463
|
+
"""
|
|
464
|
+
self._forwardcheck = forwardcheck
|
|
465
|
+
|
|
466
|
+
def recursiveBacktracking(self, solutions, domains, vconstraints, assignments, single):
|
|
467
|
+
"""Mix the Degree and Minimum Remaing Values (MRV) heuristics.
|
|
468
|
+
|
|
469
|
+
Args:
|
|
470
|
+
solutions: _description_
|
|
471
|
+
domains: _description_
|
|
472
|
+
vconstraints: _description_
|
|
473
|
+
assignments: _description_
|
|
474
|
+
single: _description_
|
|
475
|
+
|
|
476
|
+
Returns:
|
|
477
|
+
_description_
|
|
478
|
+
"""
|
|
479
|
+
lst = [(-len(vconstraints[variable]), len(domains[variable]), variable) for variable in domains]
|
|
480
|
+
lst.sort(key=lambda x: (x[0], x[1]))
|
|
481
|
+
for item in lst:
|
|
482
|
+
if item[-1] not in assignments:
|
|
483
|
+
# Found an unassigned variable. Let's go.
|
|
484
|
+
break
|
|
485
|
+
else:
|
|
486
|
+
# No unassigned variables. We've got a solution.
|
|
487
|
+
solutions.append(assignments.copy())
|
|
488
|
+
return solutions
|
|
489
|
+
|
|
490
|
+
variable = item[-1]
|
|
491
|
+
assignments[variable] = None
|
|
492
|
+
|
|
493
|
+
forwardcheck = self._forwardcheck
|
|
494
|
+
if forwardcheck:
|
|
495
|
+
pushdomains = [domains[x] for x in domains if x not in assignments]
|
|
496
|
+
else:
|
|
497
|
+
pushdomains = None
|
|
498
|
+
|
|
499
|
+
for value in domains[variable]:
|
|
500
|
+
assignments[variable] = value
|
|
501
|
+
if pushdomains:
|
|
502
|
+
for domain in pushdomains:
|
|
503
|
+
domain.pushState()
|
|
504
|
+
for constraint, variables in vconstraints[variable]:
|
|
505
|
+
if not constraint(variables, domains, assignments, pushdomains):
|
|
506
|
+
# Value is not good.
|
|
507
|
+
break
|
|
508
|
+
else:
|
|
509
|
+
# Value is good. Recurse and get next variable.
|
|
510
|
+
self.recursiveBacktracking(solutions, domains, vconstraints, assignments, single)
|
|
511
|
+
if solutions and single:
|
|
512
|
+
return solutions
|
|
513
|
+
if pushdomains:
|
|
514
|
+
for domain in pushdomains:
|
|
515
|
+
domain.popState()
|
|
516
|
+
del assignments[variable]
|
|
517
|
+
return solutions
|
|
518
|
+
|
|
519
|
+
def getSolution(self, domains: dict, constraints: list[tuple], vconstraints: dict): # noqa: D102
|
|
520
|
+
solutions = self.recursiveBacktracking([], domains, vconstraints, {}, True)
|
|
521
|
+
return solutions and solutions[0] or None
|
|
522
|
+
|
|
523
|
+
def getSolutions(self, domains: dict, constraints: list[tuple], vconstraints: dict): # noqa: D102
|
|
524
|
+
return self.recursiveBacktracking([], domains, vconstraints, {}, False)
|
|
525
|
+
|
|
526
|
+
|
|
527
|
+
class MinConflictsSolver(Solver):
|
|
528
|
+
"""Problem solver based on the minimum conflicts theory.
|
|
529
|
+
|
|
530
|
+
Examples:
|
|
531
|
+
>>> result = [[('a', 1), ('b', 2)],
|
|
532
|
+
... [('a', 1), ('b', 3)],
|
|
533
|
+
... [('a', 2), ('b', 3)]]
|
|
534
|
+
|
|
535
|
+
>>> problem = Problem(MinConflictsSolver())
|
|
536
|
+
>>> problem.addVariables(["a", "b"], [1, 2, 3])
|
|
537
|
+
>>> problem.addConstraint(lambda a, b: b > a, ["a", "b"])
|
|
538
|
+
|
|
539
|
+
>>> solution = problem.getSolution()
|
|
540
|
+
>>> sorted(solution.items()) in result
|
|
541
|
+
True
|
|
542
|
+
|
|
543
|
+
>>> problem.getSolutions()
|
|
544
|
+
Traceback (most recent call last):
|
|
545
|
+
...
|
|
546
|
+
NotImplementedError: MinConflictsSolver provides only a single solution
|
|
547
|
+
|
|
548
|
+
>>> problem.getSolutionIter()
|
|
549
|
+
Traceback (most recent call last):
|
|
550
|
+
...
|
|
551
|
+
NotImplementedError: MinConflictsSolver doesn't provide iteration
|
|
552
|
+
"""
|
|
553
|
+
|
|
554
|
+
def __init__(self, steps=1000, rand=None):
|
|
555
|
+
"""Initialization method.
|
|
556
|
+
|
|
557
|
+
Args:
|
|
558
|
+
steps (int): Maximum number of steps to perform before
|
|
559
|
+
giving up when looking for a solution (default is 1000)
|
|
560
|
+
rand (Random): Optional random.Random instance to use for
|
|
561
|
+
repeatability.
|
|
562
|
+
"""
|
|
563
|
+
self._steps = steps
|
|
564
|
+
self._rand = rand
|
|
565
|
+
|
|
566
|
+
def getSolution(self, domains: dict, constraints: list[tuple], vconstraints: dict): # noqa: D102
|
|
567
|
+
choice = self._rand.choice if self._rand is not None else random.choice
|
|
568
|
+
shuffle = self._rand.shuffle if self._rand is not None else random.shuffle
|
|
569
|
+
assignments = {}
|
|
570
|
+
# Initial assignment
|
|
571
|
+
for variable in domains:
|
|
572
|
+
assignments[variable] = choice(domains[variable])
|
|
573
|
+
for _ in range(self._steps):
|
|
574
|
+
conflicted = False
|
|
575
|
+
lst = list(domains.keys())
|
|
576
|
+
shuffle(lst)
|
|
577
|
+
for variable in lst:
|
|
578
|
+
# Check if variable is not in conflict
|
|
579
|
+
for constraint, variables in vconstraints[variable]:
|
|
580
|
+
if not constraint(variables, domains, assignments):
|
|
581
|
+
break
|
|
582
|
+
else:
|
|
583
|
+
continue
|
|
584
|
+
# Variable has conflicts. Find values with less conflicts.
|
|
585
|
+
mincount = len(vconstraints[variable])
|
|
586
|
+
minvalues = []
|
|
587
|
+
for value in domains[variable]:
|
|
588
|
+
assignments[variable] = value
|
|
589
|
+
count = 0
|
|
590
|
+
for constraint, variables in vconstraints[variable]:
|
|
591
|
+
if not constraint(variables, domains, assignments):
|
|
592
|
+
count += 1
|
|
593
|
+
if count == mincount:
|
|
594
|
+
minvalues.append(value)
|
|
595
|
+
elif count < mincount:
|
|
596
|
+
mincount = count
|
|
597
|
+
del minvalues[:]
|
|
598
|
+
minvalues.append(value)
|
|
599
|
+
# Pick a random one from these values.
|
|
600
|
+
assignments[variable] = choice(minvalues)
|
|
601
|
+
conflicted = True
|
|
602
|
+
if not conflicted:
|
|
603
|
+
return assignments
|
|
604
|
+
return None
|
|
605
|
+
|
|
606
|
+
|
|
607
|
+
class ParallelSolver(Solver):
|
|
608
|
+
"""Problem solver that executes all-solution solve in parallel (ProcessPool or ThreadPool mode).
|
|
609
|
+
|
|
610
|
+
Sorts the domains on size, creating jobs for each value in the domain with the most variables.
|
|
611
|
+
Each leaf job is solved locally with either optimized backtracking or recursion.
|
|
612
|
+
Whether this is actually faster than non-parallel solving depends on your problem, and hardware and software environment.
|
|
613
|
+
|
|
614
|
+
Uses ThreadPool by default. Instantiate with process_mode=True to use ProcessPool.
|
|
615
|
+
In ProcessPool mode, the jobs do not share memory.
|
|
616
|
+
In ProcessPool mode, precompiled FunctionConstraints are not allowed due to pickling, use string constraints instead.
|
|
617
|
+
|
|
618
|
+
Examples:
|
|
619
|
+
>>> result = [[('a', 1), ('b', 2)],
|
|
620
|
+
... [('a', 1), ('b', 3)],
|
|
621
|
+
... [('a', 2), ('b', 3)]]
|
|
622
|
+
|
|
623
|
+
>>> problem = Problem(ParallelSolver())
|
|
624
|
+
>>> problem.addVariables(["a", "b"], [1, 2, 3])
|
|
625
|
+
>>> problem.addConstraint("b > a", ["a", "b"])
|
|
626
|
+
|
|
627
|
+
>>> for solution in problem.getSolutions():
|
|
628
|
+
... sorted(solution.items()) in result
|
|
629
|
+
True
|
|
630
|
+
True
|
|
631
|
+
True
|
|
632
|
+
|
|
633
|
+
>>> problem.getSolution()
|
|
634
|
+
Traceback (most recent call last):
|
|
635
|
+
...
|
|
636
|
+
NotImplementedError: ParallelSolver only provides all solutions
|
|
637
|
+
|
|
638
|
+
>>> problem.getSolutionIter()
|
|
639
|
+
Traceback (most recent call last):
|
|
640
|
+
...
|
|
641
|
+
NotImplementedError: ParallelSolver doesn't provide iteration
|
|
642
|
+
""" # noqa E501
|
|
643
|
+
|
|
644
|
+
def __init__(self, process_mode=False):
|
|
645
|
+
"""Initialization method. Set `process_mode` to True for using ProcessPool, otherwise uses ThreadPool."""
|
|
646
|
+
super().__init__()
|
|
647
|
+
self._process_mode = process_mode
|
|
648
|
+
self.requires_pickling = process_mode
|
|
649
|
+
|
|
650
|
+
def getSolution(self, domains: dict, constraints: list[tuple], vconstraints: dict):
|
|
651
|
+
"""Return one solution for the given problem.
|
|
652
|
+
|
|
653
|
+
Args:
|
|
654
|
+
domains (dict): Dictionary mapping variables to their domains
|
|
655
|
+
constraints (list): List of pairs of (constraint, variables)
|
|
656
|
+
vconstraints (dict): Dictionary mapping variables to a list
|
|
657
|
+
of constraints affecting the given variables.
|
|
658
|
+
"""
|
|
659
|
+
msg = f"{self.__class__.__name__} only provides all solutions"
|
|
660
|
+
raise NotImplementedError(msg)
|
|
661
|
+
|
|
662
|
+
def getSolutionsList(self, domains: dict[Hashable, Domain], vconstraints: dict[Hashable, list[tuple[Constraint, Hashable]]]) -> list[dict[Hashable, any]]: # noqa: D102, E501
|
|
663
|
+
"""Parallelized all-solutions finder using ProcessPoolExecutor for work-stealing."""
|
|
664
|
+
# Precompute constraints lookup per variable
|
|
665
|
+
constraint_lookup: dict[Hashable, list[tuple[Constraint, Hashable]]] = {var: vconstraints.get(var, []) for var in domains} # noqa: E501
|
|
666
|
+
|
|
667
|
+
# Sort variables by domain size (heuristic)
|
|
668
|
+
sorted_vars: list[Hashable] = sorted(domains.keys(), key=lambda v: len(domains[v]))
|
|
669
|
+
|
|
670
|
+
# Split parallel and sequential parts
|
|
671
|
+
first_var = sorted_vars[0]
|
|
672
|
+
remaining_vars = sorted_vars[1:]
|
|
673
|
+
|
|
674
|
+
# Create the parallel function arguments and solutions lists
|
|
675
|
+
args = ((self.requires_pickling, domains, constraint_lookup, first_var, val, remaining_vars.copy()) for val in domains[first_var]) # noqa: E501
|
|
676
|
+
solutions: list[dict[Hashable, any]] = []
|
|
677
|
+
|
|
678
|
+
# execute in parallel
|
|
679
|
+
parallel_pool = ProcessPoolExecutor if self._process_mode else ThreadPoolExecutor
|
|
680
|
+
with parallel_pool() as executor:
|
|
681
|
+
# results = map(parallel_worker, args) # sequential
|
|
682
|
+
results = executor.map(parallel_worker, args, chunksize=1) # parallel
|
|
683
|
+
for result in results:
|
|
684
|
+
solutions.extend(result)
|
|
685
|
+
|
|
686
|
+
return solutions
|
|
687
|
+
|
|
688
|
+
def getSolutions(self, domains: dict, constraints: list[tuple], vconstraints: dict): # noqa: D102
|
|
689
|
+
return self.getSolutionsList(domains, vconstraints)
|
|
690
|
+
|
|
691
|
+
### Helper functions for parallel solver
|
|
692
|
+
|
|
693
|
+
def is_valid(assignment: dict[Hashable, any], constraints_lookup: list[tuple[Constraint, Hashable]], domains: dict[Hashable, Domain]) -> bool: # noqa E501
|
|
694
|
+
"""Check if all constraints are satisfied given the current assignment."""
|
|
695
|
+
return all(
|
|
696
|
+
constraint(vars_involved, domains, assignment, None)
|
|
697
|
+
for constraint, vars_involved in constraints_lookup
|
|
698
|
+
if all(v in assignment for v in vars_involved)
|
|
699
|
+
)
|
|
700
|
+
|
|
701
|
+
def compile_to_function(constraint: CompilableFunctionConstraint) -> FunctionConstraint:
|
|
702
|
+
"""Compile a CompilableFunctionConstraint to a function, wrapped by a FunctionConstraint."""
|
|
703
|
+
func_string = constraint._func
|
|
704
|
+
code_object = compile(func_string, "<string>", "exec")
|
|
705
|
+
func = FunctionType(code_object.co_consts[0], globals())
|
|
706
|
+
return FunctionConstraint(func)
|
|
707
|
+
|
|
708
|
+
def sequential_recursive_backtrack(assignment: dict[Hashable, any], unassigned_vars: list[Hashable], domains: dict[Hashable, Domain], constraint_lookup: dict[Hashable, list[tuple[Constraint, Hashable]]]) -> list[dict[Hashable, any]]: # noqa E501
|
|
709
|
+
"""Sequential recursive backtracking function for subproblems."""
|
|
710
|
+
if not unassigned_vars:
|
|
711
|
+
return [assignment.copy()]
|
|
712
|
+
|
|
713
|
+
var = unassigned_vars[-1]
|
|
714
|
+
remaining_vars = unassigned_vars[:-1]
|
|
715
|
+
|
|
716
|
+
solutions: list[dict[Hashable, any]] = []
|
|
717
|
+
for value in domains[var]:
|
|
718
|
+
assignment[var] = value
|
|
719
|
+
if is_valid(assignment, constraint_lookup[var], domains):
|
|
720
|
+
solutions.extend(sequential_recursive_backtrack(assignment, remaining_vars, domains, constraint_lookup))
|
|
721
|
+
del assignment[var]
|
|
722
|
+
return solutions
|
|
723
|
+
|
|
724
|
+
def sequential_optimized_backtrack(assignment: dict[Hashable, any], unassigned_vars: list[Hashable], domains: dict[Hashable, Domain], constraint_lookup: dict[Hashable, list[tuple[Constraint, Hashable]]]) -> list[dict[Hashable, any]]: # noqa E501
|
|
725
|
+
"""Sequential optimized backtracking (as in OptimizedBacktrackingSolver) function for subproblems."""
|
|
726
|
+
# Does not do forwardcheck for simplicity
|
|
727
|
+
|
|
728
|
+
assignments = assignment
|
|
729
|
+
sorted_variables = unassigned_vars
|
|
730
|
+
queue: list[tuple] = []
|
|
731
|
+
solutions: list[dict] = list()
|
|
732
|
+
|
|
733
|
+
while True:
|
|
734
|
+
# Mix the Degree and Minimum Remaing Values (MRV) heuristics
|
|
735
|
+
for variable in sorted_variables:
|
|
736
|
+
if variable not in assignments:
|
|
737
|
+
# Found unassigned variable
|
|
738
|
+
values = domains[variable][:]
|
|
739
|
+
break
|
|
740
|
+
else:
|
|
741
|
+
# No unassigned variables. We've got a solution. Go back
|
|
742
|
+
# to last variable, if there's one.
|
|
743
|
+
solutions.append(assignments.copy())
|
|
744
|
+
if not queue:
|
|
745
|
+
return solutions
|
|
746
|
+
variable, values = queue.pop()
|
|
747
|
+
|
|
748
|
+
while True:
|
|
749
|
+
# We have a variable. Do we have any values left?
|
|
750
|
+
if not values:
|
|
751
|
+
# No. Go back to last variable, if there's one.
|
|
752
|
+
del assignments[variable]
|
|
753
|
+
while queue:
|
|
754
|
+
variable, values = queue.pop()
|
|
755
|
+
if values:
|
|
756
|
+
break
|
|
757
|
+
del assignments[variable]
|
|
758
|
+
else:
|
|
759
|
+
return solutions
|
|
760
|
+
|
|
761
|
+
# Got a value. Check it.
|
|
762
|
+
assignments[variable] = values.pop()
|
|
763
|
+
for constraint, variables in constraint_lookup[variable]:
|
|
764
|
+
if not constraint(variables, domains, assignments, None):
|
|
765
|
+
# Value is not good.
|
|
766
|
+
break
|
|
767
|
+
else:
|
|
768
|
+
break
|
|
769
|
+
|
|
770
|
+
# Push state before looking for next variable.
|
|
771
|
+
queue.append((variable, values))
|
|
772
|
+
|
|
773
|
+
|
|
774
|
+
def parallel_worker(args: tuple[bool, dict[Hashable, Domain], dict[Hashable, list[tuple[Constraint, Hashable]]], Hashable, any, list[Hashable]]) -> list[dict[Hashable, any]]: # noqa E501
|
|
775
|
+
"""Worker function for parallel execution on first variable."""
|
|
776
|
+
process_mode, domains, constraint_lookup, first_var, first_value, remaining_vars = args
|
|
777
|
+
local_assignment = {first_var: first_value}
|
|
778
|
+
|
|
779
|
+
if process_mode:
|
|
780
|
+
# if there are any CompilableFunctionConstraint, they must be compiled locally first
|
|
781
|
+
for var, constraints in constraint_lookup.items():
|
|
782
|
+
constraint_lookup[var] = [tuple([compile_to_function(constraint) if isinstance(constraint, CompilableFunctionConstraint) else constraint, vals]) for constraint, vals in constraints] # noqa E501
|
|
783
|
+
|
|
784
|
+
# continue solving sequentially on this process
|
|
785
|
+
if is_valid(local_assignment, constraint_lookup[first_var], domains):
|
|
786
|
+
return sequential_optimized_backtrack(local_assignment, remaining_vars, domains, constraint_lookup)
|
|
787
|
+
return []
|
|
788
|
+
|