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/parser.py
ADDED
|
@@ -0,0 +1,446 @@
|
|
|
1
|
+
"""Module containing the code for parsing string constraints."""
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from types import FunctionType
|
|
5
|
+
from typing import Union, Optional
|
|
6
|
+
from constraint.constraints import (
|
|
7
|
+
AllDifferentConstraint,
|
|
8
|
+
AllEqualConstraint,
|
|
9
|
+
Constraint,
|
|
10
|
+
ExactSumConstraint,
|
|
11
|
+
MinSumConstraint,
|
|
12
|
+
MaxSumConstraint,
|
|
13
|
+
ExactProdConstraint,
|
|
14
|
+
MinProdConstraint,
|
|
15
|
+
MaxProdConstraint,
|
|
16
|
+
FunctionConstraint,
|
|
17
|
+
CompilableFunctionConstraint,
|
|
18
|
+
VariableExactSumConstraint,
|
|
19
|
+
VariableMinSumConstraint,
|
|
20
|
+
VariableMaxSumConstraint,
|
|
21
|
+
VariableExactProdConstraint,
|
|
22
|
+
VariableMinProdConstraint,
|
|
23
|
+
VariableMaxProdConstraint,
|
|
24
|
+
# TODO implement parsing for these constraints:
|
|
25
|
+
# InSetConstraint,
|
|
26
|
+
# NotInSetConstraint,
|
|
27
|
+
# SomeInSetConstraint,
|
|
28
|
+
# SomeNotInSetConstraint,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def parse_restrictions(restrictions: list[str], tune_params: dict) -> list[tuple[Union[Constraint, str], list[str]]]:
|
|
33
|
+
"""Parses restrictions (constraints in string format) from a list of strings into compilable functions and constraints. Returns a list of tuples of (strings or constraints) and parameters.""" # noqa: E501
|
|
34
|
+
# rewrite the restrictions so variables are singled out
|
|
35
|
+
regex_match_variable = r"([a-zA-Z_$][a-zA-Z_$0-9]*)"
|
|
36
|
+
regex_match_variable_or_constant = r"([a-zA-Z_$0-9]*)"
|
|
37
|
+
|
|
38
|
+
def replace_params(match_object):
|
|
39
|
+
key = match_object.group(1)
|
|
40
|
+
if key in tune_params:
|
|
41
|
+
param = str(key)
|
|
42
|
+
return "params[params_index['" + param + "']]"
|
|
43
|
+
else:
|
|
44
|
+
return key
|
|
45
|
+
|
|
46
|
+
def replace_params_split(match_object):
|
|
47
|
+
# careful: has side-effect of adding to set `params_used`
|
|
48
|
+
key = match_object.group(1)
|
|
49
|
+
if key in tune_params:
|
|
50
|
+
param = str(key)
|
|
51
|
+
params_used.add(param)
|
|
52
|
+
return param
|
|
53
|
+
else:
|
|
54
|
+
return key
|
|
55
|
+
|
|
56
|
+
def to_multiple_restrictions(restrictions: list[str]) -> list[str]:
|
|
57
|
+
"""Split the restrictions into multiple restriction where possible (e.g. 3 <= x * y < 9 <= z -> [(MinProd(3), [x, y]), (MaxProd(9-1), [x, y]), (MinProd(9), [z])]).""" # noqa: E501
|
|
58
|
+
split_restrictions = list()
|
|
59
|
+
for res in restrictions:
|
|
60
|
+
# if there are logic chains in the restriction, skip splitting further
|
|
61
|
+
if " and " in res or " or " in res:
|
|
62
|
+
split_restrictions.append(res)
|
|
63
|
+
continue
|
|
64
|
+
# find the indices of splittable comparators
|
|
65
|
+
comparators = ["<=", ">=", ">", "<"]
|
|
66
|
+
comparators_indices = [(m.start(0), m.end(0)) for m in re.finditer("|".join(comparators), res)]
|
|
67
|
+
if len(comparators_indices) <= 1:
|
|
68
|
+
# this can't be split further
|
|
69
|
+
split_restrictions.append(res)
|
|
70
|
+
continue
|
|
71
|
+
# split the restrictions from the previous to the next comparator
|
|
72
|
+
for index in range(len(comparators_indices)):
|
|
73
|
+
temp_copy = res
|
|
74
|
+
prev_stop = comparators_indices[index - 1][1] + 1 if index > 0 else 0
|
|
75
|
+
next_stop = (
|
|
76
|
+
comparators_indices[index + 1][0] if index < len(comparators_indices) - 1 else len(temp_copy)
|
|
77
|
+
)
|
|
78
|
+
split_restrictions.append(temp_copy[prev_stop:next_stop].strip())
|
|
79
|
+
return split_restrictions
|
|
80
|
+
|
|
81
|
+
def to_numeric_constraint(restriction: str, params: list[str]) -> Optional[
|
|
82
|
+
Union[
|
|
83
|
+
MinSumConstraint,
|
|
84
|
+
VariableMinSumConstraint,
|
|
85
|
+
ExactSumConstraint,
|
|
86
|
+
VariableExactSumConstraint,
|
|
87
|
+
MaxSumConstraint,
|
|
88
|
+
VariableMaxSumConstraint,
|
|
89
|
+
MinProdConstraint,
|
|
90
|
+
VariableMinProdConstraint,
|
|
91
|
+
ExactProdConstraint,
|
|
92
|
+
VariableExactProdConstraint,
|
|
93
|
+
MaxProdConstraint,
|
|
94
|
+
VariableMaxProdConstraint,
|
|
95
|
+
]
|
|
96
|
+
]: # noqa: E501
|
|
97
|
+
"""Converts a restriction to a built-in numeric constraint if possible."""
|
|
98
|
+
# first check if all parameters have only numbers as values
|
|
99
|
+
if len(params) == 0 or not all(all(isinstance(v, (int, float)) for v in tune_params[p]) for p in params):
|
|
100
|
+
return None
|
|
101
|
+
|
|
102
|
+
comparators = ["<=", "==", ">=", ">", "<"]
|
|
103
|
+
comparators_found = re.findall("|".join(comparators), restriction)
|
|
104
|
+
# check if there is exactly one comparator, if not, return None
|
|
105
|
+
if len(comparators_found) != 1:
|
|
106
|
+
return None
|
|
107
|
+
comparator = comparators_found[0]
|
|
108
|
+
|
|
109
|
+
# split the string on the comparison and remove leading and trailing whitespace
|
|
110
|
+
left, right = tuple(s.strip() for s in restriction.split(comparator))
|
|
111
|
+
|
|
112
|
+
# if we have an inverse operation, rewrite to the other side
|
|
113
|
+
operators_left = extract_operators(left)
|
|
114
|
+
operators_right = extract_operators(right)
|
|
115
|
+
if len(operators_left) > 0 and len(operators_right) > 0:
|
|
116
|
+
# if there are operators on both sides, we can't handle this yet
|
|
117
|
+
return None
|
|
118
|
+
unique_operators_left = set(operators_left)
|
|
119
|
+
unique_operators_right = set(operators_right)
|
|
120
|
+
unique_operators = unique_operators_left.union(unique_operators_right)
|
|
121
|
+
if len(unique_operators) == 1:
|
|
122
|
+
variables_on_left = len(unique_operators_left) > 0
|
|
123
|
+
swapped_side_first_component = re.search(
|
|
124
|
+
regex_match_variable_or_constant, left if variables_on_left else right
|
|
125
|
+
) # noqa: E501
|
|
126
|
+
if swapped_side_first_component is None:
|
|
127
|
+
# if there is no variable on the left side, we can't handle this yet
|
|
128
|
+
return None
|
|
129
|
+
else:
|
|
130
|
+
swapped_side_first_component = swapped_side_first_component.group(0)
|
|
131
|
+
if "-" in unique_operators:
|
|
132
|
+
if not variables_on_left:
|
|
133
|
+
# e.g. "G == B-M" becomes "G+M == B"
|
|
134
|
+
right_remainder = right[len(swapped_side_first_component) :]
|
|
135
|
+
left_swap = right_remainder.replace("-", "+")
|
|
136
|
+
restriction = f"{left}{left_swap}{comparator}{swapped_side_first_component}"
|
|
137
|
+
else:
|
|
138
|
+
# e.g. "B-M == G" becomes "B == G+M"
|
|
139
|
+
left_remainder = left[len(swapped_side_first_component) :]
|
|
140
|
+
right_swap = left_remainder.replace("-", "+")
|
|
141
|
+
restriction = f"{swapped_side_first_component}{comparator}{right}{right_swap}"
|
|
142
|
+
if "/" in unique_operators:
|
|
143
|
+
if not variables_on_left:
|
|
144
|
+
# e.g. "G == B/M" becomes "G*M == B"
|
|
145
|
+
right_remainder = right[len(swapped_side_first_component) :]
|
|
146
|
+
left_swap = right_remainder.replace("/", "*")
|
|
147
|
+
restriction = f"{left}{left_swap}{comparator}{swapped_side_first_component}"
|
|
148
|
+
else:
|
|
149
|
+
# e.g. "B/M == G" becomes "B == G*M"
|
|
150
|
+
left_remainder = left[len(swapped_side_first_component) :]
|
|
151
|
+
right_swap = left_remainder.replace("/", "*")
|
|
152
|
+
restriction = f"{swapped_side_first_component}{comparator}{right}{right_swap}"
|
|
153
|
+
|
|
154
|
+
# we have a potentially rewritten restriction, split again
|
|
155
|
+
left, right = tuple(s.strip() for s in restriction.split(comparator))
|
|
156
|
+
operators_left = extract_operators(left)
|
|
157
|
+
operators_right = extract_operators(right)
|
|
158
|
+
unique_operators_left = set(operators_left)
|
|
159
|
+
unique_operators_right = set(operators_right)
|
|
160
|
+
unique_operators = unique_operators_left.union(unique_operators_right)
|
|
161
|
+
|
|
162
|
+
# find out which side is the constant number
|
|
163
|
+
# either the left or right side of the equation must evaluate to a constant number, otherwise we use a VariableConstraint # noqa: E501
|
|
164
|
+
left_num = is_or_evals_to_number(left)
|
|
165
|
+
right_num = is_or_evals_to_number(right)
|
|
166
|
+
if (left_num is None and right_num is None) or (left_num is not None and right_num is not None):
|
|
167
|
+
# if both sides are parameters, try to use the VariableConstraints
|
|
168
|
+
variable_supported_operators = ["+", "*"]
|
|
169
|
+
# variables = [s.strip() for s in list(left + right) if s not in variable_supported_operators]
|
|
170
|
+
variables = re.findall(regex_match_variable, restriction)
|
|
171
|
+
|
|
172
|
+
# if the restriction contains more than the variables and supported operators, return None
|
|
173
|
+
if len(variables) == 0:
|
|
174
|
+
return None
|
|
175
|
+
if any(var.strip() not in tune_params for var in variables):
|
|
176
|
+
raise ValueError(f"Variables {variables} not in tune_params {tune_params.keys()}")
|
|
177
|
+
if (
|
|
178
|
+
len(re.findall(r"[+-]?\d+", restriction)) > 0
|
|
179
|
+
): # TODO adjust when we support modifiers such as multipliers (see roadmap) # noqa: E501
|
|
180
|
+
# if the restriction contains numbers, return None
|
|
181
|
+
return None
|
|
182
|
+
|
|
183
|
+
# find all unique variable_supported_operators in the restriction, can have at most one
|
|
184
|
+
variable_operators_left = list(s.strip() for s in list(left) if s in variable_supported_operators)
|
|
185
|
+
variable_operators_right = list(s.strip() for s in list(right) if s in variable_supported_operators)
|
|
186
|
+
variable_unique_operators = list(set(variable_operators_left).union(set(variable_operators_right)))
|
|
187
|
+
# if there is a mix of operators (e.g. 'x + y * z == a') or multiple variables on both sides, return None
|
|
188
|
+
if (
|
|
189
|
+
len(variable_unique_operators) <= 1
|
|
190
|
+
and all(s.strip() in params for s in variables)
|
|
191
|
+
and (len(unique_operators_left) == 0 or len(unique_operators_right) == 0)
|
|
192
|
+
): # noqa: E501
|
|
193
|
+
variables_on_left = len(unique_operators_left) > 0
|
|
194
|
+
if len(variable_unique_operators) == 0 or variable_unique_operators[0] == "+":
|
|
195
|
+
if comparator == "==":
|
|
196
|
+
return (
|
|
197
|
+
VariableExactSumConstraint(variables[-1], variables[:-1])
|
|
198
|
+
if variables_on_left
|
|
199
|
+
else VariableExactSumConstraint(variables[0], variables[1:])
|
|
200
|
+
) # noqa: E501
|
|
201
|
+
elif comparator == "<=":
|
|
202
|
+
# "B+C <= A" (maxsum) if variables_on_left else "A <= B+C" (minsum)
|
|
203
|
+
return (
|
|
204
|
+
VariableMaxSumConstraint(variables[-1], variables[:-1])
|
|
205
|
+
if variables_on_left
|
|
206
|
+
else VariableMinSumConstraint(variables[0], variables[1:])
|
|
207
|
+
) # noqa: E501
|
|
208
|
+
elif comparator == ">=":
|
|
209
|
+
# "B+C >= A" (minsum) if variables_on_left else "A >= B+C" (maxsum)
|
|
210
|
+
return (
|
|
211
|
+
VariableMinSumConstraint(variables[-1], variables[:-1])
|
|
212
|
+
if variables_on_left
|
|
213
|
+
else VariableMaxSumConstraint(variables[0], variables[1:])
|
|
214
|
+
) # noqa: E501
|
|
215
|
+
elif variable_unique_operators[0] == "*":
|
|
216
|
+
if comparator == "==":
|
|
217
|
+
return (
|
|
218
|
+
VariableExactProdConstraint(variables[-1], variables[:-1])
|
|
219
|
+
if variables_on_left
|
|
220
|
+
else VariableExactProdConstraint(variables[0], variables[1:])
|
|
221
|
+
) # noqa: E501
|
|
222
|
+
elif comparator == "<=":
|
|
223
|
+
# "B*C <= A" (maxprod) if variables_on_left else "A <= B*C" (minprod)
|
|
224
|
+
return (
|
|
225
|
+
VariableMaxProdConstraint(variables[-1], variables[:-1])
|
|
226
|
+
if variables_on_left
|
|
227
|
+
else VariableMinProdConstraint(variables[0], variables[1:])
|
|
228
|
+
) # noqa: E501
|
|
229
|
+
elif comparator == ">=":
|
|
230
|
+
# "B*C >= A" (minprod) if variables_on_left else "A >= B*C" (maxprod)
|
|
231
|
+
return (
|
|
232
|
+
VariableMinProdConstraint(variables[-1], variables[:-1])
|
|
233
|
+
if variables_on_left
|
|
234
|
+
else VariableMaxProdConstraint(variables[0], variables[1:])
|
|
235
|
+
) # noqa: E501
|
|
236
|
+
|
|
237
|
+
# left_num and right_num can't both be constants, or for other reasons we can't use a VariableConstraint
|
|
238
|
+
return None
|
|
239
|
+
|
|
240
|
+
# if one side is a number, the other side must be a variable or expression
|
|
241
|
+
number, variables, variables_on_left = (
|
|
242
|
+
(left_num, right.strip(), False) if left_num is not None else (right_num, left.strip(), True)
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
# we can map '>' to '>=' and '<' to '<=' by adding a tiny offset to the number
|
|
246
|
+
offset = 1e-12
|
|
247
|
+
if comparator == "<":
|
|
248
|
+
if variables_on_left:
|
|
249
|
+
# (x < 2) == (x <= 2-offset)
|
|
250
|
+
number -= offset
|
|
251
|
+
else:
|
|
252
|
+
# (2 < x) == (2+offset <= x)
|
|
253
|
+
number += offset
|
|
254
|
+
elif comparator == ">":
|
|
255
|
+
if variables_on_left:
|
|
256
|
+
# (x > 2) == (x >= 2+offset)
|
|
257
|
+
number += offset
|
|
258
|
+
else:
|
|
259
|
+
# (2 > x) == (2-offset >= x)
|
|
260
|
+
number -= offset
|
|
261
|
+
|
|
262
|
+
# check if an operator is applied on the variables, if not return
|
|
263
|
+
operators = [r"\*\*", r"\*", r"\+"]
|
|
264
|
+
operators_found = re.findall(str("|".join(operators)), variables)
|
|
265
|
+
if len(operators_found) == 0:
|
|
266
|
+
# no operators found, return only based on comparator
|
|
267
|
+
if len(params) != 1 or variables not in params:
|
|
268
|
+
# there were more than one variable but no operator
|
|
269
|
+
return None
|
|
270
|
+
# map to a Constraint
|
|
271
|
+
# if there are restrictions with a single variable, it will be used to prune the domain at the start
|
|
272
|
+
elif comparator == "==":
|
|
273
|
+
return ExactSumConstraint(number)
|
|
274
|
+
elif comparator == "<=" or comparator == "<":
|
|
275
|
+
return MaxSumConstraint(number) if variables_on_left else MinSumConstraint(number)
|
|
276
|
+
elif comparator == ">=" or comparator == ">":
|
|
277
|
+
return MinSumConstraint(number) if variables_on_left else MaxSumConstraint(number)
|
|
278
|
+
raise ValueError(f"Invalid comparator {comparator}")
|
|
279
|
+
|
|
280
|
+
# check which operator is applied on the variables
|
|
281
|
+
operator = operators_found[0]
|
|
282
|
+
if not all(o == operator for o in operators_found):
|
|
283
|
+
# if there is a mix of operators (e.g. 'x + y * z == 3'), return None
|
|
284
|
+
return None
|
|
285
|
+
|
|
286
|
+
# split the string on the comparison
|
|
287
|
+
splitted = variables.split(operator)
|
|
288
|
+
# check if there are only pure, non-recurring variables (no operations or constants) in the restriction
|
|
289
|
+
if len(splitted) == len(params) and all(s.strip() in params for s in splitted):
|
|
290
|
+
# map to a Constraint
|
|
291
|
+
if operator == "**":
|
|
292
|
+
# power operations are not (yet) supported, added to avoid matching the double asterisk
|
|
293
|
+
return None
|
|
294
|
+
elif operator == "*":
|
|
295
|
+
if comparator == "==":
|
|
296
|
+
return ExactProdConstraint(number)
|
|
297
|
+
elif comparator == "<=" or comparator == "<":
|
|
298
|
+
return MaxProdConstraint(number) if variables_on_left else MinProdConstraint(number)
|
|
299
|
+
elif comparator == ">=" or comparator == ">":
|
|
300
|
+
return MinProdConstraint(number) if variables_on_left else MaxProdConstraint(number)
|
|
301
|
+
elif operator == "+":
|
|
302
|
+
if comparator == "==":
|
|
303
|
+
return ExactSumConstraint(number)
|
|
304
|
+
elif comparator == "<=" or comparator == "<":
|
|
305
|
+
# raise ValueError(restriction, comparator)
|
|
306
|
+
return MaxSumConstraint(number) if variables_on_left else MinSumConstraint(number)
|
|
307
|
+
elif comparator == ">=" or comparator == ">":
|
|
308
|
+
return MinSumConstraint(number) if variables_on_left else MaxSumConstraint(number)
|
|
309
|
+
else:
|
|
310
|
+
raise ValueError(f"Invalid operator {operator}")
|
|
311
|
+
return None
|
|
312
|
+
|
|
313
|
+
def to_equality_constraint(
|
|
314
|
+
restriction: str, params: list[str]
|
|
315
|
+
) -> Optional[Union[AllEqualConstraint, AllDifferentConstraint]]:
|
|
316
|
+
"""Converts a restriction to either an equality or inequality constraint on all the parameters if possible."""
|
|
317
|
+
# check if all parameters are involved
|
|
318
|
+
if len(params) != len(tune_params):
|
|
319
|
+
return None
|
|
320
|
+
|
|
321
|
+
# find whether (in)equalities appear in this restriction
|
|
322
|
+
equalities_found = re.findall("==", restriction)
|
|
323
|
+
inequalities_found = re.findall("!=", restriction)
|
|
324
|
+
# check if one of the two have been found, if none or both have been found, return None
|
|
325
|
+
if not (bool(len(equalities_found) > 0) ^ bool(len(inequalities_found) > 0)):
|
|
326
|
+
return None
|
|
327
|
+
comparator = equalities_found[0] if len(equalities_found) > 0 else inequalities_found[0]
|
|
328
|
+
|
|
329
|
+
# split the string on the comparison
|
|
330
|
+
splitted = restriction.split(comparator)
|
|
331
|
+
# check if there are only pure, non-recurring variables (no operations or constants) in the restriction
|
|
332
|
+
if len(splitted) == len(params) and all(s.strip() in params for s in splitted):
|
|
333
|
+
# map to a Constraint
|
|
334
|
+
if comparator == "==":
|
|
335
|
+
return AllEqualConstraint()
|
|
336
|
+
elif comparator == "!=":
|
|
337
|
+
return AllDifferentConstraint()
|
|
338
|
+
return ValueError(f"Not possible: comparator should be '==' or '!=', is {comparator}")
|
|
339
|
+
return None
|
|
340
|
+
|
|
341
|
+
# remove functionally duplicate restrictions (preserves order and whitespace)
|
|
342
|
+
if all(isinstance(r, str) for r in restrictions):
|
|
343
|
+
# clean the restriction strings to functional equivalence
|
|
344
|
+
restrictions_cleaned = [r.replace(" ", "") for r in restrictions]
|
|
345
|
+
restrictions_cleaned_unique = list(dict.fromkeys(restrictions_cleaned)) # dict preserves order
|
|
346
|
+
# get the indices of the unique restrictions, use these to build a new list of restrictions
|
|
347
|
+
restrictions_unique_indices = [restrictions_cleaned.index(r) for r in restrictions_cleaned_unique]
|
|
348
|
+
restrictions = [restrictions[i] for i in restrictions_unique_indices]
|
|
349
|
+
|
|
350
|
+
# create the parsed restrictions, split into multiple restrictions where possible
|
|
351
|
+
restrictions = to_multiple_restrictions(restrictions)
|
|
352
|
+
# split into functions that only take their relevant parameters
|
|
353
|
+
parsed_restrictions = list()
|
|
354
|
+
for res in restrictions:
|
|
355
|
+
params_used: set[str] = set()
|
|
356
|
+
parsed_restriction = re.sub(regex_match_variable, replace_params_split, res).strip()
|
|
357
|
+
params_used_list = list(params_used)
|
|
358
|
+
finalized_constraint = None
|
|
359
|
+
if " or " not in res and " and " not in res:
|
|
360
|
+
# if applicable, strip the outermost round brackets
|
|
361
|
+
while (
|
|
362
|
+
parsed_restriction[0] == "("
|
|
363
|
+
and parsed_restriction[-1] == ")"
|
|
364
|
+
and "(" not in parsed_restriction[1:]
|
|
365
|
+
and ")" not in parsed_restriction[:1]
|
|
366
|
+
):
|
|
367
|
+
parsed_restriction = parsed_restriction[1:-1]
|
|
368
|
+
# check if we can turn this into the built-in numeric comparison constraint
|
|
369
|
+
finalized_constraint = to_numeric_constraint(parsed_restriction, params_used_list)
|
|
370
|
+
if finalized_constraint is None:
|
|
371
|
+
# check if we can turn this into the built-in equality comparison constraint
|
|
372
|
+
finalized_constraint = to_equality_constraint(parsed_restriction, params_used_list)
|
|
373
|
+
if finalized_constraint is None:
|
|
374
|
+
# we must turn it into a general function
|
|
375
|
+
finalized_constraint = f"def r({', '.join(params_used_list)}): return {parsed_restriction} \n"
|
|
376
|
+
parsed_restrictions.append((finalized_constraint, params_used_list))
|
|
377
|
+
|
|
378
|
+
return parsed_restrictions
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
def compile_to_constraints(
|
|
382
|
+
constraints: list[str], domains: dict, picklable=False
|
|
383
|
+
) -> list[tuple[Constraint, list[str], Union[str, None]]]: # noqa: E501
|
|
384
|
+
"""Parses constraints in string format (referred to as restrictions) from a list of strings into a list of Constraints, parameters used, and source if applicable.
|
|
385
|
+
|
|
386
|
+
Args:
|
|
387
|
+
constraints (list[str]): list of constraints in string format to compile.
|
|
388
|
+
domains (dict): the domains to use.
|
|
389
|
+
picklable (bool, optional): whether to keep constraints such that they can be pickled for parallel solvers. Defaults to False.
|
|
390
|
+
|
|
391
|
+
Returns:
|
|
392
|
+
list of tuples with restrictions, parameters used (list[str]), and source (str) if applicable. Returned restrictions are strings, functions, or Constraints depending on the options provided.
|
|
393
|
+
""" # noqa: E501
|
|
394
|
+
parsed_restrictions = parse_restrictions(constraints, domains)
|
|
395
|
+
compiled_constraints: list[tuple[Constraint, list[str], Union[str, None]]] = list()
|
|
396
|
+
for restriction, params_used in parsed_restrictions:
|
|
397
|
+
if isinstance(restriction, str):
|
|
398
|
+
# if it's a string, wrap it in a (compilable or compiled) function constraint
|
|
399
|
+
if picklable:
|
|
400
|
+
constraint = CompilableFunctionConstraint(restriction)
|
|
401
|
+
else:
|
|
402
|
+
code_object = compile(restriction, "<string>", "exec")
|
|
403
|
+
func = FunctionType(code_object.co_consts[0], globals())
|
|
404
|
+
constraint = FunctionConstraint(func)
|
|
405
|
+
compiled_constraints.append((constraint, params_used, restriction))
|
|
406
|
+
elif isinstance(restriction, Constraint):
|
|
407
|
+
# otherwise it already is a Constraint, pass it directly
|
|
408
|
+
compiled_constraints.append((restriction, params_used, None))
|
|
409
|
+
else:
|
|
410
|
+
raise ValueError(f"Restriction {restriction} is neither a string or Constraint {type(restriction)}")
|
|
411
|
+
|
|
412
|
+
# return the restrictions and used parameters
|
|
413
|
+
return compiled_constraints
|
|
414
|
+
|
|
415
|
+
|
|
416
|
+
# Utility functions
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
def is_or_evals_to_number(s: str) -> Optional[Union[int, float]]:
|
|
420
|
+
"""Check if the string is a number or can be evaluated to a number."""
|
|
421
|
+
try:
|
|
422
|
+
# check if it's a number or solvable to a number (e.g. '32*2')
|
|
423
|
+
number = eval(s)
|
|
424
|
+
assert isinstance(number, (int, float))
|
|
425
|
+
return number
|
|
426
|
+
except Exception:
|
|
427
|
+
# it's not a solvable subexpression, return None
|
|
428
|
+
return None
|
|
429
|
+
|
|
430
|
+
|
|
431
|
+
def extract_operators(expr: str) -> list[str]:
|
|
432
|
+
"""Extracts all operators from an expression string."""
|
|
433
|
+
# Regex for all supported binary operators:
|
|
434
|
+
# supported_operators = ["**", "*", "+", "-", "/"]
|
|
435
|
+
|
|
436
|
+
# remove any whitespace from the expression
|
|
437
|
+
expr = expr.strip().replace(" ", "")
|
|
438
|
+
|
|
439
|
+
# Match ** first to avoid matching * twice
|
|
440
|
+
pattern = r"""
|
|
441
|
+
(?<!\*)\*\* | # match ** but not ***
|
|
442
|
+
(?<=[\w)\d])\- | # binary -: preceded by var/number/closing )
|
|
443
|
+
[+\*/] # match +, *, /
|
|
444
|
+
"""
|
|
445
|
+
matches = re.findall(pattern, expr, re.VERBOSE)
|
|
446
|
+
return [m.strip() for m in matches]
|