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
constraint/solvers.py
ADDED
|
@@ -0,0 +1,592 @@
|
|
|
1
|
+
"""Module containing the code for the problem solvers."""
|
|
2
|
+
|
|
3
|
+
import random
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def getArcs(domains: dict, constraints: list[tuple]) -> dict:
|
|
7
|
+
"""Return a dictionary mapping pairs (arcs) of constrained variables.
|
|
8
|
+
|
|
9
|
+
@attention: Currently unused.
|
|
10
|
+
"""
|
|
11
|
+
arcs = {}
|
|
12
|
+
for x in constraints:
|
|
13
|
+
constraint, variables = x
|
|
14
|
+
if len(variables) == 2:
|
|
15
|
+
variable1, variable2 = variables
|
|
16
|
+
arcs.setdefault(variable1, {}).setdefault(variable2, []).append(x)
|
|
17
|
+
arcs.setdefault(variable2, {}).setdefault(variable1, []).append(x)
|
|
18
|
+
return arcs
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def doArc8(arcs: dict, domains: dict, assignments: dict) -> bool:
|
|
22
|
+
"""Perform the ARC-8 arc checking algorithm and prune domains.
|
|
23
|
+
|
|
24
|
+
@attention: Currently unused.
|
|
25
|
+
"""
|
|
26
|
+
check = dict.fromkeys(domains, True)
|
|
27
|
+
while check:
|
|
28
|
+
variable, _ = check.popitem()
|
|
29
|
+
if variable not in arcs or variable in assignments:
|
|
30
|
+
continue
|
|
31
|
+
domain = domains[variable]
|
|
32
|
+
arcsvariable = arcs[variable]
|
|
33
|
+
for othervariable in arcsvariable:
|
|
34
|
+
arcconstraints = arcsvariable[othervariable]
|
|
35
|
+
if othervariable in assignments:
|
|
36
|
+
otherdomain = [assignments[othervariable]]
|
|
37
|
+
else:
|
|
38
|
+
otherdomain = domains[othervariable]
|
|
39
|
+
if domain:
|
|
40
|
+
# changed = False
|
|
41
|
+
for value in domain[:]:
|
|
42
|
+
assignments[variable] = value
|
|
43
|
+
if otherdomain:
|
|
44
|
+
for othervalue in otherdomain:
|
|
45
|
+
assignments[othervariable] = othervalue
|
|
46
|
+
for constraint, variables in arcconstraints:
|
|
47
|
+
if not constraint(variables, domains, assignments, True):
|
|
48
|
+
break
|
|
49
|
+
else:
|
|
50
|
+
# All constraints passed. Value is safe.
|
|
51
|
+
break
|
|
52
|
+
else:
|
|
53
|
+
# All othervalues failed. Kill value.
|
|
54
|
+
domain.hideValue(value)
|
|
55
|
+
# changed = True
|
|
56
|
+
del assignments[othervariable]
|
|
57
|
+
del assignments[variable]
|
|
58
|
+
# if changed:
|
|
59
|
+
# check.update(dict.fromkeys(arcsvariable))
|
|
60
|
+
if not domain:
|
|
61
|
+
return False
|
|
62
|
+
return True
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class Solver:
|
|
66
|
+
"""Abstract base class for solvers."""
|
|
67
|
+
|
|
68
|
+
def getSolution(self, domains: dict, constraints: list[tuple], vconstraints: dict):
|
|
69
|
+
"""Return one solution for the given problem.
|
|
70
|
+
|
|
71
|
+
Args:
|
|
72
|
+
domains (dict): Dictionary mapping variables to their domains
|
|
73
|
+
constraints (list): List of pairs of (constraint, variables)
|
|
74
|
+
vconstraints (dict): Dictionary mapping variables to a list
|
|
75
|
+
of constraints affecting the given variables.
|
|
76
|
+
"""
|
|
77
|
+
msg = f"{self.__class__.__name__} is an abstract class"
|
|
78
|
+
raise NotImplementedError(msg)
|
|
79
|
+
|
|
80
|
+
def getSolutions(self, domains: dict, constraints: list[tuple], vconstraints: dict):
|
|
81
|
+
"""Return all solutions for the given problem.
|
|
82
|
+
|
|
83
|
+
Args:
|
|
84
|
+
domains (dict): Dictionary mapping variables to domains
|
|
85
|
+
constraints (list): List of pairs of (constraint, variables)
|
|
86
|
+
vconstraints (dict): Dictionary mapping variables to a list
|
|
87
|
+
of constraints affecting the given variables.
|
|
88
|
+
"""
|
|
89
|
+
msg = f"{self.__class__.__name__} provides only a single solution"
|
|
90
|
+
raise NotImplementedError(msg)
|
|
91
|
+
|
|
92
|
+
def getSolutionIter(self, domains: dict, constraints: list[tuple], vconstraints: dict):
|
|
93
|
+
"""Return an iterator for the solutions of the given problem.
|
|
94
|
+
|
|
95
|
+
Args:
|
|
96
|
+
domains (dict): Dictionary mapping variables to domains
|
|
97
|
+
constraints (list): List of pairs of (constraint, variables)
|
|
98
|
+
vconstraints (dict): Dictionary mapping variables to a list
|
|
99
|
+
of constraints affecting the given variables.
|
|
100
|
+
"""
|
|
101
|
+
msg = f"{self.__class__.__name__} doesn't provide iteration"
|
|
102
|
+
raise NotImplementedError(msg)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
class BacktrackingSolver(Solver):
|
|
106
|
+
"""Problem solver with backtracking capabilities.
|
|
107
|
+
|
|
108
|
+
Examples:
|
|
109
|
+
>>> result = [[('a', 1), ('b', 2)],
|
|
110
|
+
... [('a', 1), ('b', 3)],
|
|
111
|
+
... [('a', 2), ('b', 3)]]
|
|
112
|
+
|
|
113
|
+
>>> problem = Problem(BacktrackingSolver())
|
|
114
|
+
>>> problem.addVariables(["a", "b"], [1, 2, 3])
|
|
115
|
+
>>> problem.addConstraint(lambda a, b: b > a, ["a", "b"])
|
|
116
|
+
|
|
117
|
+
>>> solution = problem.getSolution()
|
|
118
|
+
>>> sorted(solution.items()) in result
|
|
119
|
+
True
|
|
120
|
+
|
|
121
|
+
>>> for solution in problem.getSolutionIter():
|
|
122
|
+
... sorted(solution.items()) in result
|
|
123
|
+
True
|
|
124
|
+
True
|
|
125
|
+
True
|
|
126
|
+
|
|
127
|
+
>>> for solution in problem.getSolutions():
|
|
128
|
+
... sorted(solution.items()) in result
|
|
129
|
+
True
|
|
130
|
+
True
|
|
131
|
+
True
|
|
132
|
+
"""
|
|
133
|
+
|
|
134
|
+
def __init__(self, forwardcheck=True):
|
|
135
|
+
"""Initialization method.
|
|
136
|
+
|
|
137
|
+
Args:
|
|
138
|
+
forwardcheck (bool): If false forward checking will not be
|
|
139
|
+
requested to constraints while looking for solutions
|
|
140
|
+
(default is true)
|
|
141
|
+
"""
|
|
142
|
+
self._forwardcheck = forwardcheck
|
|
143
|
+
|
|
144
|
+
def getSolutionIter(self, domains: dict, constraints: list[tuple], vconstraints: dict): # noqa: D102
|
|
145
|
+
forwardcheck = self._forwardcheck
|
|
146
|
+
assignments = {}
|
|
147
|
+
|
|
148
|
+
queue = []
|
|
149
|
+
|
|
150
|
+
while True:
|
|
151
|
+
# Mix the Degree and Minimum Remaing Values (MRV) heuristics
|
|
152
|
+
lst = [(-len(vconstraints[variable]), len(domains[variable]), variable) for variable in domains]
|
|
153
|
+
lst.sort()
|
|
154
|
+
for item in lst:
|
|
155
|
+
if item[-1] not in assignments:
|
|
156
|
+
# Found unassigned variable
|
|
157
|
+
variable = item[-1]
|
|
158
|
+
values = domains[variable][:]
|
|
159
|
+
if forwardcheck:
|
|
160
|
+
pushdomains = [domains[x] for x in domains if x not in assignments and x != variable]
|
|
161
|
+
else:
|
|
162
|
+
pushdomains = None
|
|
163
|
+
break
|
|
164
|
+
else:
|
|
165
|
+
# No unassigned variables. We've got a solution. Go back
|
|
166
|
+
# to last variable, if there's one.
|
|
167
|
+
yield assignments.copy()
|
|
168
|
+
if not queue:
|
|
169
|
+
return
|
|
170
|
+
variable, values, pushdomains = queue.pop()
|
|
171
|
+
if pushdomains:
|
|
172
|
+
for domain in pushdomains:
|
|
173
|
+
domain.popState()
|
|
174
|
+
|
|
175
|
+
while True:
|
|
176
|
+
# We have a variable. Do we have any values left?
|
|
177
|
+
if not values:
|
|
178
|
+
# No. Go back to last variable, if there's one.
|
|
179
|
+
del assignments[variable]
|
|
180
|
+
while queue:
|
|
181
|
+
variable, values, pushdomains = queue.pop()
|
|
182
|
+
if pushdomains:
|
|
183
|
+
for domain in pushdomains:
|
|
184
|
+
domain.popState()
|
|
185
|
+
if values:
|
|
186
|
+
break
|
|
187
|
+
del assignments[variable]
|
|
188
|
+
else:
|
|
189
|
+
return
|
|
190
|
+
|
|
191
|
+
# Got a value. Check it.
|
|
192
|
+
assignments[variable] = values.pop()
|
|
193
|
+
|
|
194
|
+
if pushdomains:
|
|
195
|
+
for domain in pushdomains:
|
|
196
|
+
domain.pushState()
|
|
197
|
+
|
|
198
|
+
for constraint, variables in vconstraints[variable]:
|
|
199
|
+
if not constraint(variables, domains, assignments, pushdomains):
|
|
200
|
+
# Value is not good.
|
|
201
|
+
break
|
|
202
|
+
else:
|
|
203
|
+
break
|
|
204
|
+
|
|
205
|
+
if pushdomains:
|
|
206
|
+
for domain in pushdomains:
|
|
207
|
+
domain.popState()
|
|
208
|
+
|
|
209
|
+
# Push state before looking for next variable.
|
|
210
|
+
queue.append((variable, values, pushdomains))
|
|
211
|
+
|
|
212
|
+
raise RuntimeError("Can't happen")
|
|
213
|
+
|
|
214
|
+
def getSolution(self, domains: dict, constraints: list[tuple], vconstraints: dict): # noqa: D102
|
|
215
|
+
iter = self.getSolutionIter(domains, constraints, vconstraints)
|
|
216
|
+
try:
|
|
217
|
+
return next(iter)
|
|
218
|
+
except StopIteration:
|
|
219
|
+
return None
|
|
220
|
+
|
|
221
|
+
def getSolutions(self, domains: dict, constraints: list[tuple], vconstraints: dict): # noqa: D102
|
|
222
|
+
return list(self.getSolutionIter(domains, constraints, vconstraints))
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
class OptimizedBacktrackingSolver(Solver):
|
|
226
|
+
"""Problem solver with backtracking capabilities, implementing several optimizations for increased performance.
|
|
227
|
+
|
|
228
|
+
Optimizations are especially in obtaining all solutions.
|
|
229
|
+
View https://github.com/python-constraint/python-constraint/pull/76 for more details.
|
|
230
|
+
|
|
231
|
+
Examples:
|
|
232
|
+
>>> result = [[('a', 1), ('b', 2)],
|
|
233
|
+
... [('a', 1), ('b', 3)],
|
|
234
|
+
... [('a', 2), ('b', 3)]]
|
|
235
|
+
|
|
236
|
+
>>> problem = Problem(OptimizedBacktrackingSolver())
|
|
237
|
+
>>> problem.addVariables(["a", "b"], [1, 2, 3])
|
|
238
|
+
>>> problem.addConstraint(lambda a, b: b > a, ["a", "b"])
|
|
239
|
+
|
|
240
|
+
>>> solution = problem.getSolution()
|
|
241
|
+
>>> sorted(solution.items()) in result
|
|
242
|
+
True
|
|
243
|
+
|
|
244
|
+
>>> for solution in problem.getSolutionIter():
|
|
245
|
+
... sorted(solution.items()) in result
|
|
246
|
+
True
|
|
247
|
+
True
|
|
248
|
+
True
|
|
249
|
+
|
|
250
|
+
>>> for solution in problem.getSolutions():
|
|
251
|
+
... sorted(solution.items()) in result
|
|
252
|
+
True
|
|
253
|
+
True
|
|
254
|
+
True
|
|
255
|
+
"""
|
|
256
|
+
|
|
257
|
+
def __init__(self, forwardcheck=True):
|
|
258
|
+
"""Initialization method.
|
|
259
|
+
|
|
260
|
+
Args:
|
|
261
|
+
forwardcheck (bool): If false forward checking will not be
|
|
262
|
+
requested to constraints while looking for solutions
|
|
263
|
+
(default is true)
|
|
264
|
+
"""
|
|
265
|
+
self._forwardcheck = forwardcheck
|
|
266
|
+
|
|
267
|
+
def getSolutionIter(self, domains: dict, constraints: list[tuple], vconstraints: dict): # noqa: D102
|
|
268
|
+
forwardcheck = self._forwardcheck
|
|
269
|
+
assignments = {}
|
|
270
|
+
sorted_variables = self.getSortedVariables(domains, vconstraints)
|
|
271
|
+
|
|
272
|
+
queue = []
|
|
273
|
+
|
|
274
|
+
while True:
|
|
275
|
+
# Mix the Degree and Minimum Remaing Values (MRV) heuristics
|
|
276
|
+
for variable in sorted_variables:
|
|
277
|
+
if variable not in assignments:
|
|
278
|
+
# Found unassigned variable
|
|
279
|
+
values = domains[variable][:]
|
|
280
|
+
if forwardcheck:
|
|
281
|
+
pushdomains = [domains[x] for x in domains if x not in assignments and x != variable]
|
|
282
|
+
else:
|
|
283
|
+
pushdomains = None
|
|
284
|
+
break
|
|
285
|
+
else:
|
|
286
|
+
# No unassigned variables. We've got a solution. Go back
|
|
287
|
+
# to last variable, if there's one.
|
|
288
|
+
yield assignments.copy()
|
|
289
|
+
if not queue:
|
|
290
|
+
return
|
|
291
|
+
variable, values, pushdomains = queue.pop()
|
|
292
|
+
if pushdomains:
|
|
293
|
+
for domain in pushdomains:
|
|
294
|
+
domain.popState()
|
|
295
|
+
|
|
296
|
+
while True:
|
|
297
|
+
# We have a variable. Do we have any values left?
|
|
298
|
+
if not values:
|
|
299
|
+
# No. Go back to last variable, if there's one.
|
|
300
|
+
del assignments[variable]
|
|
301
|
+
while queue:
|
|
302
|
+
variable, values, pushdomains = queue.pop()
|
|
303
|
+
if pushdomains:
|
|
304
|
+
for domain in pushdomains:
|
|
305
|
+
domain.popState()
|
|
306
|
+
if values:
|
|
307
|
+
break
|
|
308
|
+
del assignments[variable]
|
|
309
|
+
else:
|
|
310
|
+
return
|
|
311
|
+
|
|
312
|
+
# Got a value. Check it.
|
|
313
|
+
assignments[variable] = values.pop()
|
|
314
|
+
|
|
315
|
+
if pushdomains:
|
|
316
|
+
for domain in pushdomains:
|
|
317
|
+
domain.pushState()
|
|
318
|
+
|
|
319
|
+
for constraint, variables in vconstraints[variable]:
|
|
320
|
+
if not constraint(variables, domains, assignments, pushdomains):
|
|
321
|
+
# Value is not good.
|
|
322
|
+
break
|
|
323
|
+
else:
|
|
324
|
+
break
|
|
325
|
+
|
|
326
|
+
if pushdomains:
|
|
327
|
+
for domain in pushdomains:
|
|
328
|
+
domain.popState()
|
|
329
|
+
|
|
330
|
+
# Push state before looking for next variable.
|
|
331
|
+
queue.append((variable, values, pushdomains))
|
|
332
|
+
|
|
333
|
+
raise RuntimeError("Can't happen")
|
|
334
|
+
|
|
335
|
+
def getSolutionsList(self, domains: dict, vconstraints: dict) -> list[dict]: # noqa: D102
|
|
336
|
+
"""Optimized all-solutions finder that skips forwardchecking and returns the solutions in a list.
|
|
337
|
+
|
|
338
|
+
Args:
|
|
339
|
+
domains: Dictionary mapping variables to domains
|
|
340
|
+
vconstraints: Dictionary mapping variables to a list of constraints affecting the given variables.
|
|
341
|
+
|
|
342
|
+
Returns:
|
|
343
|
+
the list of solutions as a dictionary.
|
|
344
|
+
"""
|
|
345
|
+
# Does not do forwardcheck for simplicity
|
|
346
|
+
assignments: dict = {}
|
|
347
|
+
queue: list[tuple] = []
|
|
348
|
+
solutions: list[dict] = list()
|
|
349
|
+
sorted_variables = self.getSortedVariables(domains, vconstraints)
|
|
350
|
+
|
|
351
|
+
while True:
|
|
352
|
+
# Mix the Degree and Minimum Remaing Values (MRV) heuristics
|
|
353
|
+
for variable in sorted_variables:
|
|
354
|
+
if variable not in assignments:
|
|
355
|
+
# Found unassigned variable
|
|
356
|
+
values = domains[variable][:]
|
|
357
|
+
break
|
|
358
|
+
else:
|
|
359
|
+
# No unassigned variables. We've got a solution. Go back
|
|
360
|
+
# to last variable, if there's one.
|
|
361
|
+
solutions.append(assignments.copy())
|
|
362
|
+
if not queue:
|
|
363
|
+
return solutions
|
|
364
|
+
variable, values = queue.pop()
|
|
365
|
+
|
|
366
|
+
while True:
|
|
367
|
+
# We have a variable. Do we have any values left?
|
|
368
|
+
if not values:
|
|
369
|
+
# No. Go back to last variable, if there's one.
|
|
370
|
+
del assignments[variable]
|
|
371
|
+
while queue:
|
|
372
|
+
variable, values = queue.pop()
|
|
373
|
+
if values:
|
|
374
|
+
break
|
|
375
|
+
del assignments[variable]
|
|
376
|
+
else:
|
|
377
|
+
return solutions
|
|
378
|
+
|
|
379
|
+
# Got a value. Check it.
|
|
380
|
+
assignments[variable] = values.pop()
|
|
381
|
+
for constraint, variables in vconstraints[variable]:
|
|
382
|
+
if not constraint(variables, domains, assignments, None):
|
|
383
|
+
# Value is not good.
|
|
384
|
+
break
|
|
385
|
+
else:
|
|
386
|
+
break
|
|
387
|
+
|
|
388
|
+
# Push state before looking for next variable.
|
|
389
|
+
queue.append((variable, values))
|
|
390
|
+
|
|
391
|
+
raise RuntimeError("Can't happen")
|
|
392
|
+
|
|
393
|
+
def getSolutions(self, domains: dict, constraints: list[tuple], vconstraints: dict): # noqa: D102
|
|
394
|
+
if self._forwardcheck:
|
|
395
|
+
return list(self.getSolutionIter(domains, constraints, vconstraints))
|
|
396
|
+
return self.getSolutionsList(domains, vconstraints)
|
|
397
|
+
|
|
398
|
+
def getSolution(self, domains: dict, constraints: list[tuple], vconstraints: dict): # noqa: D102
|
|
399
|
+
iter = self.getSolutionIter(domains, constraints, vconstraints)
|
|
400
|
+
try:
|
|
401
|
+
return next(iter)
|
|
402
|
+
except StopIteration:
|
|
403
|
+
return None
|
|
404
|
+
|
|
405
|
+
def getSortedVariables(self, domains: dict, vconstraints: dict) -> list:
|
|
406
|
+
"""Sorts the list of variables on number of vconstraints to find unassigned variables quicker.
|
|
407
|
+
|
|
408
|
+
Args:
|
|
409
|
+
domains: Dictionary mapping variables to their domains
|
|
410
|
+
vconstraints: Dictionary mapping variables to a list
|
|
411
|
+
of constraints affecting the given variables.
|
|
412
|
+
|
|
413
|
+
Returns:
|
|
414
|
+
the list of variables, sorted from highest number of vconstraints to lowest.
|
|
415
|
+
"""
|
|
416
|
+
lst = [(-len(vconstraints[variable]), len(domains[variable]), variable) for variable in domains]
|
|
417
|
+
lst.sort()
|
|
418
|
+
return [c for _, _, c in lst]
|
|
419
|
+
|
|
420
|
+
|
|
421
|
+
class RecursiveBacktrackingSolver(Solver):
|
|
422
|
+
"""Recursive problem solver with backtracking capabilities.
|
|
423
|
+
|
|
424
|
+
Examples:
|
|
425
|
+
>>> result = [[('a', 1), ('b', 2)],
|
|
426
|
+
... [('a', 1), ('b', 3)],
|
|
427
|
+
... [('a', 2), ('b', 3)]]
|
|
428
|
+
|
|
429
|
+
>>> problem = Problem(RecursiveBacktrackingSolver())
|
|
430
|
+
>>> problem.addVariables(["a", "b"], [1, 2, 3])
|
|
431
|
+
>>> problem.addConstraint(lambda a, b: b > a, ["a", "b"])
|
|
432
|
+
|
|
433
|
+
>>> solution = problem.getSolution()
|
|
434
|
+
>>> sorted(solution.items()) in result
|
|
435
|
+
True
|
|
436
|
+
|
|
437
|
+
>>> for solution in problem.getSolutions():
|
|
438
|
+
... sorted(solution.items()) in result
|
|
439
|
+
True
|
|
440
|
+
True
|
|
441
|
+
True
|
|
442
|
+
|
|
443
|
+
>>> problem.getSolutionIter()
|
|
444
|
+
Traceback (most recent call last):
|
|
445
|
+
...
|
|
446
|
+
NotImplementedError: RecursiveBacktrackingSolver doesn't provide iteration
|
|
447
|
+
"""
|
|
448
|
+
|
|
449
|
+
def __init__(self, forwardcheck=True):
|
|
450
|
+
"""Initialization method.
|
|
451
|
+
|
|
452
|
+
Args:
|
|
453
|
+
forwardcheck (bool): If false forward checking will not be
|
|
454
|
+
requested to constraints while looking for solutions
|
|
455
|
+
(default is true)
|
|
456
|
+
"""
|
|
457
|
+
self._forwardcheck = forwardcheck
|
|
458
|
+
|
|
459
|
+
def recursiveBacktracking(self, solutions, domains, vconstraints, assignments, single):
|
|
460
|
+
"""Mix the Degree and Minimum Remaing Values (MRV) heuristics.
|
|
461
|
+
|
|
462
|
+
Args:
|
|
463
|
+
solutions: _description_
|
|
464
|
+
domains: _description_
|
|
465
|
+
vconstraints: _description_
|
|
466
|
+
assignments: _description_
|
|
467
|
+
single: _description_
|
|
468
|
+
|
|
469
|
+
Returns:
|
|
470
|
+
_description_
|
|
471
|
+
"""
|
|
472
|
+
lst = [(-len(vconstraints[variable]), len(domains[variable]), variable) for variable in domains]
|
|
473
|
+
lst.sort()
|
|
474
|
+
for item in lst:
|
|
475
|
+
if item[-1] not in assignments:
|
|
476
|
+
# Found an unassigned variable. Let's go.
|
|
477
|
+
break
|
|
478
|
+
else:
|
|
479
|
+
# No unassigned variables. We've got a solution.
|
|
480
|
+
solutions.append(assignments.copy())
|
|
481
|
+
return solutions
|
|
482
|
+
|
|
483
|
+
variable = item[-1]
|
|
484
|
+
assignments[variable] = None
|
|
485
|
+
|
|
486
|
+
forwardcheck = self._forwardcheck
|
|
487
|
+
if forwardcheck:
|
|
488
|
+
pushdomains = [domains[x] for x in domains if x not in assignments]
|
|
489
|
+
else:
|
|
490
|
+
pushdomains = None
|
|
491
|
+
|
|
492
|
+
for value in domains[variable]:
|
|
493
|
+
assignments[variable] = value
|
|
494
|
+
if pushdomains:
|
|
495
|
+
for domain in pushdomains:
|
|
496
|
+
domain.pushState()
|
|
497
|
+
for constraint, variables in vconstraints[variable]:
|
|
498
|
+
if not constraint(variables, domains, assignments, pushdomains):
|
|
499
|
+
# Value is not good.
|
|
500
|
+
break
|
|
501
|
+
else:
|
|
502
|
+
# Value is good. Recurse and get next variable.
|
|
503
|
+
self.recursiveBacktracking(solutions, domains, vconstraints, assignments, single)
|
|
504
|
+
if solutions and single:
|
|
505
|
+
return solutions
|
|
506
|
+
if pushdomains:
|
|
507
|
+
for domain in pushdomains:
|
|
508
|
+
domain.popState()
|
|
509
|
+
del assignments[variable]
|
|
510
|
+
return solutions
|
|
511
|
+
|
|
512
|
+
def getSolution(self, domains: dict, constraints: list[tuple], vconstraints: dict): # noqa: D102
|
|
513
|
+
solutions = self.recursiveBacktracking([], domains, vconstraints, {}, True)
|
|
514
|
+
return solutions and solutions[0] or None
|
|
515
|
+
|
|
516
|
+
def getSolutions(self, domains: dict, constraints: list[tuple], vconstraints: dict): # noqa: D102
|
|
517
|
+
return self.recursiveBacktracking([], domains, vconstraints, {}, False)
|
|
518
|
+
|
|
519
|
+
|
|
520
|
+
class MinConflictsSolver(Solver):
|
|
521
|
+
"""Problem solver based on the minimum conflicts theory.
|
|
522
|
+
|
|
523
|
+
Examples:
|
|
524
|
+
>>> result = [[('a', 1), ('b', 2)],
|
|
525
|
+
... [('a', 1), ('b', 3)],
|
|
526
|
+
... [('a', 2), ('b', 3)]]
|
|
527
|
+
|
|
528
|
+
>>> problem = Problem(MinConflictsSolver())
|
|
529
|
+
>>> problem.addVariables(["a", "b"], [1, 2, 3])
|
|
530
|
+
>>> problem.addConstraint(lambda a, b: b > a, ["a", "b"])
|
|
531
|
+
|
|
532
|
+
>>> solution = problem.getSolution()
|
|
533
|
+
>>> sorted(solution.items()) in result
|
|
534
|
+
True
|
|
535
|
+
|
|
536
|
+
>>> problem.getSolutions()
|
|
537
|
+
Traceback (most recent call last):
|
|
538
|
+
...
|
|
539
|
+
NotImplementedError: MinConflictsSolver provides only a single solution
|
|
540
|
+
|
|
541
|
+
>>> problem.getSolutionIter()
|
|
542
|
+
Traceback (most recent call last):
|
|
543
|
+
...
|
|
544
|
+
NotImplementedError: MinConflictsSolver doesn't provide iteration
|
|
545
|
+
"""
|
|
546
|
+
|
|
547
|
+
def __init__(self, steps=1000):
|
|
548
|
+
"""Initialization method.
|
|
549
|
+
|
|
550
|
+
Args:
|
|
551
|
+
steps (int): Maximum number of steps to perform before
|
|
552
|
+
giving up when looking for a solution (default is 1000)
|
|
553
|
+
"""
|
|
554
|
+
self._steps = steps
|
|
555
|
+
|
|
556
|
+
def getSolution(self, domains: dict, constraints: list[tuple], vconstraints: dict): # noqa: D102
|
|
557
|
+
assignments = {}
|
|
558
|
+
# Initial assignment
|
|
559
|
+
for variable in domains:
|
|
560
|
+
assignments[variable] = random.choice(domains[variable])
|
|
561
|
+
for _ in range(self._steps):
|
|
562
|
+
conflicted = False
|
|
563
|
+
lst = list(domains.keys())
|
|
564
|
+
random.shuffle(lst)
|
|
565
|
+
for variable in lst:
|
|
566
|
+
# Check if variable is not in conflict
|
|
567
|
+
for constraint, variables in vconstraints[variable]:
|
|
568
|
+
if not constraint(variables, domains, assignments):
|
|
569
|
+
break
|
|
570
|
+
else:
|
|
571
|
+
continue
|
|
572
|
+
# Variable has conflicts. Find values with less conflicts.
|
|
573
|
+
mincount = len(vconstraints[variable])
|
|
574
|
+
minvalues = []
|
|
575
|
+
for value in domains[variable]:
|
|
576
|
+
assignments[variable] = value
|
|
577
|
+
count = 0
|
|
578
|
+
for constraint, variables in vconstraints[variable]:
|
|
579
|
+
if not constraint(variables, domains, assignments):
|
|
580
|
+
count += 1
|
|
581
|
+
if count == mincount:
|
|
582
|
+
minvalues.append(value)
|
|
583
|
+
elif count < mincount:
|
|
584
|
+
mincount = count
|
|
585
|
+
del minvalues[:]
|
|
586
|
+
minvalues.append(value)
|
|
587
|
+
# Pick a random one from these values.
|
|
588
|
+
assignments[variable] = random.choice(minvalues)
|
|
589
|
+
conflicted = True
|
|
590
|
+
if not conflicted:
|
|
591
|
+
return assignments
|
|
592
|
+
return None
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
Copyright (c) 2005-2014 - Gustavo Niemeyer <gustavo@niemeyer.net>
|
|
2
|
+
|
|
3
|
+
All rights reserved.
|
|
4
|
+
|
|
5
|
+
Redistribution and use in source and binary forms, with or without
|
|
6
|
+
modification, are permitted provided that the following conditions are met:
|
|
7
|
+
|
|
8
|
+
1. Redistributions of source code must retain the above copyright notice, this
|
|
9
|
+
list of conditions and the following disclaimer.
|
|
10
|
+
2. Redistributions in binary form must reproduce the above copyright notice,
|
|
11
|
+
this list of conditions and the following disclaimer in the documentation
|
|
12
|
+
and/or other materials provided with the distribution.
|
|
13
|
+
|
|
14
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
|
15
|
+
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
|
16
|
+
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
17
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
|
|
18
|
+
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
|
19
|
+
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
|
20
|
+
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
|
21
|
+
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
|
22
|
+
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
|
23
|
+
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|