solvor 0.3.1__py3-none-any.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.
- solvor/__init__.py +21 -0
- solvor/anneal.py +80 -0
- solvor/bayesian.py +149 -0
- solvor/cp.py +289 -0
- solvor/flow.py +198 -0
- solvor/genetic.py +101 -0
- solvor/gradient.py +140 -0
- solvor/milp.py +182 -0
- solvor/sat.py +275 -0
- solvor/simplex.py +200 -0
- solvor/tabu.py +130 -0
- solvor/types.py +15 -0
- solvor-0.3.1.dist-info/METADATA +281 -0
- solvor-0.3.1.dist-info/RECORD +16 -0
- solvor-0.3.1.dist-info/WHEEL +4 -0
- solvor-0.3.1.dist-info/licenses/LICENSE +201 -0
solvor/__init__.py
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""Solvor - Pure Python Optimization Solvers."""
|
|
2
|
+
|
|
3
|
+
from solvor.types import Status, Result
|
|
4
|
+
from solvor.simplex import solve_lp
|
|
5
|
+
from solvor.tabu import tabu_search, solve_tsp
|
|
6
|
+
from solvor.anneal import anneal
|
|
7
|
+
from solvor.milp import solve_milp
|
|
8
|
+
from solvor.sat import solve_sat
|
|
9
|
+
from solvor.cp import Model
|
|
10
|
+
from solvor.bayesian import bayesian_opt
|
|
11
|
+
from solvor.genetic import evolve
|
|
12
|
+
from solvor.flow import max_flow, min_cost_flow, solve_assignment
|
|
13
|
+
from solvor.gradient import gradient_descent, momentum, adam
|
|
14
|
+
|
|
15
|
+
__all__ = [
|
|
16
|
+
"solve_lp", "solve_milp", "tabu_search", "solve_tsp", "anneal",
|
|
17
|
+
"solve_sat", "Model", "bayesian_opt", "evolve",
|
|
18
|
+
"max_flow", "min_cost_flow", "solve_assignment",
|
|
19
|
+
"gradient_descent", "momentum", "adam",
|
|
20
|
+
"Status", "Result",
|
|
21
|
+
]
|
solvor/anneal.py
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Simulated Annealing - Probabilistic Metaheuristic for Global Optimization
|
|
3
|
+
|
|
4
|
+
Escapes local optima by accepting worse solutions with probability exp(-delta/T).
|
|
5
|
+
Temperature T decreases over time following a cooling schedule, gradually shifting
|
|
6
|
+
from exploration (high T) to exploitation (low T).
|
|
7
|
+
|
|
8
|
+
Usage:
|
|
9
|
+
from solvor.anneal import anneal, Status
|
|
10
|
+
result = anneal(initial, objective_fn, neighbors)
|
|
11
|
+
result = anneal(initial, objective_fn, neighbors, minimize=False)
|
|
12
|
+
|
|
13
|
+
Parameters:
|
|
14
|
+
initial : Starting solution
|
|
15
|
+
objective_fn : solution -> float (value to minimize)
|
|
16
|
+
neighbors : solution -> new_solution (random neighbor generator)
|
|
17
|
+
minimize : True for min, False for max (default: True)
|
|
18
|
+
temperature : Starting temperature (default: 1000.0)
|
|
19
|
+
cooling : Multiplicative cooling factor per iteration (default: 0.9995)
|
|
20
|
+
min_temp : Stop when temperature falls below this (default: 1e-8)
|
|
21
|
+
max_iter : Maximum iterations (default: 100,000)
|
|
22
|
+
|
|
23
|
+
Returns Result(solution, objective, iterations, evaluations, status)
|
|
24
|
+
|
|
25
|
+
Parameter impact:
|
|
26
|
+
temperature too low -> trapped in local_optimum early
|
|
27
|
+
temperature too high -> random walk, slow convergence
|
|
28
|
+
cooling too fast -> insufficient exploration
|
|
29
|
+
cooling too slow -> wastes iterations at high temperature
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
from collections.abc import Callable
|
|
33
|
+
from math import exp
|
|
34
|
+
from random import random
|
|
35
|
+
from solvor.types import Status, Result
|
|
36
|
+
|
|
37
|
+
__all__ = ["anneal", "Status", "Result"]
|
|
38
|
+
|
|
39
|
+
def anneal[T](
|
|
40
|
+
initial: T,
|
|
41
|
+
objective_fn: Callable[[T], float],
|
|
42
|
+
neighbors: Callable[[T], T],
|
|
43
|
+
*,
|
|
44
|
+
minimize: bool = True,
|
|
45
|
+
temperature: float = 1000.0,
|
|
46
|
+
cooling: float = 0.9995,
|
|
47
|
+
min_temp: float = 1e-8,
|
|
48
|
+
max_iter: int = 100_000,
|
|
49
|
+
) -> Result:
|
|
50
|
+
"""(initial, objective_fn, neighbors, opts) -> Result with best_solution found."""
|
|
51
|
+
sign = 1 if minimize else -1
|
|
52
|
+
evals = 0
|
|
53
|
+
|
|
54
|
+
def evaluate(sol):
|
|
55
|
+
nonlocal evals
|
|
56
|
+
evals += 1
|
|
57
|
+
return sign * objective_fn(sol)
|
|
58
|
+
|
|
59
|
+
solution, obj = initial, evaluate(initial)
|
|
60
|
+
best_solution, best_obj = solution, obj
|
|
61
|
+
|
|
62
|
+
for iteration in range(1, max_iter + 1):
|
|
63
|
+
if temperature < min_temp:
|
|
64
|
+
break
|
|
65
|
+
|
|
66
|
+
neighbor = neighbors(solution)
|
|
67
|
+
neighbor_obj = evaluate(neighbor)
|
|
68
|
+
delta = neighbor_obj - obj
|
|
69
|
+
|
|
70
|
+
if delta < 0 or random() < exp(-delta / temperature):
|
|
71
|
+
solution, obj = neighbor, neighbor_obj
|
|
72
|
+
|
|
73
|
+
if obj < best_obj:
|
|
74
|
+
best_solution, best_obj = solution, obj
|
|
75
|
+
|
|
76
|
+
temperature *= cooling
|
|
77
|
+
|
|
78
|
+
final_obj = best_obj * sign
|
|
79
|
+
status = Status.MAX_ITER if iteration == max_iter else Status.FEASIBLE
|
|
80
|
+
return Result(best_solution, final_obj, iteration, evals, status)
|
solvor/bayesian.py
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Bayesian Optimization - Gaussian Process-based Global Optimization
|
|
3
|
+
|
|
4
|
+
Optimizes expensive black-box functions using a Gaussian Process surrogate
|
|
5
|
+
model and Expected Improvement acquisition function. Ideal for hyperparameter
|
|
6
|
+
tuning where function evaluations are costly.
|
|
7
|
+
|
|
8
|
+
Usage:
|
|
9
|
+
from solvor.bayesian import bayesian_opt, Status
|
|
10
|
+
result = bayesian_opt(objective_fn, bounds=[(0, 1), (0, 1)], max_iter=50)
|
|
11
|
+
result = bayesian_opt(f, bounds, minimize=False) # maximize
|
|
12
|
+
|
|
13
|
+
Parameters:
|
|
14
|
+
objective_fn : x -> float (function to optimize, x is list of floats)
|
|
15
|
+
bounds : List of (low, high) tuples for each dimension
|
|
16
|
+
minimize : True for min, False for max (default: True)
|
|
17
|
+
max_iter : Number of iterations (default: 50)
|
|
18
|
+
n_initial : Random initial samples before GP (default: 5)
|
|
19
|
+
seed : Random seed (default: None)
|
|
20
|
+
|
|
21
|
+
Returns Result(solution, objective, iterations, evaluations, status)
|
|
22
|
+
solution = best input found (list of floats)
|
|
23
|
+
objective = best objective value
|
|
24
|
+
|
|
25
|
+
How it works:
|
|
26
|
+
1. Sample n_initial random points
|
|
27
|
+
2. Fit Gaussian Process to observations
|
|
28
|
+
3. Find next point by maximizing Expected Improvement
|
|
29
|
+
4. Evaluate objective at new point, repeat from step 2
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
from collections.abc import Callable, Sequence
|
|
33
|
+
from math import sqrt, exp, pi, erf
|
|
34
|
+
from random import Random
|
|
35
|
+
from solvor.types import Status, Result
|
|
36
|
+
|
|
37
|
+
__all__ = ["bayesian_opt", "Status", "Result"]
|
|
38
|
+
|
|
39
|
+
def bayesian_opt(
|
|
40
|
+
objective_fn: Callable[[Sequence[float]], float],
|
|
41
|
+
bounds: Sequence[tuple[float, float]],
|
|
42
|
+
*,
|
|
43
|
+
minimize: bool = True,
|
|
44
|
+
max_iter: int = 50,
|
|
45
|
+
n_initial: int = 5,
|
|
46
|
+
seed: int | None = None,
|
|
47
|
+
) -> Result:
|
|
48
|
+
"""(objective_fn, bounds, opts) -> Result with best solution found."""
|
|
49
|
+
rng = Random(seed)
|
|
50
|
+
sign = 1 if minimize else -1
|
|
51
|
+
n_dims = len(bounds)
|
|
52
|
+
evals = 0
|
|
53
|
+
|
|
54
|
+
def evaluate(x):
|
|
55
|
+
nonlocal evals
|
|
56
|
+
evals += 1
|
|
57
|
+
return sign * objective_fn(x)
|
|
58
|
+
|
|
59
|
+
def random_point():
|
|
60
|
+
return [rng.uniform(lo, hi) for lo, hi in bounds]
|
|
61
|
+
|
|
62
|
+
xs = [random_point() for _ in range(n_initial)]
|
|
63
|
+
ys = [evaluate(x) for x in xs]
|
|
64
|
+
|
|
65
|
+
best_idx = min(range(len(ys)), key=lambda i: ys[i])
|
|
66
|
+
best_x, best_y = xs[best_idx], ys[best_idx]
|
|
67
|
+
|
|
68
|
+
length_scales = [(hi - lo) / 2 for lo, hi in bounds]
|
|
69
|
+
|
|
70
|
+
def kernel(x1, x2):
|
|
71
|
+
sq_dist = sum(((a - b) / ls) ** 2 for a, b, ls in zip(x1, x2, length_scales))
|
|
72
|
+
return exp(-0.5 * sq_dist)
|
|
73
|
+
|
|
74
|
+
def gp_predict(x_new, X, Y, noise=1e-6):
|
|
75
|
+
n = len(X)
|
|
76
|
+
if n == 0:
|
|
77
|
+
return 0.0, 1.0
|
|
78
|
+
|
|
79
|
+
K = [[kernel(X[i], X[j]) + (noise if i == j else 0) for j in range(n)] for i in range(n)]
|
|
80
|
+
k_star = [kernel(x_new, X[i]) for i in range(n)]
|
|
81
|
+
|
|
82
|
+
K_inv_y = _solve_linear(K, Y)
|
|
83
|
+
K_inv_k = _solve_linear(K, k_star)
|
|
84
|
+
|
|
85
|
+
mu = sum(k_star[i] * K_inv_y[i] for i in range(n))
|
|
86
|
+
var = kernel(x_new, x_new) - sum(k_star[i] * K_inv_k[i] for i in range(n))
|
|
87
|
+
var = max(var, 1e-10)
|
|
88
|
+
|
|
89
|
+
return mu, sqrt(var)
|
|
90
|
+
|
|
91
|
+
def expected_improvement(x, X, Y, best_y, xi=0.01):
|
|
92
|
+
mu, sigma = gp_predict(x, X, Y)
|
|
93
|
+
if sigma < 1e-10:
|
|
94
|
+
return 0.0
|
|
95
|
+
|
|
96
|
+
z = (best_y - mu - xi) / sigma
|
|
97
|
+
ei = (best_y - mu - xi) * _norm_cdf(z) + sigma * _norm_pdf(z)
|
|
98
|
+
return ei
|
|
99
|
+
|
|
100
|
+
for iteration in range(n_initial, max_iter):
|
|
101
|
+
best_ei, best_candidate = -float('inf'), None
|
|
102
|
+
|
|
103
|
+
for _ in range(100 * n_dims):
|
|
104
|
+
candidate = random_point()
|
|
105
|
+
ei = expected_improvement(candidate, xs, ys, best_y)
|
|
106
|
+
if ei > best_ei:
|
|
107
|
+
best_ei, best_candidate = ei, candidate
|
|
108
|
+
|
|
109
|
+
if best_candidate is None:
|
|
110
|
+
best_candidate = random_point()
|
|
111
|
+
|
|
112
|
+
y_new = evaluate(best_candidate)
|
|
113
|
+
xs.append(best_candidate)
|
|
114
|
+
ys.append(y_new)
|
|
115
|
+
|
|
116
|
+
if y_new < best_y:
|
|
117
|
+
best_x, best_y = best_candidate, y_new
|
|
118
|
+
|
|
119
|
+
final_obj = best_y * sign
|
|
120
|
+
return Result(best_x, final_obj, max_iter, evals, Status.FEASIBLE)
|
|
121
|
+
|
|
122
|
+
def _solve_linear(A, b):
|
|
123
|
+
n = len(A)
|
|
124
|
+
aug = [row[:] + [b[i]] for i, row in enumerate(A)]
|
|
125
|
+
|
|
126
|
+
for col in range(n):
|
|
127
|
+
max_row = max(range(col, n), key=lambda r: abs(aug[r][col]))
|
|
128
|
+
aug[col], aug[max_row] = aug[max_row], aug[col]
|
|
129
|
+
|
|
130
|
+
pivot = aug[col][col]
|
|
131
|
+
if abs(pivot) < 1e-12:
|
|
132
|
+
continue
|
|
133
|
+
|
|
134
|
+
for j in range(col, n + 1):
|
|
135
|
+
aug[col][j] /= pivot
|
|
136
|
+
|
|
137
|
+
for row in range(n):
|
|
138
|
+
if row != col:
|
|
139
|
+
factor = aug[row][col]
|
|
140
|
+
for j in range(col, n + 1):
|
|
141
|
+
aug[row][j] -= factor * aug[col][j]
|
|
142
|
+
|
|
143
|
+
return [aug[i][n] for i in range(n)]
|
|
144
|
+
|
|
145
|
+
def _norm_pdf(x):
|
|
146
|
+
return exp(-0.5 * x * x) / sqrt(2 * pi)
|
|
147
|
+
|
|
148
|
+
def _norm_cdf(x):
|
|
149
|
+
return 0.5 * (1 + erf(x / sqrt(2)))
|
solvor/cp.py
ADDED
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CP-SAT Solver - Constraint Programming via SAT Encoding
|
|
3
|
+
|
|
4
|
+
Solves constraint satisfaction problems by encoding integer variables and
|
|
5
|
+
constraints into SAT clauses, then solving with the SAT backend.
|
|
6
|
+
|
|
7
|
+
Usage:
|
|
8
|
+
from solvor.cp import Model, Status
|
|
9
|
+
m = Model()
|
|
10
|
+
x = m.int_var(0, 9, 'x')
|
|
11
|
+
y = m.int_var(0, 9, 'y')
|
|
12
|
+
m.add(m.all_different([x, y]))
|
|
13
|
+
m.add(x + y == 10)
|
|
14
|
+
result = m.solve()
|
|
15
|
+
print(result.solution) # {'x': 3, 'y': 7} or similar
|
|
16
|
+
|
|
17
|
+
Supported constraints:
|
|
18
|
+
all_different(vars) - All variables take distinct values
|
|
19
|
+
sum(vars) == k - Sum equals constant
|
|
20
|
+
sum(vars) <= k - Sum at most constant
|
|
21
|
+
sum(vars) >= k - Sum at least constant
|
|
22
|
+
var == k - Variable equals constant
|
|
23
|
+
var != k - Variable not equal constant
|
|
24
|
+
var1 == var2 - Variables equal
|
|
25
|
+
var1 != var2 - Variables different
|
|
26
|
+
|
|
27
|
+
Returns Result(solution, objective, iterations, evaluations, status)
|
|
28
|
+
solution = dict mapping variable names to values
|
|
29
|
+
objective = 0 (satisfaction only, no optimization)
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
from itertools import combinations
|
|
33
|
+
from solvor.sat import solve_sat, Status as SATStatus
|
|
34
|
+
from solvor.types import Status, Result
|
|
35
|
+
|
|
36
|
+
__all__ = ["Model", "Status", "Result"]
|
|
37
|
+
|
|
38
|
+
class IntVar:
|
|
39
|
+
def __init__(self, model, lb, ub, name):
|
|
40
|
+
self.model = model
|
|
41
|
+
self.lb = lb
|
|
42
|
+
self.ub = ub
|
|
43
|
+
self.name = name
|
|
44
|
+
self.bool_vars = {}
|
|
45
|
+
for v in range(lb, ub + 1):
|
|
46
|
+
self.bool_vars[v] = model._new_bool_var()
|
|
47
|
+
|
|
48
|
+
def __eq__(self, other):
|
|
49
|
+
if isinstance(other, int):
|
|
50
|
+
return ('eq_const', self, other)
|
|
51
|
+
if isinstance(other, IntVar):
|
|
52
|
+
return ('eq_var', self, other)
|
|
53
|
+
return NotImplemented
|
|
54
|
+
|
|
55
|
+
def __ne__(self, other):
|
|
56
|
+
if isinstance(other, int):
|
|
57
|
+
return ('ne_const', self, other)
|
|
58
|
+
if isinstance(other, IntVar):
|
|
59
|
+
return ('ne_var', self, other)
|
|
60
|
+
return NotImplemented
|
|
61
|
+
|
|
62
|
+
def __add__(self, other):
|
|
63
|
+
return ('add', self, other)
|
|
64
|
+
|
|
65
|
+
def __radd__(self, other):
|
|
66
|
+
return ('add', other, self)
|
|
67
|
+
|
|
68
|
+
class Model:
|
|
69
|
+
def __init__(self):
|
|
70
|
+
self._next_bool = 1
|
|
71
|
+
self._vars = {}
|
|
72
|
+
self._constraints = []
|
|
73
|
+
self._clauses = []
|
|
74
|
+
|
|
75
|
+
def _new_bool_var(self):
|
|
76
|
+
v = self._next_bool
|
|
77
|
+
self._next_bool += 1
|
|
78
|
+
return v
|
|
79
|
+
|
|
80
|
+
def int_var(self, lb, ub, name=None):
|
|
81
|
+
"""(lb, ub, name) -> IntVar with domain [lb, ub]."""
|
|
82
|
+
if name is None:
|
|
83
|
+
name = f"_v{len(self._vars)}"
|
|
84
|
+
var = IntVar(self, lb, ub, name)
|
|
85
|
+
self._vars[name] = var
|
|
86
|
+
return var
|
|
87
|
+
|
|
88
|
+
def all_different(self, variables):
|
|
89
|
+
"""(vars) -> constraint that all variables take different values."""
|
|
90
|
+
return ('all_different', tuple(variables))
|
|
91
|
+
|
|
92
|
+
def add(self, constraint):
|
|
93
|
+
"""(constraint) -> add constraint to model."""
|
|
94
|
+
self._constraints.append(constraint)
|
|
95
|
+
|
|
96
|
+
def _encode_exactly_one(self, lits):
|
|
97
|
+
if not lits:
|
|
98
|
+
return
|
|
99
|
+
self._clauses.append(lits)
|
|
100
|
+
for a, b in combinations(lits, 2):
|
|
101
|
+
self._clauses.append([-a, -b])
|
|
102
|
+
|
|
103
|
+
def _encode_at_most_one(self, lits):
|
|
104
|
+
for a, b in combinations(lits, 2):
|
|
105
|
+
self._clauses.append([-a, -b])
|
|
106
|
+
|
|
107
|
+
def _encode_vars(self):
|
|
108
|
+
for var in self._vars.values():
|
|
109
|
+
lits = [var.bool_vars[v] for v in range(var.lb, var.ub + 1)]
|
|
110
|
+
self._encode_exactly_one(lits)
|
|
111
|
+
|
|
112
|
+
def _encode_all_different(self, variables):
|
|
113
|
+
all_vals = set()
|
|
114
|
+
for var in variables:
|
|
115
|
+
all_vals.update(range(var.lb, var.ub + 1))
|
|
116
|
+
|
|
117
|
+
for val in all_vals:
|
|
118
|
+
lits = []
|
|
119
|
+
for var in variables:
|
|
120
|
+
if val in var.bool_vars:
|
|
121
|
+
lits.append(var.bool_vars[val])
|
|
122
|
+
if len(lits) > 1:
|
|
123
|
+
self._encode_at_most_one(lits)
|
|
124
|
+
|
|
125
|
+
def _encode_eq_const(self, var, val):
|
|
126
|
+
if val in var.bool_vars:
|
|
127
|
+
self._clauses.append([var.bool_vars[val]])
|
|
128
|
+
else:
|
|
129
|
+
self._clauses.append([])
|
|
130
|
+
|
|
131
|
+
def _encode_ne_const(self, var, val):
|
|
132
|
+
if val in var.bool_vars:
|
|
133
|
+
self._clauses.append([-var.bool_vars[val]])
|
|
134
|
+
|
|
135
|
+
def _encode_eq_var(self, var1, var2):
|
|
136
|
+
common = set(var1.bool_vars.keys()) & set(var2.bool_vars.keys())
|
|
137
|
+
for val in common:
|
|
138
|
+
self._clauses.append([-var1.bool_vars[val], var2.bool_vars[val]])
|
|
139
|
+
self._clauses.append([var1.bool_vars[val], -var2.bool_vars[val]])
|
|
140
|
+
|
|
141
|
+
for val in set(var1.bool_vars.keys()) - common:
|
|
142
|
+
self._clauses.append([-var1.bool_vars[val]])
|
|
143
|
+
for val in set(var2.bool_vars.keys()) - common:
|
|
144
|
+
self._clauses.append([-var2.bool_vars[val]])
|
|
145
|
+
|
|
146
|
+
def _encode_ne_var(self, var1, var2):
|
|
147
|
+
common = set(var1.bool_vars.keys()) & set(var2.bool_vars.keys())
|
|
148
|
+
for val in common:
|
|
149
|
+
self._clauses.append([-var1.bool_vars[val], -var2.bool_vars[val]])
|
|
150
|
+
|
|
151
|
+
def _flatten_sum(self, expr):
|
|
152
|
+
terms = []
|
|
153
|
+
const = 0
|
|
154
|
+
|
|
155
|
+
def flatten(e):
|
|
156
|
+
nonlocal const
|
|
157
|
+
if isinstance(e, IntVar):
|
|
158
|
+
terms.append(e)
|
|
159
|
+
elif isinstance(e, int):
|
|
160
|
+
const += e
|
|
161
|
+
elif isinstance(e, tuple) and e[0] == 'add':
|
|
162
|
+
flatten(e[1])
|
|
163
|
+
flatten(e[2])
|
|
164
|
+
|
|
165
|
+
flatten(expr)
|
|
166
|
+
return terms, const
|
|
167
|
+
|
|
168
|
+
def _encode_sum_eq(self, variables, target):
|
|
169
|
+
n = len(variables)
|
|
170
|
+
if n == 0:
|
|
171
|
+
if target != 0:
|
|
172
|
+
self._clauses.append([])
|
|
173
|
+
return
|
|
174
|
+
|
|
175
|
+
min_sum = sum(v.lb for v in variables)
|
|
176
|
+
max_sum = sum(v.ub for v in variables)
|
|
177
|
+
|
|
178
|
+
if target < min_sum or target > max_sum:
|
|
179
|
+
self._clauses.append([])
|
|
180
|
+
return
|
|
181
|
+
|
|
182
|
+
if n == 1:
|
|
183
|
+
self._encode_eq_const(variables[0], target)
|
|
184
|
+
return
|
|
185
|
+
|
|
186
|
+
if n == 2:
|
|
187
|
+
v1, v2 = variables
|
|
188
|
+
for val1 in range(v1.lb, v1.ub + 1):
|
|
189
|
+
val2 = target - val1
|
|
190
|
+
if val2 < v2.lb or val2 > v2.ub:
|
|
191
|
+
self._clauses.append([-v1.bool_vars[val1]])
|
|
192
|
+
else:
|
|
193
|
+
self._clauses.append([-v1.bool_vars[val1], v2.bool_vars[val2]])
|
|
194
|
+
return
|
|
195
|
+
|
|
196
|
+
aux = self.int_var(variables[0].lb + variables[1].lb,
|
|
197
|
+
variables[0].ub + variables[1].ub)
|
|
198
|
+
|
|
199
|
+
for v1 in range(variables[0].lb, variables[0].ub + 1):
|
|
200
|
+
for v2 in range(variables[1].lb, variables[1].ub + 1):
|
|
201
|
+
s = v1 + v2
|
|
202
|
+
if s in aux.bool_vars:
|
|
203
|
+
self._clauses.append([-variables[0].bool_vars[v1],
|
|
204
|
+
-variables[1].bool_vars[v2],
|
|
205
|
+
aux.bool_vars[s]])
|
|
206
|
+
|
|
207
|
+
self._encode_sum_eq([aux] + list(variables[2:]), target)
|
|
208
|
+
|
|
209
|
+
def _encode_constraint(self, constraint):
|
|
210
|
+
if isinstance(constraint, tuple):
|
|
211
|
+
kind = constraint[0]
|
|
212
|
+
|
|
213
|
+
if kind == 'all_different':
|
|
214
|
+
self._encode_all_different(constraint[1])
|
|
215
|
+
|
|
216
|
+
elif kind == 'eq_const':
|
|
217
|
+
self._encode_eq_const(constraint[1], constraint[2])
|
|
218
|
+
|
|
219
|
+
elif kind == 'ne_const':
|
|
220
|
+
self._encode_ne_const(constraint[1], constraint[2])
|
|
221
|
+
|
|
222
|
+
elif kind == 'eq_var':
|
|
223
|
+
self._encode_eq_var(constraint[1], constraint[2])
|
|
224
|
+
|
|
225
|
+
elif kind == 'ne_var':
|
|
226
|
+
self._encode_ne_var(constraint[1], constraint[2])
|
|
227
|
+
|
|
228
|
+
elif kind == 'add':
|
|
229
|
+
terms, const = self._flatten_sum(constraint)
|
|
230
|
+
if len(terms) == 0:
|
|
231
|
+
if const != 0:
|
|
232
|
+
self._clauses.append([])
|
|
233
|
+
else:
|
|
234
|
+
pass
|
|
235
|
+
|
|
236
|
+
elif kind == 'sum_eq':
|
|
237
|
+
self._encode_sum_eq(list(constraint[1]), constraint[2])
|
|
238
|
+
|
|
239
|
+
elif kind == 'sum_le':
|
|
240
|
+
terms, target = constraint[1], constraint[2]
|
|
241
|
+
aux = self.int_var(sum(v.lb for v in terms), min(sum(v.ub for v in terms), target))
|
|
242
|
+
self._encode_sum_eq(list(terms), aux)
|
|
243
|
+
|
|
244
|
+
elif kind == 'sum_ge':
|
|
245
|
+
terms, target = constraint[1], constraint[2]
|
|
246
|
+
aux = self.int_var(max(sum(v.lb for v in terms), target), sum(v.ub for v in terms))
|
|
247
|
+
self._encode_sum_eq(list(terms), aux)
|
|
248
|
+
|
|
249
|
+
def sum_eq(self, variables, target):
|
|
250
|
+
"""(vars, k) -> constraint that sum of vars equals k."""
|
|
251
|
+
return ('sum_eq', tuple(variables), target)
|
|
252
|
+
|
|
253
|
+
def sum_le(self, variables, target):
|
|
254
|
+
"""(vars, k) -> constraint that sum of vars <= k."""
|
|
255
|
+
return ('sum_le', tuple(variables), target)
|
|
256
|
+
|
|
257
|
+
def sum_ge(self, variables, target):
|
|
258
|
+
"""(vars, k) -> constraint that sum of vars >= k."""
|
|
259
|
+
return ('sum_ge', tuple(variables), target)
|
|
260
|
+
|
|
261
|
+
def solve(self, **kwargs):
|
|
262
|
+
"""() -> Result with solution or UNSAT."""
|
|
263
|
+
self._clauses = []
|
|
264
|
+
self._encode_vars()
|
|
265
|
+
|
|
266
|
+
for constraint in self._constraints:
|
|
267
|
+
self._encode_constraint(constraint)
|
|
268
|
+
|
|
269
|
+
if any(len(c) == 0 for c in self._clauses):
|
|
270
|
+
return Result(None, 0, 0, 0, Status.INFEASIBLE)
|
|
271
|
+
|
|
272
|
+
sat_result = solve_sat(self._clauses, **kwargs)
|
|
273
|
+
|
|
274
|
+
if sat_result.status == SATStatus.INFEASIBLE:
|
|
275
|
+
return Result(None, 0, sat_result.iterations, sat_result.evaluations, Status.INFEASIBLE)
|
|
276
|
+
|
|
277
|
+
if sat_result.status == SATStatus.MAX_ITER:
|
|
278
|
+
return Result(None, 0, sat_result.iterations, sat_result.evaluations, Status.MAX_ITER)
|
|
279
|
+
|
|
280
|
+
solution = {}
|
|
281
|
+
for name, var in self._vars.items():
|
|
282
|
+
if name.startswith('_'):
|
|
283
|
+
continue
|
|
284
|
+
for val, bool_var in var.bool_vars.items():
|
|
285
|
+
if sat_result.solution.get(bool_var, False):
|
|
286
|
+
solution[name] = val
|
|
287
|
+
break
|
|
288
|
+
|
|
289
|
+
return Result(solution, 0, sat_result.iterations, sat_result.evaluations, Status.OPTIMAL)
|