solvor 0.3.2__tar.gz → 0.3.3__tar.gz
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-0.3.2 → solvor-0.3.3}/CONTRIBUTING.md +1 -1
- {solvor-0.3.2 → solvor-0.3.3}/LICENSE +1 -1
- {solvor-0.3.2 → solvor-0.3.3}/PKG-INFO +38 -16
- {solvor-0.3.2 → solvor-0.3.3}/README.md +34 -12
- {solvor-0.3.2 → solvor-0.3.3}/pyproject.toml +4 -4
- {solvor-0.3.2 → solvor-0.3.3}/solvor/__init__.py +2 -0
- solvor-0.3.3/solvor/anneal.py +75 -0
- {solvor-0.3.2 → solvor-0.3.3}/solvor/bayesian.py +20 -30
- {solvor-0.3.2 → solvor-0.3.3}/solvor/cp.py +16 -28
- solvor-0.3.3/solvor/dlx.py +249 -0
- {solvor-0.3.2 → solvor-0.3.3}/solvor/flow.py +24 -26
- {solvor-0.3.2 → solvor-0.3.3}/solvor/genetic.py +27 -33
- {solvor-0.3.2 → solvor-0.3.3}/solvor/gradient.py +27 -30
- {solvor-0.3.2 → solvor-0.3.3}/solvor/milp.py +21 -28
- {solvor-0.3.2 → solvor-0.3.3}/solvor/sat.py +26 -30
- {solvor-0.3.2 → solvor-0.3.3}/solvor/simplex.py +17 -25
- {solvor-0.3.2 → solvor-0.3.3}/solvor/tabu.py +24 -28
- {solvor-0.3.2 → solvor-0.3.3}/tests/test_solvers.py +96 -1
- solvor-0.3.2/solvor/anneal.py +0 -80
- {solvor-0.3.2 → solvor-0.3.3}/.github/workflows/ci.yml +0 -0
- {solvor-0.3.2 → solvor-0.3.3}/.github/workflows/publish.yml +0 -0
- {solvor-0.3.2 → solvor-0.3.3}/.gitignore +0 -0
- {solvor-0.3.2 → solvor-0.3.3}/solvor/types.py +0 -0
- {solvor-0.3.2 → solvor-0.3.3}/tests/__init__.py +0 -0
|
@@ -186,7 +186,7 @@
|
|
|
186
186
|
same "printed page" as the copyright notice for easier
|
|
187
187
|
identification within third-party archives.
|
|
188
188
|
|
|
189
|
-
Copyright [
|
|
189
|
+
Copyright [2025] [S.T. Grond]
|
|
190
190
|
|
|
191
191
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
192
192
|
you may not use this file except in compliance with the License.
|
|
@@ -1,21 +1,21 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: solvor
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.3
|
|
4
4
|
Summary: Solvor your optimization needs.
|
|
5
5
|
Project-URL: Repository, https://github.com/StevenBtw/solvor
|
|
6
6
|
Author: Steven Bytheway
|
|
7
|
-
License:
|
|
7
|
+
License: Apache-2.0
|
|
8
8
|
License-File: LICENSE
|
|
9
9
|
Keywords: linear-programming,metaheuristics,operations-research,optimization,sat,solver
|
|
10
10
|
Classifier: Development Status :: 3 - Alpha
|
|
11
11
|
Classifier: Intended Audience :: Developers
|
|
12
|
-
Classifier: License :: OSI Approved ::
|
|
12
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
13
13
|
Classifier: Programming Language :: Python :: 3.14
|
|
14
14
|
Classifier: Topic :: Scientific/Engineering :: Mathematics
|
|
15
15
|
Requires-Python: >=3.14
|
|
16
16
|
Provides-Extra: dev
|
|
17
17
|
Requires-Dist: pytest>=9.0.2; extra == 'dev'
|
|
18
|
-
Requires-Dist: ruff>=0.14.
|
|
18
|
+
Requires-Dist: ruff>=0.14.10; extra == 'dev'
|
|
19
19
|
Description-Content-Type: text/markdown
|
|
20
20
|
|
|
21
21
|
# Solvor
|
|
@@ -30,12 +30,13 @@ Solvor your optimization needs..
|
|
|
30
30
|
| Category | Solvers | Use Case |
|
|
31
31
|
|----------|---------|----------|
|
|
32
32
|
| **Linear/Integer** | `solve_lp`, `solve_milp` | Resource allocation, scheduling |
|
|
33
|
-
| **Constraint** | `solve_sat
|
|
33
|
+
| **Constraint** | `solve_sat` | Sudoku, configuration, puzzles |
|
|
34
34
|
| **Local Search** | `anneal`, `tabu_search` | TSP, combinatorial optimization |
|
|
35
35
|
| **Population** | `evolve` | When you want nature to do the work |
|
|
36
36
|
| **Continuous** | `gradient_descent`, `momentum`, `adam` | ML, curve fitting |
|
|
37
37
|
| **Black-box** | `bayesian_opt` | Hyperparameter tuning, expensive functions |
|
|
38
38
|
| **Graph** | `max_flow`, `min_cost_flow`, `solve_assignment` | Matching, transportation |
|
|
39
|
+
| **Exact Cover** | `solve_exact_cover` | Sudoku, N-Queens, tiling puzzles |
|
|
39
40
|
|
|
40
41
|
---
|
|
41
42
|
|
|
@@ -75,7 +76,7 @@ print(result.solution) # {'x': 3, 'y': 7}
|
|
|
75
76
|
<summary><strong>Linear & Integer Programming</strong></summary>
|
|
76
77
|
|
|
77
78
|
### solve_lp
|
|
78
|
-
|
|
79
|
+
For resource allocation, blending, production planning. Finds the exact optimum for linear objectives with linear constraints.
|
|
79
80
|
|
|
80
81
|
```python
|
|
81
82
|
# minimize 2x + 3y subject to x + y >= 4, x <= 3
|
|
@@ -87,7 +88,7 @@ result = solve_lp(
|
|
|
87
88
|
```
|
|
88
89
|
|
|
89
90
|
### solve_milp
|
|
90
|
-
|
|
91
|
+
When some variables must be integers. Diet problems, scheduling with discrete slots, set covering.
|
|
91
92
|
|
|
92
93
|
```python
|
|
93
94
|
# same as above, but x must be integer
|
|
@@ -100,7 +101,7 @@ result = solve_milp(c=[2, 3], A=[[-1, -1], [1, 0]], b=[-4, 3], integers=[0])
|
|
|
100
101
|
<summary><strong>Constraint Programming</strong></summary>
|
|
101
102
|
|
|
102
103
|
### solve_sat
|
|
103
|
-
|
|
104
|
+
For "is this configuration valid?" problems. Dependencies, exclusions, implications - anything that boils down to boolean constraints.
|
|
104
105
|
|
|
105
106
|
```python
|
|
106
107
|
# (x1 OR x2) AND (NOT x1 OR x3) AND (NOT x2 OR NOT x3)
|
|
@@ -109,7 +110,7 @@ print(result.solution) # {1: True, 2: False, 3: True}
|
|
|
109
110
|
```
|
|
110
111
|
|
|
111
112
|
### Model (CP-SAT)
|
|
112
|
-
|
|
113
|
+
For puzzles and scheduling with "all different", arithmetic, and logical constraints. Sudoku, N-Queens, timetabling.
|
|
113
114
|
|
|
114
115
|
```python
|
|
115
116
|
m = Model()
|
|
@@ -128,7 +129,7 @@ result = m.solve()
|
|
|
128
129
|
<summary><strong>Metaheuristics</strong></summary>
|
|
129
130
|
|
|
130
131
|
### anneal
|
|
131
|
-
Simulated annealing
|
|
132
|
+
Simulated annealing, accepts worse solutions probabilistically.
|
|
132
133
|
|
|
133
134
|
```python
|
|
134
135
|
result = anneal(
|
|
@@ -141,7 +142,7 @@ result = anneal(
|
|
|
141
142
|
```
|
|
142
143
|
|
|
143
144
|
### tabu_search
|
|
144
|
-
|
|
145
|
+
Greedy local search with memory. Prevents cycling back to recent solutions, forcing exploration of new territory. More deterministic than anneal.
|
|
145
146
|
|
|
146
147
|
```python
|
|
147
148
|
result = tabu_search(
|
|
@@ -153,7 +154,7 @@ result = tabu_search(
|
|
|
153
154
|
```
|
|
154
155
|
|
|
155
156
|
### evolve
|
|
156
|
-
|
|
157
|
+
Population-based search. More overhead than anneal/tabu, but better diversity and parallelizable.
|
|
157
158
|
|
|
158
159
|
```python
|
|
159
160
|
result = evolve(
|
|
@@ -171,7 +172,7 @@ result = evolve(
|
|
|
171
172
|
<summary><strong>Continuous Optimization</strong></summary>
|
|
172
173
|
|
|
173
174
|
### gradient_descent / momentum / adam
|
|
174
|
-
|
|
175
|
+
Follow the slope downhill. Great for polishing solutions from other methods if your objective is differentiable. Adam adapts learning rates per parameter - usually the default choice.
|
|
175
176
|
|
|
176
177
|
```python
|
|
177
178
|
def grad_fn(x):
|
|
@@ -182,7 +183,7 @@ print(result.solution) # [~0, ~0]
|
|
|
182
183
|
```
|
|
183
184
|
|
|
184
185
|
### bayesian_opt
|
|
185
|
-
|
|
186
|
+
When each evaluation is expensive (think hyperparameter tuning, simulations). Builds a surrogate model to guess where to sample next instead of brute-forcing.
|
|
186
187
|
|
|
187
188
|
```python
|
|
188
189
|
def expensive_fn(x):
|
|
@@ -198,7 +199,7 @@ result = bayesian_opt(expensive_fn, bounds=[(0, 1), (0, 1)], max_iter=30)
|
|
|
198
199
|
<summary><strong>Network Flow</strong></summary>
|
|
199
200
|
|
|
200
201
|
### max_flow
|
|
201
|
-
|
|
202
|
+
"How much can I push through this network?" Assigning workers to tasks, finding bottlenecks. The max-flow min-cut theorem gives you bottleneck analysis for free.
|
|
202
203
|
|
|
203
204
|
```python
|
|
204
205
|
graph = {
|
|
@@ -213,7 +214,7 @@ print(result.solution) # edge flows dict
|
|
|
213
214
|
```
|
|
214
215
|
|
|
215
216
|
### min_cost_flow / solve_assignment
|
|
216
|
-
|
|
217
|
+
"What's the cheapest way to route X units?" Transportation, logistics, matching with costs.
|
|
217
218
|
|
|
218
219
|
```python
|
|
219
220
|
# Assignment problem: 3 workers, 3 tasks
|
|
@@ -229,6 +230,26 @@ result = solve_assignment(costs)
|
|
|
229
230
|
|
|
230
231
|
</details>
|
|
231
232
|
|
|
233
|
+
<details>
|
|
234
|
+
<summary><strong>Exact Cover</strong></summary>
|
|
235
|
+
|
|
236
|
+
### solve_exact_cover
|
|
237
|
+
For "place these pieces without overlap" or "fill this grid with exactly one of each" problems. Sudoku, pentomino tiling, scheduling where every slot must be filled exactly once.
|
|
238
|
+
|
|
239
|
+
```python
|
|
240
|
+
# Tiling problem: cover all columns with non-overlapping rows
|
|
241
|
+
matrix = [
|
|
242
|
+
[1, 1, 0, 0], # row 0 covers columns 0, 1
|
|
243
|
+
[0, 1, 1, 0], # row 1 covers columns 1, 2
|
|
244
|
+
[0, 0, 1, 1], # row 2 covers columns 2, 3
|
|
245
|
+
[1, 0, 0, 1], # row 3 covers columns 0, 3
|
|
246
|
+
]
|
|
247
|
+
result = solve_exact_cover(matrix)
|
|
248
|
+
# result.solution = (0, 2) or (1, 3) - rows that cover all columns exactly once
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
</details>
|
|
252
|
+
|
|
232
253
|
---
|
|
233
254
|
|
|
234
255
|
## Result Format
|
|
@@ -260,6 +281,7 @@ Result(
|
|
|
260
281
|
| Smooth, differentiable | `adam` |
|
|
261
282
|
| Expensive black-box | `bayesian_opt` |
|
|
262
283
|
| Assignment, matching, flow | `max_flow`, `solve_assignment` |
|
|
284
|
+
| Exact cover, tiling, N-Queens | `solve_exact_cover` |
|
|
263
285
|
|
|
264
286
|
---
|
|
265
287
|
|
|
@@ -10,12 +10,13 @@ Solvor your optimization needs..
|
|
|
10
10
|
| Category | Solvers | Use Case |
|
|
11
11
|
|----------|---------|----------|
|
|
12
12
|
| **Linear/Integer** | `solve_lp`, `solve_milp` | Resource allocation, scheduling |
|
|
13
|
-
| **Constraint** | `solve_sat
|
|
13
|
+
| **Constraint** | `solve_sat` | Sudoku, configuration, puzzles |
|
|
14
14
|
| **Local Search** | `anneal`, `tabu_search` | TSP, combinatorial optimization |
|
|
15
15
|
| **Population** | `evolve` | When you want nature to do the work |
|
|
16
16
|
| **Continuous** | `gradient_descent`, `momentum`, `adam` | ML, curve fitting |
|
|
17
17
|
| **Black-box** | `bayesian_opt` | Hyperparameter tuning, expensive functions |
|
|
18
18
|
| **Graph** | `max_flow`, `min_cost_flow`, `solve_assignment` | Matching, transportation |
|
|
19
|
+
| **Exact Cover** | `solve_exact_cover` | Sudoku, N-Queens, tiling puzzles |
|
|
19
20
|
|
|
20
21
|
---
|
|
21
22
|
|
|
@@ -55,7 +56,7 @@ print(result.solution) # {'x': 3, 'y': 7}
|
|
|
55
56
|
<summary><strong>Linear & Integer Programming</strong></summary>
|
|
56
57
|
|
|
57
58
|
### solve_lp
|
|
58
|
-
|
|
59
|
+
For resource allocation, blending, production planning. Finds the exact optimum for linear objectives with linear constraints.
|
|
59
60
|
|
|
60
61
|
```python
|
|
61
62
|
# minimize 2x + 3y subject to x + y >= 4, x <= 3
|
|
@@ -67,7 +68,7 @@ result = solve_lp(
|
|
|
67
68
|
```
|
|
68
69
|
|
|
69
70
|
### solve_milp
|
|
70
|
-
|
|
71
|
+
When some variables must be integers. Diet problems, scheduling with discrete slots, set covering.
|
|
71
72
|
|
|
72
73
|
```python
|
|
73
74
|
# same as above, but x must be integer
|
|
@@ -80,7 +81,7 @@ result = solve_milp(c=[2, 3], A=[[-1, -1], [1, 0]], b=[-4, 3], integers=[0])
|
|
|
80
81
|
<summary><strong>Constraint Programming</strong></summary>
|
|
81
82
|
|
|
82
83
|
### solve_sat
|
|
83
|
-
|
|
84
|
+
For "is this configuration valid?" problems. Dependencies, exclusions, implications - anything that boils down to boolean constraints.
|
|
84
85
|
|
|
85
86
|
```python
|
|
86
87
|
# (x1 OR x2) AND (NOT x1 OR x3) AND (NOT x2 OR NOT x3)
|
|
@@ -89,7 +90,7 @@ print(result.solution) # {1: True, 2: False, 3: True}
|
|
|
89
90
|
```
|
|
90
91
|
|
|
91
92
|
### Model (CP-SAT)
|
|
92
|
-
|
|
93
|
+
For puzzles and scheduling with "all different", arithmetic, and logical constraints. Sudoku, N-Queens, timetabling.
|
|
93
94
|
|
|
94
95
|
```python
|
|
95
96
|
m = Model()
|
|
@@ -108,7 +109,7 @@ result = m.solve()
|
|
|
108
109
|
<summary><strong>Metaheuristics</strong></summary>
|
|
109
110
|
|
|
110
111
|
### anneal
|
|
111
|
-
Simulated annealing
|
|
112
|
+
Simulated annealing, accepts worse solutions probabilistically.
|
|
112
113
|
|
|
113
114
|
```python
|
|
114
115
|
result = anneal(
|
|
@@ -121,7 +122,7 @@ result = anneal(
|
|
|
121
122
|
```
|
|
122
123
|
|
|
123
124
|
### tabu_search
|
|
124
|
-
|
|
125
|
+
Greedy local search with memory. Prevents cycling back to recent solutions, forcing exploration of new territory. More deterministic than anneal.
|
|
125
126
|
|
|
126
127
|
```python
|
|
127
128
|
result = tabu_search(
|
|
@@ -133,7 +134,7 @@ result = tabu_search(
|
|
|
133
134
|
```
|
|
134
135
|
|
|
135
136
|
### evolve
|
|
136
|
-
|
|
137
|
+
Population-based search. More overhead than anneal/tabu, but better diversity and parallelizable.
|
|
137
138
|
|
|
138
139
|
```python
|
|
139
140
|
result = evolve(
|
|
@@ -151,7 +152,7 @@ result = evolve(
|
|
|
151
152
|
<summary><strong>Continuous Optimization</strong></summary>
|
|
152
153
|
|
|
153
154
|
### gradient_descent / momentum / adam
|
|
154
|
-
|
|
155
|
+
Follow the slope downhill. Great for polishing solutions from other methods if your objective is differentiable. Adam adapts learning rates per parameter - usually the default choice.
|
|
155
156
|
|
|
156
157
|
```python
|
|
157
158
|
def grad_fn(x):
|
|
@@ -162,7 +163,7 @@ print(result.solution) # [~0, ~0]
|
|
|
162
163
|
```
|
|
163
164
|
|
|
164
165
|
### bayesian_opt
|
|
165
|
-
|
|
166
|
+
When each evaluation is expensive (think hyperparameter tuning, simulations). Builds a surrogate model to guess where to sample next instead of brute-forcing.
|
|
166
167
|
|
|
167
168
|
```python
|
|
168
169
|
def expensive_fn(x):
|
|
@@ -178,7 +179,7 @@ result = bayesian_opt(expensive_fn, bounds=[(0, 1), (0, 1)], max_iter=30)
|
|
|
178
179
|
<summary><strong>Network Flow</strong></summary>
|
|
179
180
|
|
|
180
181
|
### max_flow
|
|
181
|
-
|
|
182
|
+
"How much can I push through this network?" Assigning workers to tasks, finding bottlenecks. The max-flow min-cut theorem gives you bottleneck analysis for free.
|
|
182
183
|
|
|
183
184
|
```python
|
|
184
185
|
graph = {
|
|
@@ -193,7 +194,7 @@ print(result.solution) # edge flows dict
|
|
|
193
194
|
```
|
|
194
195
|
|
|
195
196
|
### min_cost_flow / solve_assignment
|
|
196
|
-
|
|
197
|
+
"What's the cheapest way to route X units?" Transportation, logistics, matching with costs.
|
|
197
198
|
|
|
198
199
|
```python
|
|
199
200
|
# Assignment problem: 3 workers, 3 tasks
|
|
@@ -209,6 +210,26 @@ result = solve_assignment(costs)
|
|
|
209
210
|
|
|
210
211
|
</details>
|
|
211
212
|
|
|
213
|
+
<details>
|
|
214
|
+
<summary><strong>Exact Cover</strong></summary>
|
|
215
|
+
|
|
216
|
+
### solve_exact_cover
|
|
217
|
+
For "place these pieces without overlap" or "fill this grid with exactly one of each" problems. Sudoku, pentomino tiling, scheduling where every slot must be filled exactly once.
|
|
218
|
+
|
|
219
|
+
```python
|
|
220
|
+
# Tiling problem: cover all columns with non-overlapping rows
|
|
221
|
+
matrix = [
|
|
222
|
+
[1, 1, 0, 0], # row 0 covers columns 0, 1
|
|
223
|
+
[0, 1, 1, 0], # row 1 covers columns 1, 2
|
|
224
|
+
[0, 0, 1, 1], # row 2 covers columns 2, 3
|
|
225
|
+
[1, 0, 0, 1], # row 3 covers columns 0, 3
|
|
226
|
+
]
|
|
227
|
+
result = solve_exact_cover(matrix)
|
|
228
|
+
# result.solution = (0, 2) or (1, 3) - rows that cover all columns exactly once
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
</details>
|
|
232
|
+
|
|
212
233
|
---
|
|
213
234
|
|
|
214
235
|
## Result Format
|
|
@@ -240,6 +261,7 @@ Result(
|
|
|
240
261
|
| Smooth, differentiable | `adam` |
|
|
241
262
|
| Expensive black-box | `bayesian_opt` |
|
|
242
263
|
| Assignment, matching, flow | `max_flow`, `solve_assignment` |
|
|
264
|
+
| Exact cover, tiling, N-Queens | `solve_exact_cover` |
|
|
243
265
|
|
|
244
266
|
---
|
|
245
267
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "solvor"
|
|
3
|
-
version = "0.3.
|
|
3
|
+
version = "0.3.3"
|
|
4
4
|
description = "Solvor your optimization needs."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.14"
|
|
@@ -8,12 +8,12 @@ dependencies = []
|
|
|
8
8
|
authors = [
|
|
9
9
|
{ name = "Steven Bytheway" }
|
|
10
10
|
]
|
|
11
|
-
license = { text = "
|
|
11
|
+
license = { text = "Apache-2.0" }
|
|
12
12
|
keywords = ["optimization", "solver", "linear-programming", "sat", "metaheuristics", "operations-research"]
|
|
13
13
|
classifiers = [
|
|
14
14
|
"Development Status :: 3 - Alpha",
|
|
15
15
|
"Intended Audience :: Developers",
|
|
16
|
-
"License :: OSI Approved ::
|
|
16
|
+
"License :: OSI Approved :: Apache Software License",
|
|
17
17
|
"Programming Language :: Python :: 3.14",
|
|
18
18
|
"Topic :: Scientific/Engineering :: Mathematics",
|
|
19
19
|
]
|
|
@@ -24,7 +24,7 @@ Repository = "https://github.com/StevenBtw/solvor"
|
|
|
24
24
|
[project.optional-dependencies]
|
|
25
25
|
dev = [
|
|
26
26
|
"pytest>=9.0.2",
|
|
27
|
-
"ruff>=0.14.
|
|
27
|
+
"ruff>=0.14.10",
|
|
28
28
|
]
|
|
29
29
|
|
|
30
30
|
[build-system]
|
|
@@ -11,11 +11,13 @@ from solvor.bayesian import bayesian_opt
|
|
|
11
11
|
from solvor.genetic import evolve
|
|
12
12
|
from solvor.flow import max_flow, min_cost_flow, solve_assignment
|
|
13
13
|
from solvor.gradient import gradient_descent, momentum, adam
|
|
14
|
+
from solvor.dlx import solve_exact_cover
|
|
14
15
|
|
|
15
16
|
__all__ = [
|
|
16
17
|
"solve_lp", "solve_milp", "tabu_search", "solve_tsp", "anneal",
|
|
17
18
|
"solve_sat", "Model", "bayesian_opt", "evolve",
|
|
18
19
|
"max_flow", "min_cost_flow", "solve_assignment",
|
|
19
20
|
"gradient_descent", "momentum", "adam",
|
|
21
|
+
"solve_exact_cover",
|
|
20
22
|
"Status", "Result",
|
|
21
23
|
]
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Simulated Annealing, a black box optimization that handles local optima well.
|
|
3
|
+
|
|
4
|
+
Use this when you have an objective function and can generate neighbors, but don't
|
|
5
|
+
know much else about the problem structure. Fast to prototype, low constraints on
|
|
6
|
+
problem formulation. Works well on landscapes with many local optima where gradient
|
|
7
|
+
descent would get stuck.
|
|
8
|
+
|
|
9
|
+
Don't use this for: problems where you need guarantees, or when you have constraints
|
|
10
|
+
that are easier to encode in MILP/CP. If you need more control over the search,
|
|
11
|
+
consider tabu (memory of visited states) or genetic (population-based exploration).
|
|
12
|
+
|
|
13
|
+
from solvor.anneal import anneal
|
|
14
|
+
|
|
15
|
+
result = anneal(start, cost_fn, neighbor_fn)
|
|
16
|
+
result = anneal(start, cost_fn, neighbor_fn, minimize=False) # maximize
|
|
17
|
+
|
|
18
|
+
The neighbor function is the key part, good neighbors make small moves, not random
|
|
19
|
+
jumps. Think "swap two cities" for TSP, not "shuffle everything". Small perturbations
|
|
20
|
+
let the algorithm actually explore the neighborhood instead of teleporting randomly.
|
|
21
|
+
|
|
22
|
+
If it's getting stuck: try higher starting temperature or slower cooling. If it's
|
|
23
|
+
taking forever: cool faster. For serious tuning, look into adaptive cooling schedules
|
|
24
|
+
or reheating strategies, this implementation keeps it simple with geometric cooling.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
from collections.abc import Callable
|
|
28
|
+
from math import exp
|
|
29
|
+
from random import random
|
|
30
|
+
from solvor.types import Status, Result
|
|
31
|
+
|
|
32
|
+
__all__ = ["anneal", "Status", "Result"]
|
|
33
|
+
|
|
34
|
+
def anneal[T](
|
|
35
|
+
initial: T,
|
|
36
|
+
objective_fn: Callable[[T], float],
|
|
37
|
+
neighbors: Callable[[T], T],
|
|
38
|
+
*,
|
|
39
|
+
minimize: bool = True,
|
|
40
|
+
temperature: float = 1000.0,
|
|
41
|
+
cooling: float = 0.9995,
|
|
42
|
+
min_temp: float = 1e-8,
|
|
43
|
+
max_iter: int = 100_000,
|
|
44
|
+
) -> Result:
|
|
45
|
+
|
|
46
|
+
sign = 1 if minimize else -1
|
|
47
|
+
evals = 0
|
|
48
|
+
|
|
49
|
+
def evaluate(sol):
|
|
50
|
+
nonlocal evals
|
|
51
|
+
evals += 1
|
|
52
|
+
return sign * objective_fn(sol)
|
|
53
|
+
|
|
54
|
+
solution, obj = initial, evaluate(initial)
|
|
55
|
+
best_solution, best_obj = solution, obj
|
|
56
|
+
|
|
57
|
+
for iteration in range(1, max_iter + 1):
|
|
58
|
+
if temperature < min_temp:
|
|
59
|
+
break
|
|
60
|
+
|
|
61
|
+
neighbor = neighbors(solution)
|
|
62
|
+
neighbor_obj = evaluate(neighbor)
|
|
63
|
+
delta = neighbor_obj - obj
|
|
64
|
+
|
|
65
|
+
if delta < 0 or random() < exp(-delta / temperature):
|
|
66
|
+
solution, obj = neighbor, neighbor_obj
|
|
67
|
+
|
|
68
|
+
if obj < best_obj:
|
|
69
|
+
best_solution, best_obj = solution, obj
|
|
70
|
+
|
|
71
|
+
temperature *= cooling
|
|
72
|
+
|
|
73
|
+
final_obj = best_obj * sign
|
|
74
|
+
status = Status.MAX_ITER if iteration == max_iter else Status.FEASIBLE
|
|
75
|
+
return Result(best_solution, final_obj, iteration, evals, status)
|
|
@@ -1,32 +1,22 @@
|
|
|
1
1
|
"""
|
|
2
|
-
Bayesian Optimization
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
|
2
|
+
Bayesian Optimization, for when each evaluation is expensive.
|
|
3
|
+
|
|
4
|
+
Use this for hyperparameter tuning, A/B testing, simulation optimization, or any
|
|
5
|
+
black-box problem where you can only afford 20-100 evaluations. Works well in
|
|
6
|
+
low dimensions (5-15 parameters), builds a surrogate model to guess where to
|
|
7
|
+
sample next instead of brute-forcing the space.
|
|
8
|
+
|
|
9
|
+
Don't use this for: functions that are cheap to evaluate (just use anneal or genetic with
|
|
10
|
+
more iterations), high-dimensional problems (>20 dims, the surrogate struggles),
|
|
11
|
+
or discrete/categorical parameters without encoding tricks.
|
|
12
|
+
|
|
13
|
+
from solvor.bayesian import bayesian_opt
|
|
14
|
+
|
|
15
|
+
result = bayesian_opt(cost_fn, bounds=[(0, 1), (-5, 5)])
|
|
16
|
+
result = bayesian_opt(cost_fn, bounds, minimize=False) # maximize
|
|
17
|
+
|
|
18
|
+
If you're doing serious ML hyperparameter tuning, consider scikit-optimize or
|
|
19
|
+
Optuna, they handle the edge cases and integrations this implementation doesn't.
|
|
30
20
|
"""
|
|
31
21
|
|
|
32
22
|
from collections.abc import Callable, Sequence
|
|
@@ -45,7 +35,7 @@ def bayesian_opt(
|
|
|
45
35
|
n_initial: int = 5,
|
|
46
36
|
seed: int | None = None,
|
|
47
37
|
) -> Result:
|
|
48
|
-
|
|
38
|
+
|
|
49
39
|
rng = Random(seed)
|
|
50
40
|
sign = 1 if minimize else -1
|
|
51
41
|
n_dims = len(bounds)
|
|
@@ -146,4 +136,4 @@ def _norm_pdf(x):
|
|
|
146
136
|
return exp(-0.5 * x * x) / sqrt(2 * pi)
|
|
147
137
|
|
|
148
138
|
def _norm_cdf(x):
|
|
149
|
-
return 0.5 * (1 + erf(x / sqrt(2)))
|
|
139
|
+
return 0.5 * (1 + erf(x / sqrt(2)))
|
|
@@ -1,32 +1,27 @@
|
|
|
1
1
|
"""
|
|
2
|
-
CP-SAT Solver
|
|
2
|
+
CP-SAT Solver, constraint programming with a SAT backend.
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
Use this for puzzles and scheduling with "all different", arithmetic constraints,
|
|
5
|
+
and logical combinations. Sudoku, N-Queens, nurse rostering, timetabling. The
|
|
6
|
+
solver encodes your integer variables and constraints into SAT clauses, then
|
|
7
|
+
hands it off to the SAT solver. You get the expressiveness of CP with the
|
|
8
|
+
raw solving power of modern SAT.
|
|
9
|
+
|
|
10
|
+
Don't use this for: optimization problems (use milp), pure linear problems
|
|
11
|
+
(simplex is simpler and faster), or trivially small problems where the encoding
|
|
12
|
+
overhead isn't worth it.
|
|
13
|
+
|
|
14
|
+
from solvor.cp import Model
|
|
6
15
|
|
|
7
|
-
Usage:
|
|
8
|
-
from solvor.cp import Model, Status
|
|
9
16
|
m = Model()
|
|
10
17
|
x = m.int_var(0, 9, 'x')
|
|
11
18
|
y = m.int_var(0, 9, 'y')
|
|
12
19
|
m.add(m.all_different([x, y]))
|
|
13
20
|
m.add(x + y == 10)
|
|
14
|
-
result = m.solve()
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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)
|
|
21
|
+
result = m.solve() # {'x': 3, 'y': 7} or similar
|
|
22
|
+
|
|
23
|
+
For problems that need both satisfaction and optimization, or heavier constraint
|
|
24
|
+
logic, z3 sits nicely between this CP approach and full MILP.
|
|
30
25
|
"""
|
|
31
26
|
|
|
32
27
|
from itertools import combinations
|
|
@@ -78,7 +73,6 @@ class Model:
|
|
|
78
73
|
return v
|
|
79
74
|
|
|
80
75
|
def int_var(self, lb, ub, name=None):
|
|
81
|
-
"""(lb, ub, name) -> IntVar with domain [lb, ub]."""
|
|
82
76
|
if name is None:
|
|
83
77
|
name = f"_v{len(self._vars)}"
|
|
84
78
|
var = IntVar(self, lb, ub, name)
|
|
@@ -86,11 +80,9 @@ class Model:
|
|
|
86
80
|
return var
|
|
87
81
|
|
|
88
82
|
def all_different(self, variables):
|
|
89
|
-
"""(vars) -> constraint that all variables take different values."""
|
|
90
83
|
return ('all_different', tuple(variables))
|
|
91
84
|
|
|
92
85
|
def add(self, constraint):
|
|
93
|
-
"""(constraint) -> add constraint to model."""
|
|
94
86
|
self._constraints.append(constraint)
|
|
95
87
|
|
|
96
88
|
def _encode_exactly_one(self, lits):
|
|
@@ -247,19 +239,15 @@ class Model:
|
|
|
247
239
|
self._encode_sum_eq(list(terms), aux)
|
|
248
240
|
|
|
249
241
|
def sum_eq(self, variables, target):
|
|
250
|
-
"""(vars, k) -> constraint that sum of vars equals k."""
|
|
251
242
|
return ('sum_eq', tuple(variables), target)
|
|
252
243
|
|
|
253
244
|
def sum_le(self, variables, target):
|
|
254
|
-
"""(vars, k) -> constraint that sum of vars <= k."""
|
|
255
245
|
return ('sum_le', tuple(variables), target)
|
|
256
246
|
|
|
257
247
|
def sum_ge(self, variables, target):
|
|
258
|
-
"""(vars, k) -> constraint that sum of vars >= k."""
|
|
259
248
|
return ('sum_ge', tuple(variables), target)
|
|
260
249
|
|
|
261
250
|
def solve(self, **kwargs):
|
|
262
|
-
"""() -> Result with solution or UNSAT."""
|
|
263
251
|
self._clauses = []
|
|
264
252
|
self._encode_vars()
|
|
265
253
|
|