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/problem.py
ADDED
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
"""Module containing the code for problem definitions."""
|
|
2
|
+
|
|
3
|
+
import copy
|
|
4
|
+
from warnings import warn
|
|
5
|
+
from operator import itemgetter
|
|
6
|
+
from typing import Callable, Optional, Union
|
|
7
|
+
from collections.abc import Sequence, Hashable
|
|
8
|
+
|
|
9
|
+
from constraint.constraints import Constraint, FunctionConstraint, CompilableFunctionConstraint
|
|
10
|
+
from constraint.domain import Domain
|
|
11
|
+
from constraint.solvers import Solver, OptimizedBacktrackingSolver, ParallelSolver
|
|
12
|
+
from constraint.parser import compile_to_constraints
|
|
13
|
+
|
|
14
|
+
try:
|
|
15
|
+
from sys import _is_gil_enabled
|
|
16
|
+
freethreading = _is_gil_enabled()
|
|
17
|
+
except ImportError:
|
|
18
|
+
freethreading = False
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class Problem:
|
|
22
|
+
"""Class used to define a problem and retrieve solutions."""
|
|
23
|
+
|
|
24
|
+
def __init__(self, solver: Solver=None):
|
|
25
|
+
"""Initialization method.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
solver (instance of a :py:class:`Solver`): Problem solver (default :py:class:`OptimizedBacktrackingSolver`)
|
|
29
|
+
"""
|
|
30
|
+
self._solver = solver or OptimizedBacktrackingSolver()
|
|
31
|
+
self._constraints: list[tuple[Constraint, any]] = []
|
|
32
|
+
self._str_constraints: list[str] = []
|
|
33
|
+
self._variables: dict[Hashable, Domain] = {}
|
|
34
|
+
|
|
35
|
+
# check if solver is instance instead of class
|
|
36
|
+
assert isinstance(self._solver, Solver), f"`solver` is not instance of Solver class (is {type(self._solver)})."
|
|
37
|
+
|
|
38
|
+
# warn for experimental parallel solver
|
|
39
|
+
if isinstance(self._solver, ParallelSolver):
|
|
40
|
+
warn("ParallelSolver is currently experimental, and unlikely to be faster than OptimizedBacktrackingSolver. Please report any issues.") # future: remove # noqa E501
|
|
41
|
+
if not self._solver._process_mode and not freethreading:
|
|
42
|
+
warn("Using the ParallelSolver in ThreadPool mode without freethreading will cause poor performance.")
|
|
43
|
+
|
|
44
|
+
def reset(self):
|
|
45
|
+
"""Reset the current problem definition.
|
|
46
|
+
|
|
47
|
+
Example:
|
|
48
|
+
>>> problem = Problem()
|
|
49
|
+
>>> problem.addVariable("a", [1, 2])
|
|
50
|
+
>>> problem.reset()
|
|
51
|
+
>>> problem.getSolution()
|
|
52
|
+
>>>
|
|
53
|
+
"""
|
|
54
|
+
del self._constraints[:]
|
|
55
|
+
self._variables.clear()
|
|
56
|
+
|
|
57
|
+
def setSolver(self, solver):
|
|
58
|
+
"""Change the problem solver currently in use.
|
|
59
|
+
|
|
60
|
+
Example:
|
|
61
|
+
>>> solver = OptimizedBacktrackingSolver()
|
|
62
|
+
>>> problem = Problem(solver)
|
|
63
|
+
>>> problem.getSolver() is solver
|
|
64
|
+
True
|
|
65
|
+
|
|
66
|
+
Args:
|
|
67
|
+
solver (instance of a :py:class:`Solver`): New problem
|
|
68
|
+
solver
|
|
69
|
+
"""
|
|
70
|
+
self._solver = solver
|
|
71
|
+
|
|
72
|
+
def getSolver(self):
|
|
73
|
+
"""Obtain the problem solver currently in use.
|
|
74
|
+
|
|
75
|
+
Example:
|
|
76
|
+
>>> solver = OptimizedBacktrackingSolver()
|
|
77
|
+
>>> problem = Problem(solver)
|
|
78
|
+
>>> problem.getSolver() is solver
|
|
79
|
+
True
|
|
80
|
+
|
|
81
|
+
Returns:
|
|
82
|
+
instance of a :py:class:`Solver` subclass: Solver currently in use
|
|
83
|
+
"""
|
|
84
|
+
return self._solver
|
|
85
|
+
|
|
86
|
+
def addVariable(self, variable: Hashable, domain):
|
|
87
|
+
"""Add a variable to the problem.
|
|
88
|
+
|
|
89
|
+
Example:
|
|
90
|
+
>>> problem = Problem()
|
|
91
|
+
>>> problem.addVariable("a", [1, 2])
|
|
92
|
+
>>> problem.getSolution() in ({'a': 1}, {'a': 2})
|
|
93
|
+
True
|
|
94
|
+
|
|
95
|
+
Args:
|
|
96
|
+
variable (hashable object): Object representing a problem
|
|
97
|
+
variable
|
|
98
|
+
domain (list, tuple, set, or instance of :py:class:`Domain`): Set of items
|
|
99
|
+
defining the possible values that the given variable may
|
|
100
|
+
assume
|
|
101
|
+
"""
|
|
102
|
+
if variable in self._variables:
|
|
103
|
+
msg = f"Tried to insert duplicated variable {repr(variable)}"
|
|
104
|
+
raise ValueError(msg)
|
|
105
|
+
if not isinstance(variable, (list, tuple, Domain)) and not hasattr(domain, "__getitem__"):
|
|
106
|
+
domain = list(domain)
|
|
107
|
+
if isinstance(domain, Domain):
|
|
108
|
+
domain = copy.deepcopy(domain)
|
|
109
|
+
elif hasattr(domain, "__getitem__"):
|
|
110
|
+
domain = Domain(domain)
|
|
111
|
+
else:
|
|
112
|
+
msg = "Domains must be instances of subclasses of the Domain class"
|
|
113
|
+
raise TypeError(msg)
|
|
114
|
+
if not domain:
|
|
115
|
+
raise ValueError("Domain is empty")
|
|
116
|
+
self._variables[variable] = domain
|
|
117
|
+
|
|
118
|
+
def addVariables(self, variables: Sequence, domain):
|
|
119
|
+
"""Add one or more variables to the problem.
|
|
120
|
+
|
|
121
|
+
Example:
|
|
122
|
+
>>> problem = Problem()
|
|
123
|
+
>>> problem.addVariables(["a", "b"], [1, 2, 3])
|
|
124
|
+
>>> solutions = problem.getSolutions()
|
|
125
|
+
>>> len(solutions)
|
|
126
|
+
9
|
|
127
|
+
>>> {'a': 3, 'b': 1} in solutions
|
|
128
|
+
True
|
|
129
|
+
|
|
130
|
+
Args:
|
|
131
|
+
variables (sequence of hashable objects): Any object
|
|
132
|
+
containing a sequence of objects represeting problem
|
|
133
|
+
variables
|
|
134
|
+
domain (list, tuple, or instance of :py:class:`Domain`): Set of items
|
|
135
|
+
defining the possible values that the given variables
|
|
136
|
+
may assume
|
|
137
|
+
"""
|
|
138
|
+
for variable in variables:
|
|
139
|
+
self.addVariable(variable, domain)
|
|
140
|
+
|
|
141
|
+
def addConstraint(self, constraint: Union[Constraint, Callable, str], variables: Optional[Sequence] = None):
|
|
142
|
+
"""Add a constraint to the problem.
|
|
143
|
+
|
|
144
|
+
Example:
|
|
145
|
+
>>> problem = Problem()
|
|
146
|
+
>>> problem.addVariables(["a", "b"], [1, 2, 3])
|
|
147
|
+
>>> problem.addConstraint(lambda a, b: b == a+1, ["a", "b"])
|
|
148
|
+
>>> problem.addConstraint("b == a+1 and a+b >= 2") # experimental string format, automatically parsed, preferable over callables
|
|
149
|
+
>>> solutions = problem.getSolutions()
|
|
150
|
+
>>>
|
|
151
|
+
|
|
152
|
+
Args:
|
|
153
|
+
constraint (instance of :py:class:`Constraint`, function to be wrapped by :py:class:`FunctionConstraint`, or string expression):
|
|
154
|
+
Constraint to be included in the problem. Can be either a Constraint, a callable (function or lambda), or Python-evaluable string expression that will be parsed automatically.
|
|
155
|
+
variables (set or sequence of variables): :py:class:`Variables` affected
|
|
156
|
+
by the constraint (default to all variables). Depending
|
|
157
|
+
on the constraint type the order may be important.
|
|
158
|
+
""" # noqa: E501
|
|
159
|
+
# compile string constraints (variables argument ignored as it is inferred from the string and may be reordered)
|
|
160
|
+
if isinstance(constraint, str):
|
|
161
|
+
self._str_constraints.append(constraint)
|
|
162
|
+
return
|
|
163
|
+
elif isinstance(constraint, list):
|
|
164
|
+
assert all(isinstance(c, str) for c in constraint), f"Expected constraints to be strings, got {constraint}"
|
|
165
|
+
self._str_constraints.extend(constraint)
|
|
166
|
+
return
|
|
167
|
+
|
|
168
|
+
# add regular constraints
|
|
169
|
+
if not isinstance(constraint, Constraint):
|
|
170
|
+
if callable(constraint):
|
|
171
|
+
# future warn("A function or lambda has been used for a constraint, consider using string constraints")
|
|
172
|
+
constraint = FunctionConstraint(constraint)
|
|
173
|
+
elif isinstance(constraint, str):
|
|
174
|
+
constraint = CompilableFunctionConstraint(constraint)
|
|
175
|
+
else:
|
|
176
|
+
msg = "Constraints must be instances of subclasses " "of the Constraint class"
|
|
177
|
+
raise ValueError(msg)
|
|
178
|
+
self._constraints.append((constraint, variables))
|
|
179
|
+
|
|
180
|
+
def getSolution(self):
|
|
181
|
+
"""Find and return a solution to the problem.
|
|
182
|
+
|
|
183
|
+
Example:
|
|
184
|
+
>>> problem = Problem()
|
|
185
|
+
>>> problem.getSolution() is None
|
|
186
|
+
True
|
|
187
|
+
>>> problem.addVariables(["a"], [42])
|
|
188
|
+
>>> problem.getSolution()
|
|
189
|
+
{'a': 42}
|
|
190
|
+
|
|
191
|
+
Returns:
|
|
192
|
+
dictionary mapping variables to values: Solution for the problem
|
|
193
|
+
"""
|
|
194
|
+
domains, constraints, vconstraints = self._getArgs(picklable=self._solver.requires_pickling)
|
|
195
|
+
if not domains:
|
|
196
|
+
return None
|
|
197
|
+
return self._solver.getSolution(domains, constraints, vconstraints)
|
|
198
|
+
|
|
199
|
+
def getSolutions(self):
|
|
200
|
+
"""Find and return all solutions to the problem.
|
|
201
|
+
|
|
202
|
+
Example:
|
|
203
|
+
>>> problem = Problem()
|
|
204
|
+
>>> problem.getSolutions() == []
|
|
205
|
+
True
|
|
206
|
+
>>> problem.addVariables(["a"], [42])
|
|
207
|
+
>>> problem.getSolutions()
|
|
208
|
+
[{'a': 42}]
|
|
209
|
+
|
|
210
|
+
Returns:
|
|
211
|
+
list of dictionaries mapping variables to values: All solutions for the problem
|
|
212
|
+
"""
|
|
213
|
+
domains, constraints, vconstraints = self._getArgs(picklable=self._solver.requires_pickling)
|
|
214
|
+
if not domains:
|
|
215
|
+
return []
|
|
216
|
+
return self._solver.getSolutions(domains, constraints, vconstraints)
|
|
217
|
+
|
|
218
|
+
def getSolutionIter(self):
|
|
219
|
+
"""Return an iterator to the solutions of the problem.
|
|
220
|
+
|
|
221
|
+
Example:
|
|
222
|
+
>>> problem = Problem()
|
|
223
|
+
>>> list(problem.getSolutionIter()) == []
|
|
224
|
+
True
|
|
225
|
+
>>> problem.addVariables(["a"], [42])
|
|
226
|
+
>>> iter = problem.getSolutionIter()
|
|
227
|
+
>>> next(iter)
|
|
228
|
+
{'a': 42}
|
|
229
|
+
>>> next(iter)
|
|
230
|
+
Traceback (most recent call last):
|
|
231
|
+
...
|
|
232
|
+
StopIteration
|
|
233
|
+
"""
|
|
234
|
+
domains, constraints, vconstraints = self._getArgs(picklable=self._solver.requires_pickling)
|
|
235
|
+
if not domains:
|
|
236
|
+
return iter(())
|
|
237
|
+
return self._solver.getSolutionIter(domains, constraints, vconstraints)
|
|
238
|
+
|
|
239
|
+
def getSolutionsOrderedList(self, order: list[str] = None) -> list[tuple]:
|
|
240
|
+
"""Returns the solutions as a list of tuples, with each solution tuple ordered according to `order`."""
|
|
241
|
+
solutions: list[dict] = self.getSolutions()
|
|
242
|
+
if order is None or len(order) == 1:
|
|
243
|
+
return list(tuple(solution.values()) for solution in solutions)
|
|
244
|
+
get_in_order = itemgetter(*order)
|
|
245
|
+
return list(get_in_order(params) for params in solutions)
|
|
246
|
+
|
|
247
|
+
def getSolutionsAsListDict(
|
|
248
|
+
self, order: list[str] = None, validate: bool = True
|
|
249
|
+
) -> tuple[list[tuple], dict[tuple, int], int]: # noqa: E501
|
|
250
|
+
"""Returns the searchspace as a list of tuples, a dict of the searchspace for fast lookups and the size."""
|
|
251
|
+
solutions_list = self.getSolutionsOrderedList(order)
|
|
252
|
+
size_list = len(solutions_list)
|
|
253
|
+
solutions_dict: dict = dict(zip(solutions_list, range(size_list)))
|
|
254
|
+
if validate:
|
|
255
|
+
# check for duplicates
|
|
256
|
+
size_dict = len(solutions_dict)
|
|
257
|
+
if size_list != size_dict:
|
|
258
|
+
raise ValueError(
|
|
259
|
+
f"{size_list - size_dict} duplicate parameter configurations out of {size_dict} unique.",
|
|
260
|
+
f"Duplicate configs: {list(set([c for c in solutions_list if solutions_list.count(c) > 1]))}",
|
|
261
|
+
f"Constraints: {self._constraints}, {self._str_constraints}"
|
|
262
|
+
)
|
|
263
|
+
return (
|
|
264
|
+
solutions_list,
|
|
265
|
+
solutions_dict,
|
|
266
|
+
size_list,
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
def _getArgs(self, picklable=False):
|
|
270
|
+
domains = self._variables.copy()
|
|
271
|
+
allvariables = domains.keys()
|
|
272
|
+
constraints: list[tuple[Constraint, list]] = []
|
|
273
|
+
|
|
274
|
+
# parse string constraints
|
|
275
|
+
if len(self._str_constraints) > 0:
|
|
276
|
+
# warn("String constraints are a beta feature, please report issues experienced.") # future: remove
|
|
277
|
+
for constraint in self._str_constraints:
|
|
278
|
+
parsed = compile_to_constraints([constraint], domains, picklable=picklable)
|
|
279
|
+
for c, v, _ in parsed:
|
|
280
|
+
self.addConstraint(c, v)
|
|
281
|
+
|
|
282
|
+
# add regular constraints
|
|
283
|
+
for constraint, variables in self._constraints:
|
|
284
|
+
if not variables:
|
|
285
|
+
variables = list(allvariables)
|
|
286
|
+
constraints.append((constraint, variables))
|
|
287
|
+
|
|
288
|
+
# check if there are any precompiled FunctionConstraints when there shouldn't be
|
|
289
|
+
if picklable:
|
|
290
|
+
assert not any(isinstance(c, FunctionConstraint) for c, _ in constraints), f"You have used FunctionConstraints with ParallelSolver(process_mode=True). Please use string constraints instead (see https://python-constraint.github.io/python-constraint/reference.html#constraint.ParallelSolver docs as to why)" # noqa E501
|
|
291
|
+
|
|
292
|
+
vconstraints = {}
|
|
293
|
+
for variable in domains:
|
|
294
|
+
vconstraints[variable] = []
|
|
295
|
+
for constraint, variables in constraints:
|
|
296
|
+
for variable in variables:
|
|
297
|
+
vconstraints[variable].append((constraint, variables))
|
|
298
|
+
for constraint, variables in constraints[:]:
|
|
299
|
+
constraint.preProcess(variables, domains, constraints, vconstraints)
|
|
300
|
+
for domain in domains.values():
|
|
301
|
+
domain.resetState()
|
|
302
|
+
if not domain:
|
|
303
|
+
return None, None, None
|
|
304
|
+
# doArc8(getArcs(domains, constraints), domains, {})
|
|
305
|
+
return domains, constraints, vconstraints
|