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.
@@ -16,7 +16,7 @@ uv sync --extra dev
16
16
  uv run pytest
17
17
 
18
18
  # Run linter
19
- uv run ruff check src/
19
+ uv run ruff check solvor/
20
20
  ```
21
21
 
22
22
  ## Code Style
@@ -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 [yyyy] [name of copyright owner]
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.2
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: MIT
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 :: MIT License
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.9; extra == 'dev'
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`, `Model` | Sudoku, configuration, puzzles |
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
- Two-phase simplex for linear programming.
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
- Branch and bound for mixed-integer problems.
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
- DPLL with unit propagation and clause learning.
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
- Constraint programming via SAT encoding.
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 - accepts worse solutions probabilistically.
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
- Maintains a "tabu list" of recent moves to escape local optima.
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
- Genetic algorithm with selection, crossover, mutation.
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
- First-order methods for differentiable functions.
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
- Gaussian process surrogate for expensive black-box functions.
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
- Ford-Fulkerson with BFS (Edmonds-Karp).
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
- Successive shortest paths for minimum cost flow.
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`, `Model` | Sudoku, configuration, puzzles |
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
- Two-phase simplex for linear programming.
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
- Branch and bound for mixed-integer problems.
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
- DPLL with unit propagation and clause learning.
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
- Constraint programming via SAT encoding.
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 - accepts worse solutions probabilistically.
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
- Maintains a "tabu list" of recent moves to escape local optima.
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
- Genetic algorithm with selection, crossover, mutation.
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
- First-order methods for differentiable functions.
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
- Gaussian process surrogate for expensive black-box functions.
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
- Ford-Fulkerson with BFS (Edmonds-Karp).
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
- Successive shortest paths for minimum cost flow.
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.2"
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 = "MIT" }
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 :: MIT License",
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.9",
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 - 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
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
- """(objective_fn, bounds, opts) -> Result with best solution found."""
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 - Constraint Programming via SAT Encoding
2
+ CP-SAT Solver, constraint programming with a SAT backend.
3
3
 
4
- Solves constraint satisfaction problems by encoding integer variables and
5
- constraints into SAT clauses, then solving with the SAT backend.
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
- 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)
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