python-constraint2 2.0.0__cp310-cp310-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/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.