better-optimize 0.2.1__tar.gz → 0.3.1__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.
Files changed (27) hide show
  1. {better_optimize-0.2.1 → better_optimize-0.3.1}/PKG-INFO +52 -1
  2. {better_optimize-0.2.1 → better_optimize-0.3.1}/README.md +48 -0
  3. {better_optimize-0.2.1 → better_optimize-0.3.1}/better_optimize/__init__.py +2 -1
  4. {better_optimize-0.2.1 → better_optimize-0.3.1}/better_optimize/_version.py +3 -3
  5. {better_optimize-0.2.1 → better_optimize-0.3.1}/better_optimize/basinhopping.py +6 -1
  6. {better_optimize-0.2.1 → better_optimize-0.3.1}/better_optimize/constants.py +1 -0
  7. better_optimize-0.3.1/better_optimize/multi_optimize.py +516 -0
  8. {better_optimize-0.2.1 → better_optimize-0.3.1}/better_optimize/root.py +4 -1
  9. {better_optimize-0.2.1 → better_optimize-0.3.1}/better_optimize/wrapper.py +51 -26
  10. {better_optimize-0.2.1 → better_optimize-0.3.1}/better_optimize.egg-info/PKG-INFO +52 -1
  11. {better_optimize-0.2.1 → better_optimize-0.3.1}/better_optimize.egg-info/SOURCES.txt +2 -0
  12. {better_optimize-0.2.1 → better_optimize-0.3.1}/better_optimize.egg-info/requires.txt +3 -0
  13. {better_optimize-0.2.1 → better_optimize-0.3.1}/pyproject.toml +3 -0
  14. better_optimize-0.3.1/tests/test_multistart.py +433 -0
  15. {better_optimize-0.2.1 → better_optimize-0.3.1}/LICENSE +0 -0
  16. {better_optimize-0.2.1 → better_optimize-0.3.1}/better_optimize/minimize.py +0 -0
  17. {better_optimize-0.2.1 → better_optimize-0.3.1}/better_optimize/utilities.py +0 -0
  18. {better_optimize-0.2.1 → better_optimize-0.3.1}/better_optimize.egg-info/dependency_links.txt +0 -0
  19. {better_optimize-0.2.1 → better_optimize-0.3.1}/better_optimize.egg-info/top_level.txt +0 -0
  20. {better_optimize-0.2.1 → better_optimize-0.3.1}/setup.cfg +0 -0
  21. {better_optimize-0.2.1 → better_optimize-0.3.1}/setup.py +0 -0
  22. {better_optimize-0.2.1 → better_optimize-0.3.1}/tests/test_basinhopping.py +0 -0
  23. {better_optimize-0.2.1 → better_optimize-0.3.1}/tests/test_jax_integration.py +0 -0
  24. {better_optimize-0.2.1 → better_optimize-0.3.1}/tests/test_minimize.py +0 -0
  25. {better_optimize-0.2.1 → better_optimize-0.3.1}/tests/test_root.py +0 -0
  26. {better_optimize-0.2.1 → better_optimize-0.3.1}/tests/test_utilities.py +0 -0
  27. {better_optimize-0.2.1 → better_optimize-0.3.1}/tests/test_wrapper.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: better_optimize
3
- Version: 0.2.1
3
+ Version: 0.3.1
4
4
  Summary: A drop-in replacement for scipy optimize functions with quality of life improvements
5
5
  License: MIT License
6
6
  Project-URL: Homepage, https://github.com/jessegrabowski/better_optimize
@@ -21,6 +21,9 @@ License-File: LICENSE
21
21
  Requires-Dist: numpy
22
22
  Requires-Dist: scipy>=1.15
23
23
  Requires-Dist: rich
24
+ Requires-Dist: threadpoolctl
25
+ Requires-Dist: joblib
26
+ Requires-Dist: pandas
24
27
  Provides-Extra: tests
25
28
  Requires-Dist: pytest; extra == "tests"
26
29
  Requires-Dist: pytest-cov; extra == "tests"
@@ -146,6 +149,54 @@ Many sub-computations are repeated between the objective, gradient, and hessian
146
149
  fused value_and_grad function, but `better_optimize` also lets you pass a triple-fused value_grad_and_hess function.
147
150
  This avoids redundant computation and speeds up the optimization process.
148
151
 
152
+ ### Parallel Optimization from Multiple Starting Points
153
+
154
+ Real-world objectives often have multiple local minima. A common workaround is to throw many random starting points at
155
+ the optimizer and keep the best result. `better_optimize` makes this painless with `multi_optimize`:
156
+
157
+ ```python
158
+ from better_optimize import minimize, multi_optimize
159
+
160
+ def rosenbrock(x):
161
+ return sum(100.0*(x[1:] - x[:-1]**2.0)**2.0 + (1 - x[:-1])**2.0)
162
+
163
+ result = multi_optimize(
164
+ solver=minimize,
165
+ solver_kwargs=dict(f=rosenbrock, method="L-BFGS-B", tol=1e-10),
166
+ x0=np.zeros(5),
167
+ n_runs=16,
168
+ init_strategy="uniform",
169
+ bounds=(-5, 5),
170
+ backend="loky",
171
+ n_jobs=-1,
172
+ seed=42,
173
+ progressbar=True,
174
+ )
175
+
176
+ print(result.best) # Best OptimizeResult
177
+ print(result.x_best) # Best parameter vector
178
+ print(result.fun_best) # Best objective value
179
+ result.summary() # Rich table of all runs, ranked
180
+ ```
181
+
182
+ `multi_optimize` works with **any** solver that follows the `(x0, **kwargs) → OptimizeResult` signature — that includes
183
+ `minimize`, `root`, `basinhopping`, or your own custom wrapper. It just calls `solver(x0=x0_i, **solver_kwargs)` for
184
+ each starting point; it never inspects the solver internals.
185
+
186
+ A few highlights:
187
+
188
+ - **Initialization strategies** — `"uniform"`, `"normal"`, `"sobol"`, `"lhs"`, or pass your own callable. Bounded
189
+ strategies (`uniform`, `sobol`, `lhs`) require a `bounds` argument; `"normal"` perturbs around `x0` with a
190
+ configurable `init_scale`. Or just pass an explicit `list[np.ndarray]` as `x0` and skip the generation entirely.
191
+ - **Parallel backends** — `"sequential"` (for debugging), `"loky"` (CPU-bound work, default), or `"threading"`
192
+ (GIL-releasing code). Under the hood this is `joblib`, so the usual `n_jobs=-1` convention works.
193
+ - **BLAS thread control** — When many workers each spawn a full BLAS/OpenMP thread pool, you get noisy-neighbor
194
+ over-subscription. The `blas_cores` argument (default `"auto"`) caps the total thread budget so workers don't
195
+ fight over the same cores.
196
+
197
+ The returned `MultiStartResult` gives you `best`, `top_k(k)`, `ranked()`, `success_rate`, a `summary()` table, and
198
+ `to_dataframe()` for further analysis.
199
+
149
200
 
150
201
  ## Contributing
151
202
 
@@ -117,6 +117,54 @@ Many sub-computations are repeated between the objective, gradient, and hessian
117
117
  fused value_and_grad function, but `better_optimize` also lets you pass a triple-fused value_grad_and_hess function.
118
118
  This avoids redundant computation and speeds up the optimization process.
119
119
 
120
+ ### Parallel Optimization from Multiple Starting Points
121
+
122
+ Real-world objectives often have multiple local minima. A common workaround is to throw many random starting points at
123
+ the optimizer and keep the best result. `better_optimize` makes this painless with `multi_optimize`:
124
+
125
+ ```python
126
+ from better_optimize import minimize, multi_optimize
127
+
128
+ def rosenbrock(x):
129
+ return sum(100.0*(x[1:] - x[:-1]**2.0)**2.0 + (1 - x[:-1])**2.0)
130
+
131
+ result = multi_optimize(
132
+ solver=minimize,
133
+ solver_kwargs=dict(f=rosenbrock, method="L-BFGS-B", tol=1e-10),
134
+ x0=np.zeros(5),
135
+ n_runs=16,
136
+ init_strategy="uniform",
137
+ bounds=(-5, 5),
138
+ backend="loky",
139
+ n_jobs=-1,
140
+ seed=42,
141
+ progressbar=True,
142
+ )
143
+
144
+ print(result.best) # Best OptimizeResult
145
+ print(result.x_best) # Best parameter vector
146
+ print(result.fun_best) # Best objective value
147
+ result.summary() # Rich table of all runs, ranked
148
+ ```
149
+
150
+ `multi_optimize` works with **any** solver that follows the `(x0, **kwargs) → OptimizeResult` signature — that includes
151
+ `minimize`, `root`, `basinhopping`, or your own custom wrapper. It just calls `solver(x0=x0_i, **solver_kwargs)` for
152
+ each starting point; it never inspects the solver internals.
153
+
154
+ A few highlights:
155
+
156
+ - **Initialization strategies** — `"uniform"`, `"normal"`, `"sobol"`, `"lhs"`, or pass your own callable. Bounded
157
+ strategies (`uniform`, `sobol`, `lhs`) require a `bounds` argument; `"normal"` perturbs around `x0` with a
158
+ configurable `init_scale`. Or just pass an explicit `list[np.ndarray]` as `x0` and skip the generation entirely.
159
+ - **Parallel backends** — `"sequential"` (for debugging), `"loky"` (CPU-bound work, default), or `"threading"`
160
+ (GIL-releasing code). Under the hood this is `joblib`, so the usual `n_jobs=-1` convention works.
161
+ - **BLAS thread control** — When many workers each spawn a full BLAS/OpenMP thread pool, you get noisy-neighbor
162
+ over-subscription. The `blas_cores` argument (default `"auto"`) caps the total thread budget so workers don't
163
+ fight over the same cores.
164
+
165
+ The returned `MultiStartResult` gives you `best`, `top_k(k)`, `ranked()`, `success_rate`, a `summary()` table, and
166
+ `to_dataframe()` for further analysis.
167
+
120
168
 
121
169
  ## Contributing
122
170
 
@@ -3,6 +3,7 @@ import logging
3
3
  from better_optimize._version import get_versions
4
4
  from better_optimize.basinhopping import basinhopping
5
5
  from better_optimize.minimize import minimize
6
+ from better_optimize.multi_optimize import multi_optimize
6
7
  from better_optimize.root import root
7
8
 
8
9
  _log = logging.getLogger(__name__)
@@ -15,4 +16,4 @@ if not logging.root.handlers:
15
16
 
16
17
 
17
18
  __version__ = get_versions()["version"]
18
- __all__ = ["minimize", "root", "basinhopping"]
19
+ __all__ = ["minimize", "root", "basinhopping", "multi_optimize"]
@@ -8,11 +8,11 @@ import json
8
8
 
9
9
  version_json = '''
10
10
  {
11
- "date": "2026-03-02T01:36:39+0100",
11
+ "date": "2026-03-20T00:48:11+0100",
12
12
  "dirty": false,
13
13
  "error": null,
14
- "full-revisionid": "14b8b75021dd22fa3d38135eee74b2ab95c30a06",
15
- "version": "0.2.1"
14
+ "full-revisionid": "0478ed4b401b74272d06dee5e904e77be2916235",
15
+ "version": "0.3.1"
16
16
  }
17
17
  ''' # END VERSION_JSON
18
18
 
@@ -21,6 +21,7 @@ from scipy.optimize._basinhopping import (
21
21
  check_random_state,
22
22
  )
23
23
 
24
+ from better_optimize.constants import CONSOLE_WIDTH
24
25
  from better_optimize.minimize import minimize
25
26
  from better_optimize.utilities import (
26
27
  LRUCache1,
@@ -60,7 +61,11 @@ def initialize_progress_bar(progressbar, use_jac=True, use_hess=False):
60
61
  ]
61
62
 
62
63
  return ToggleableProgress(
63
- *columns, expand=False, disable=not progressbar, console=Console(width=100), transient=True
64
+ *columns,
65
+ expand=False,
66
+ disable=not progressbar,
67
+ console=Console(width=CONSOLE_WIDTH),
68
+ transient=True,
64
69
  )
65
70
 
66
71
 
@@ -31,6 +31,7 @@ root_method = Literal[
31
31
  ]
32
32
 
33
33
  TOLERANCES = ["xtol", "ftol", "gtol", "fatol", "xatol"]
34
+ CONSOLE_WIDTH = 100
34
35
 
35
36
  MINIMIZE_MODE_KWARGS = {
36
37
  "nelder-mead": {