better-optimize 0.3__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.
- {better_optimize-0.3 → better_optimize-0.3.1}/PKG-INFO +49 -1
- {better_optimize-0.3 → better_optimize-0.3.1}/README.md +48 -0
- {better_optimize-0.3 → better_optimize-0.3.1}/better_optimize/__init__.py +2 -1
- {better_optimize-0.3 → better_optimize-0.3.1}/better_optimize/_version.py +3 -3
- {better_optimize-0.3 → better_optimize-0.3.1}/better_optimize/constants.py +1 -1
- better_optimize-0.3/better_optimize/multistart.py → better_optimize-0.3.1/better_optimize/multi_optimize.py +129 -65
- {better_optimize-0.3 → better_optimize-0.3.1}/better_optimize/wrapper.py +50 -29
- {better_optimize-0.3 → better_optimize-0.3.1}/better_optimize.egg-info/PKG-INFO +49 -1
- {better_optimize-0.3 → better_optimize-0.3.1}/better_optimize.egg-info/SOURCES.txt +1 -1
- {better_optimize-0.3 → better_optimize-0.3.1}/tests/test_multistart.py +37 -131
- {better_optimize-0.3 → better_optimize-0.3.1}/LICENSE +0 -0
- {better_optimize-0.3 → better_optimize-0.3.1}/better_optimize/basinhopping.py +0 -0
- {better_optimize-0.3 → better_optimize-0.3.1}/better_optimize/minimize.py +0 -0
- {better_optimize-0.3 → better_optimize-0.3.1}/better_optimize/root.py +0 -0
- {better_optimize-0.3 → better_optimize-0.3.1}/better_optimize/utilities.py +0 -0
- {better_optimize-0.3 → better_optimize-0.3.1}/better_optimize.egg-info/dependency_links.txt +0 -0
- {better_optimize-0.3 → better_optimize-0.3.1}/better_optimize.egg-info/requires.txt +0 -0
- {better_optimize-0.3 → better_optimize-0.3.1}/better_optimize.egg-info/top_level.txt +0 -0
- {better_optimize-0.3 → better_optimize-0.3.1}/pyproject.toml +0 -0
- {better_optimize-0.3 → better_optimize-0.3.1}/setup.cfg +0 -0
- {better_optimize-0.3 → better_optimize-0.3.1}/setup.py +0 -0
- {better_optimize-0.3 → better_optimize-0.3.1}/tests/test_basinhopping.py +0 -0
- {better_optimize-0.3 → better_optimize-0.3.1}/tests/test_jax_integration.py +0 -0
- {better_optimize-0.3 → better_optimize-0.3.1}/tests/test_minimize.py +0 -0
- {better_optimize-0.3 → better_optimize-0.3.1}/tests/test_root.py +0 -0
- {better_optimize-0.3 → better_optimize-0.3.1}/tests/test_utilities.py +0 -0
- {better_optimize-0.3 → 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.3
|
|
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
|
|
@@ -149,6 +149,54 @@ Many sub-computations are repeated between the objective, gradient, and hessian
|
|
|
149
149
|
fused value_and_grad function, but `better_optimize` also lets you pass a triple-fused value_grad_and_hess function.
|
|
150
150
|
This avoids redundant computation and speeds up the optimization process.
|
|
151
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
|
+
|
|
152
200
|
|
|
153
201
|
## Contributing
|
|
154
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-
|
|
11
|
+
"date": "2026-03-20T00:48:11+0100",
|
|
12
12
|
"dirty": false,
|
|
13
13
|
"error": null,
|
|
14
|
-
"full-revisionid": "
|
|
15
|
-
"version": "0.3"
|
|
14
|
+
"full-revisionid": "0478ed4b401b74272d06dee5e904e77be2916235",
|
|
15
|
+
"version": "0.3.1"
|
|
16
16
|
}
|
|
17
17
|
''' # END VERSION_JSON
|
|
18
18
|
|
|
@@ -12,14 +12,15 @@ import numpy as np
|
|
|
12
12
|
import pandas as pd
|
|
13
13
|
|
|
14
14
|
from rich.console import Console
|
|
15
|
-
from rich.progress import
|
|
16
|
-
from rich.table import
|
|
15
|
+
from rich.progress import TaskID
|
|
16
|
+
from rich.table import Table
|
|
17
17
|
from scipy.optimize import OptimizeResult
|
|
18
18
|
from scipy.stats import qmc
|
|
19
19
|
from threadpoolctl import threadpool_limits
|
|
20
20
|
|
|
21
|
-
from better_optimize.constants import
|
|
21
|
+
from better_optimize.constants import MINIMIZE_MODE_KWARGS, ROOT_MODE_KWARGS
|
|
22
22
|
from better_optimize.utilities import ToggleableProgress
|
|
23
|
+
from better_optimize.wrapper import build_progress_bar
|
|
23
24
|
|
|
24
25
|
_log = logging.getLogger(__name__)
|
|
25
26
|
|
|
@@ -268,41 +269,8 @@ class MultiStartResult:
|
|
|
268
269
|
)
|
|
269
270
|
|
|
270
271
|
|
|
271
|
-
class
|
|
272
|
-
"""
|
|
273
|
-
|
|
274
|
-
Parameters
|
|
275
|
-
----------
|
|
276
|
-
solver
|
|
277
|
-
Any callable with signature ``(x0, **kwargs) → OptimizeResult``.
|
|
278
|
-
x0
|
|
279
|
-
A single starting point (used as center for ``init_strategy``) or an explicit
|
|
280
|
-
list of starting points (in which case ``n_runs`` and ``init_strategy`` are ignored).
|
|
281
|
-
solver_kwargs
|
|
282
|
-
Forwarded verbatim to ``solver`` on every call.
|
|
283
|
-
sort_key
|
|
284
|
-
Function mapping an ``OptimizeResult`` to a float used for ranking.
|
|
285
|
-
n_runs
|
|
286
|
-
Number of random restarts. Ignored when ``x0`` is a list.
|
|
287
|
-
init_strategy
|
|
288
|
-
How to generate starting points from ``x0``.
|
|
289
|
-
bounds
|
|
290
|
-
``(low, high)`` arrays or scalars, used by bounded init strategies.
|
|
291
|
-
init_scale
|
|
292
|
-
Standard deviation for ``"normal"`` init strategy.
|
|
293
|
-
backend
|
|
294
|
-
``"sequential"``, ``"loky"``, or ``"threading"``.
|
|
295
|
-
n_jobs
|
|
296
|
-
Number of parallel workers (``-1`` = all cores).
|
|
297
|
-
seed
|
|
298
|
-
Random seed for reproducible starting points.
|
|
299
|
-
progressbar
|
|
300
|
-
Whether to display per-run progress bars.
|
|
301
|
-
mp_ctx
|
|
302
|
-
Explicit ``multiprocessing`` context (``spawn`` / ``fork`` / ``forkserver``).
|
|
303
|
-
blas_cores
|
|
304
|
-
Total BLAS/OpenMP thread budget. ``"auto"`` = ``n_jobs``, ``None`` = unlimited.
|
|
305
|
-
"""
|
|
272
|
+
class _MultiStart:
|
|
273
|
+
"""Implementation behind :func:`multistart`. Not part of the public API."""
|
|
306
274
|
|
|
307
275
|
def __init__(
|
|
308
276
|
self,
|
|
@@ -384,7 +352,8 @@ class MultiStart:
|
|
|
384
352
|
progress = self._build_progress()
|
|
385
353
|
task_ids = self._register_tasks(progress)
|
|
386
354
|
|
|
387
|
-
|
|
355
|
+
manager = mp.Manager()
|
|
356
|
+
queue = manager.Queue()
|
|
388
357
|
stop = threading.Event()
|
|
389
358
|
listener = threading.Thread(
|
|
390
359
|
target=_drain_progress_queue,
|
|
@@ -394,20 +363,23 @@ class MultiStart:
|
|
|
394
363
|
|
|
395
364
|
with blas_config.limiter(), progress:
|
|
396
365
|
listener.start()
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
366
|
+
try:
|
|
367
|
+
results = Parallel(n_jobs=self._n_jobs, backend=self._backend)(
|
|
368
|
+
delayed(_run_single)(
|
|
369
|
+
run_index=i,
|
|
370
|
+
x0=x0_i,
|
|
371
|
+
solver=self._solver,
|
|
372
|
+
solver_kwargs=self._solver_kwargs,
|
|
373
|
+
progress=ProgressProxy(queue, task_id),
|
|
374
|
+
task_id=task_id,
|
|
375
|
+
blas_cores_per_worker=blas_config.per_worker,
|
|
376
|
+
)
|
|
377
|
+
for i, (x0_i, task_id) in enumerate(zip(self._starting_points, task_ids))
|
|
406
378
|
)
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
379
|
+
finally:
|
|
380
|
+
stop.set()
|
|
381
|
+
listener.join(timeout=1.0)
|
|
382
|
+
manager.shutdown()
|
|
411
383
|
|
|
412
384
|
return MultiStartResult(results=results, sort_key=self._sort_key)
|
|
413
385
|
|
|
@@ -422,31 +394,123 @@ class MultiStart:
|
|
|
422
394
|
name = getattr(self._solver, "__name__", "")
|
|
423
395
|
return self._SOLVER_LABELS.get(name, name.title() or "Optimizing")
|
|
424
396
|
|
|
397
|
+
@property
|
|
398
|
+
def _is_root(self) -> bool:
|
|
399
|
+
return getattr(self._solver, "__name__", "") == "root"
|
|
400
|
+
|
|
425
401
|
def _build_progress(self) -> ToggleableProgress:
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
402
|
+
method = self._solver_kwargs.get("method", "")
|
|
403
|
+
|
|
404
|
+
if self._is_root:
|
|
405
|
+
mode_info = ROOT_MODE_KWARGS.get(method, {})
|
|
406
|
+
use_jac = mode_info.get("uses_jac", False) or "jac" in self._solver_kwargs
|
|
407
|
+
use_hess = False
|
|
408
|
+
else:
|
|
409
|
+
mode_info = MINIMIZE_MODE_KWARGS.get(method, {})
|
|
410
|
+
use_jac = mode_info.get("uses_grad", False) or "jac" in self._solver_kwargs
|
|
411
|
+
use_hess = (
|
|
412
|
+
mode_info.get("uses_hess", False)
|
|
413
|
+
or "hess" in self._solver_kwargs
|
|
414
|
+
or "hessp" in self._solver_kwargs
|
|
415
|
+
)
|
|
416
|
+
|
|
417
|
+
return build_progress_bar(
|
|
418
|
+
description=self._task_description,
|
|
419
|
+
progressbar=self._progressbar,
|
|
420
|
+
root=self._is_root,
|
|
421
|
+
use_jac=use_jac,
|
|
422
|
+
use_hess=use_hess,
|
|
438
423
|
)
|
|
439
424
|
|
|
440
425
|
def _register_tasks(self, progress: ToggleableProgress) -> list[TaskID | None]:
|
|
426
|
+
# Include all possible fields so columns never hit a missing key.
|
|
427
|
+
# Unused fields (e.g. hess_norm when no hess column) are silently ignored.
|
|
441
428
|
return [
|
|
442
429
|
progress.add_task(
|
|
443
430
|
description=self._task_description,
|
|
444
431
|
total=None,
|
|
445
432
|
f_value=0.0,
|
|
446
433
|
grad_norm=0.0,
|
|
434
|
+
hess_norm=0.0,
|
|
447
435
|
)
|
|
448
436
|
for _ in self._starting_points
|
|
449
437
|
]
|
|
450
438
|
|
|
451
439
|
|
|
452
|
-
|
|
440
|
+
def multi_optimize(
|
|
441
|
+
solver: Callable[..., OptimizeResult],
|
|
442
|
+
x0: np.ndarray | list[np.ndarray],
|
|
443
|
+
solver_kwargs: dict | None = None,
|
|
444
|
+
sort_key: Callable[[OptimizeResult], float] | None = None,
|
|
445
|
+
n_runs: int = 16,
|
|
446
|
+
init_strategy: InitStrategy = "normal",
|
|
447
|
+
bounds: tuple[np.ndarray, np.ndarray] | None = None,
|
|
448
|
+
init_scale: float = 1.0,
|
|
449
|
+
backend: Literal["sequential", "loky", "threading"] = "loky",
|
|
450
|
+
n_jobs: int = -1,
|
|
451
|
+
seed: int | None = None,
|
|
452
|
+
progressbar: bool = True,
|
|
453
|
+
mp_ctx: mp.context.BaseContext | None = None,
|
|
454
|
+
blas_cores: int | None | Literal["auto"] = "auto",
|
|
455
|
+
) -> MultiStartResult:
|
|
456
|
+
"""
|
|
457
|
+
Run a solver from many starting points and return the collected results.
|
|
458
|
+
|
|
459
|
+
Parameters
|
|
460
|
+
----------
|
|
461
|
+
solver: Callable
|
|
462
|
+
Any solver with signature (x0, **kwargs) -> OptimizeResult, such as minimize or root
|
|
463
|
+
x0: np.ndarray or list of np.ndarray
|
|
464
|
+
A single starting point used as the center for init_strategy, or an explicit list of
|
|
465
|
+
starting points. When a list is provided, n_runs and init_strategy are ignored.
|
|
466
|
+
solver_kwargs: dict, optional
|
|
467
|
+
Keyword arguments forwarded verbatim to solver on every call
|
|
468
|
+
sort_key: Callable, optional
|
|
469
|
+
Function mapping an OptimizeResult to a float used for ranking. Defaults to
|
|
470
|
+
lambda res: res.fun, which ranks results by their optimized objective value.
|
|
471
|
+
n_runs: int
|
|
472
|
+
Number of random restarts. Ignored when x0 is a list.
|
|
473
|
+
init_strategy: str or Callable
|
|
474
|
+
How to generate starting points from x0. One of "uniform", "normal", "sobol", "lhs",
|
|
475
|
+
or a callable with signature (x0, n_runs, rng) → list of arrays.
|
|
476
|
+
bounds: tuple, optional
|
|
477
|
+
(low, high) arrays or scalars, required by bounded init strategies
|
|
478
|
+
init_scale: float
|
|
479
|
+
Standard deviation for the "normal" init strategy
|
|
480
|
+
backend: str
|
|
481
|
+
Execution backend. One of "sequential", "loky", or "threading".
|
|
482
|
+
n_jobs: int
|
|
483
|
+
Number of parallel workers. -1 means all cores.
|
|
484
|
+
seed: int, optional
|
|
485
|
+
Random seed for reproducible starting points
|
|
486
|
+
progressbar: bool
|
|
487
|
+
Whether to display per-run progress bars
|
|
488
|
+
mp_ctx: multiprocessing context, optional
|
|
489
|
+
Explicit multiprocessing context (spawn, fork, or forkserver)
|
|
490
|
+
blas_cores: int, str, or None
|
|
491
|
+
Total BLAS/OpenMP thread budget. "auto" splits evenly across workers, None is unlimited.
|
|
492
|
+
|
|
493
|
+
Returns
|
|
494
|
+
-------
|
|
495
|
+
result: MultiStartResult
|
|
496
|
+
Collected results with ranking, summary, and export utilities
|
|
497
|
+
"""
|
|
498
|
+
return _MultiStart(
|
|
499
|
+
solver=solver,
|
|
500
|
+
x0=x0,
|
|
501
|
+
solver_kwargs=solver_kwargs,
|
|
502
|
+
sort_key=sort_key,
|
|
503
|
+
n_runs=n_runs,
|
|
504
|
+
init_strategy=init_strategy,
|
|
505
|
+
bounds=bounds,
|
|
506
|
+
init_scale=init_scale,
|
|
507
|
+
backend=backend,
|
|
508
|
+
n_jobs=n_jobs,
|
|
509
|
+
seed=seed,
|
|
510
|
+
progressbar=progressbar,
|
|
511
|
+
mp_ctx=mp_ctx,
|
|
512
|
+
blas_cores=blas_cores,
|
|
513
|
+
).run()
|
|
514
|
+
|
|
515
|
+
|
|
516
|
+
__all__ = ["multi_optimize", "MultiStartResult", "generate_starts"]
|
|
@@ -24,6 +24,47 @@ from better_optimize.utilities import ToggleableProgress
|
|
|
24
24
|
_log = logging.getLogger(__name__)
|
|
25
25
|
|
|
26
26
|
|
|
27
|
+
def build_progress_bar(
|
|
28
|
+
*,
|
|
29
|
+
description: str = "Minimizing",
|
|
30
|
+
progressbar: bool = True,
|
|
31
|
+
root: bool = False,
|
|
32
|
+
use_jac: bool = False,
|
|
33
|
+
use_hess: bool = False,
|
|
34
|
+
) -> ToggleableProgress:
|
|
35
|
+
"""Create a ``ToggleableProgress`` bar with columns matching the solver configuration.
|
|
36
|
+
|
|
37
|
+
Used by both :class:`ObjectiveWrapper` (standalone) and :class:`MultiStart` (shared bar).
|
|
38
|
+
"""
|
|
39
|
+
bar_column = BarColumn(bar_width=None, table_column=Column(description, ratio=1))
|
|
40
|
+
time_column = TimeElapsedColumn(table_column=Column("Elapsed", ratio=1))
|
|
41
|
+
n_iters = MofNCompleteColumn(table_column=Column("Iteration"))
|
|
42
|
+
|
|
43
|
+
objective_name = "Objective" if not root else "Residual"
|
|
44
|
+
obj_column = TextColumn(
|
|
45
|
+
"{task.fields[f_value]:0.8f}", table_column=Column(objective_name, ratio=1)
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
columns = [bar_column, time_column, n_iters, obj_column]
|
|
49
|
+
|
|
50
|
+
if use_jac:
|
|
51
|
+
grad_name = "||grad||" if not root else "||jac||"
|
|
52
|
+
columns.append(
|
|
53
|
+
TextColumn("{task.fields[grad_norm]:0.8f}", table_column=Column(grad_name, ratio=1))
|
|
54
|
+
)
|
|
55
|
+
if use_hess:
|
|
56
|
+
columns.append(
|
|
57
|
+
TextColumn("{task.fields[hess_norm]:0.8f}", table_column=Column("||hess||", ratio=1))
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
return ToggleableProgress(
|
|
61
|
+
*columns,
|
|
62
|
+
expand=False,
|
|
63
|
+
disable=not progressbar,
|
|
64
|
+
console=Console(width=CONSOLE_WIDTH),
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
|
|
27
68
|
class ObjectiveWrapper:
|
|
28
69
|
def __init__(
|
|
29
70
|
self,
|
|
@@ -151,6 +192,9 @@ class ObjectiveWrapper:
|
|
|
151
192
|
if self.n_eval == 0 and self.task is None:
|
|
152
193
|
verb = "Minimizing" if not self.root else "Finding Roots"
|
|
153
194
|
self.task = self.progress.add_task(verb, total=self.maxeval, refresh=True, **value_dict)
|
|
195
|
+
elif self.n_eval == 0:
|
|
196
|
+
# Task was pre-registered (e.g. by MultiStart) without a known total.
|
|
197
|
+
self.progress.update(self.task, total=self.maxeval, **value_dict)
|
|
154
198
|
|
|
155
199
|
if not completed:
|
|
156
200
|
self.progress.update(self.task, advance=self.update_every, **value_dict)
|
|
@@ -160,36 +204,13 @@ class ObjectiveWrapper:
|
|
|
160
204
|
)
|
|
161
205
|
|
|
162
206
|
def initialize_progress_bar(self):
|
|
163
|
-
# text_column = TextColumn("{task.description}", table_column=Column(ratio=1))
|
|
164
207
|
description = "Minimizing" if not self.root else "Finding Roots"
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
"{task.fields[f_value]:0.8f}", table_column=Column(objective_name, ratio=1)
|
|
172
|
-
)
|
|
173
|
-
|
|
174
|
-
columns = [bar_column, time_column, n_iters, obj_column]
|
|
175
|
-
|
|
176
|
-
if self.use_jac:
|
|
177
|
-
grad_name = "||grad||" if not self.root else "||jac||"
|
|
178
|
-
columns += [
|
|
179
|
-
TextColumn("{task.fields[grad_norm]:0.8f}", table_column=Column(grad_name, ratio=1))
|
|
180
|
-
]
|
|
181
|
-
if self.use_hess:
|
|
182
|
-
columns += [
|
|
183
|
-
TextColumn(
|
|
184
|
-
"{task.fields[hess_norm]:0.8f}", table_column=Column("||hess||", ratio=1)
|
|
185
|
-
)
|
|
186
|
-
]
|
|
187
|
-
|
|
188
|
-
return ToggleableProgress(
|
|
189
|
-
*columns,
|
|
190
|
-
expand=False,
|
|
191
|
-
disable=not self.progressbar,
|
|
192
|
-
console=Console(width=CONSOLE_WIDTH),
|
|
208
|
+
return build_progress_bar(
|
|
209
|
+
description=description,
|
|
210
|
+
progressbar=self.progressbar,
|
|
211
|
+
root=self.root,
|
|
212
|
+
use_jac=self.use_jac,
|
|
213
|
+
use_hess=self.use_hess,
|
|
193
214
|
)
|
|
194
215
|
|
|
195
216
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: better_optimize
|
|
3
|
-
Version: 0.3
|
|
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
|
|
@@ -149,6 +149,54 @@ Many sub-computations are repeated between the objective, gradient, and hessian
|
|
|
149
149
|
fused value_and_grad function, but `better_optimize` also lets you pass a triple-fused value_grad_and_hess function.
|
|
150
150
|
This avoids redundant computation and speeds up the optimization process.
|
|
151
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
|
+
|
|
152
200
|
|
|
153
201
|
## Contributing
|
|
154
202
|
|
|
@@ -7,7 +7,7 @@ better_optimize/_version.py
|
|
|
7
7
|
better_optimize/basinhopping.py
|
|
8
8
|
better_optimize/constants.py
|
|
9
9
|
better_optimize/minimize.py
|
|
10
|
-
better_optimize/
|
|
10
|
+
better_optimize/multi_optimize.py
|
|
11
11
|
better_optimize/root.py
|
|
12
12
|
better_optimize/utilities.py
|
|
13
13
|
better_optimize/wrapper.py
|
|
@@ -13,14 +13,15 @@ from numpy.testing import assert_allclose
|
|
|
13
13
|
from scipy.optimize import OptimizeResult
|
|
14
14
|
|
|
15
15
|
from better_optimize.minimize import minimize
|
|
16
|
-
from better_optimize.
|
|
17
|
-
MultiStart,
|
|
16
|
+
from better_optimize.multi_optimize import (
|
|
18
17
|
MultiStartResult,
|
|
19
18
|
ProgressProxy,
|
|
20
19
|
_drain_progress_queue,
|
|
21
20
|
_is_pickle_error,
|
|
21
|
+
_MultiStart,
|
|
22
22
|
_run_single,
|
|
23
23
|
generate_starts,
|
|
24
|
+
multi_optimize,
|
|
24
25
|
setup_blas_cores,
|
|
25
26
|
)
|
|
26
27
|
from better_optimize.root import root
|
|
@@ -30,17 +31,6 @@ def rosenbrock(x):
|
|
|
30
31
|
return float(np.sum(100 * (x[1:] - x[:-1] ** 2) ** 2 + (1 - x[:-1]) ** 2))
|
|
31
32
|
|
|
32
33
|
|
|
33
|
-
def rosenbrock_fused(x):
|
|
34
|
-
f = float(np.sum(100 * (x[1:] - x[:-1] ** 2) ** 2 + (1 - x[:-1]) ** 2))
|
|
35
|
-
g = np.zeros_like(x)
|
|
36
|
-
g[0] = -400 * x[0] * (x[1] - x[0] ** 2) - 2 * (1 - x[0])
|
|
37
|
-
g[-1] = 200 * (x[-1] - x[-2] ** 2)
|
|
38
|
-
g[1:-1] = (
|
|
39
|
-
200 * (x[1:-1] - x[:-2] ** 2) - 400 * x[1:-1] * (x[2:] - x[1:-1] ** 2) - 2 * (1 - x[1:-1])
|
|
40
|
-
)
|
|
41
|
-
return f, g
|
|
42
|
-
|
|
43
|
-
|
|
44
34
|
def simple_system(x):
|
|
45
35
|
return np.array([x[0] ** 2 + x[1] - 1, x[0] - x[1] ** 2 + 1])
|
|
46
36
|
|
|
@@ -73,9 +63,14 @@ def test_normal_starts_centered_on_x0(rng):
|
|
|
73
63
|
|
|
74
64
|
|
|
75
65
|
@pytest.mark.parametrize("strategy", ["uniform", "sobol", "lhs"])
|
|
76
|
-
|
|
66
|
+
@pytest.mark.parametrize(
|
|
67
|
+
"bounds",
|
|
68
|
+
[(-5.0, 5.0), (np.array([-10.0, 0.0, -1.0]), np.array([0.0, 5.0, 1.0]))],
|
|
69
|
+
ids=["scalar", "array"],
|
|
70
|
+
)
|
|
71
|
+
def test_bounded_strategies_respect_bounds(strategy, bounds, rng):
|
|
77
72
|
n_params, n_runs = 3, 16
|
|
78
|
-
low, high =
|
|
73
|
+
low, high = bounds
|
|
79
74
|
starts = generate_starts(
|
|
80
75
|
np.zeros(n_params),
|
|
81
76
|
n_runs,
|
|
@@ -110,23 +105,6 @@ def test_lhs_stratification(rng):
|
|
|
110
105
|
assert len(set(bins)) == n_runs
|
|
111
106
|
|
|
112
107
|
|
|
113
|
-
def test_generate_starts_with_array_bounds(rng):
|
|
114
|
-
low = np.array([-10.0, 0.0, -1.0])
|
|
115
|
-
high = np.array([0.0, 5.0, 1.0])
|
|
116
|
-
starts = generate_starts(
|
|
117
|
-
np.zeros(3),
|
|
118
|
-
64,
|
|
119
|
-
"uniform",
|
|
120
|
-
bounds=(low, high),
|
|
121
|
-
init_scale=1.0,
|
|
122
|
-
rng=rng,
|
|
123
|
-
)
|
|
124
|
-
|
|
125
|
-
stacked = np.stack(starts)
|
|
126
|
-
assert np.all(stacked >= low)
|
|
127
|
-
assert np.all(stacked <= high)
|
|
128
|
-
|
|
129
|
-
|
|
130
108
|
def test_callable_strategy_dispatches_to_user_function(rng):
|
|
131
109
|
x0 = np.array([1.0, 2.0])
|
|
132
110
|
|
|
@@ -179,12 +157,6 @@ def test_progress_proxy_serializes_action_to_queue(action):
|
|
|
179
157
|
assert received_kwargs == payload
|
|
180
158
|
|
|
181
159
|
|
|
182
|
-
def test_progress_proxy_add_task_returns_stored_id():
|
|
183
|
-
queue = multiprocessing.Queue()
|
|
184
|
-
proxy = ProgressProxy(queue, task_id=99)
|
|
185
|
-
assert proxy.add_task("ignored", total=100) == 99
|
|
186
|
-
|
|
187
|
-
|
|
188
160
|
def test_drain_thread_exits_when_stop_is_set():
|
|
189
161
|
queue = multiprocessing.Queue()
|
|
190
162
|
stop = threading.Event()
|
|
@@ -223,14 +195,18 @@ def test_drain_thread_dispatches_queued_messages():
|
|
|
223
195
|
progress.reset.assert_called_once_with(1, visible=False)
|
|
224
196
|
|
|
225
197
|
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
198
|
+
@pytest.mark.parametrize(
|
|
199
|
+
"blas_cores, n_jobs, expected_per_worker",
|
|
200
|
+
[
|
|
201
|
+
("auto", 4, 1),
|
|
202
|
+
(8, 4, 2),
|
|
203
|
+
(1, 8, 1), # floors to 1
|
|
204
|
+
],
|
|
205
|
+
ids=["auto", "explicit", "floors-to-one"],
|
|
206
|
+
)
|
|
207
|
+
def test_setup_blas_cores_per_worker(blas_cores, n_jobs, expected_per_worker):
|
|
208
|
+
config = setup_blas_cores(blas_cores, n_jobs=n_jobs, mp_ctx=None)
|
|
209
|
+
assert config.per_worker == expected_per_worker
|
|
234
210
|
|
|
235
211
|
|
|
236
212
|
def test_setup_blas_cores_none_disables_limiting():
|
|
@@ -253,11 +229,6 @@ def test_setup_blas_cores_invalid_string_raises():
|
|
|
253
229
|
setup_blas_cores("bogus", n_jobs=4, mp_ctx=None)
|
|
254
230
|
|
|
255
231
|
|
|
256
|
-
def test_setup_blas_cores_per_worker_floors_to_one():
|
|
257
|
-
config = setup_blas_cores(1, n_jobs=8, mp_ctx=None)
|
|
258
|
-
assert config.per_worker == 1
|
|
259
|
-
|
|
260
|
-
|
|
261
232
|
def test_setup_blas_cores_negative_n_jobs():
|
|
262
233
|
config = setup_blas_cores("auto", n_jobs=-1, mp_ctx=None)
|
|
263
234
|
assert config.per_worker == 1
|
|
@@ -292,48 +263,6 @@ def test_run_single_attaches_metadata():
|
|
|
292
263
|
assert_allclose(result.x0, x0)
|
|
293
264
|
|
|
294
265
|
|
|
295
|
-
def test_run_single_forwards_solver_kwargs():
|
|
296
|
-
received = {}
|
|
297
|
-
|
|
298
|
-
def capturing_solver(x0, **kwargs):
|
|
299
|
-
received.update(kwargs)
|
|
300
|
-
return OptimizeResult(x=x0, fun=0.0, success=True)
|
|
301
|
-
|
|
302
|
-
_run_single(
|
|
303
|
-
run_index=0,
|
|
304
|
-
x0=np.array([1.0]),
|
|
305
|
-
solver=capturing_solver,
|
|
306
|
-
solver_kwargs={"method": "L-BFGS-B", "tol": 1e-8},
|
|
307
|
-
progress=MagicMock(),
|
|
308
|
-
task_id=0,
|
|
309
|
-
)
|
|
310
|
-
assert received["method"] == "L-BFGS-B"
|
|
311
|
-
assert received["tol"] == 1e-8
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
def test_run_single_applies_blas_thread_limit():
|
|
315
|
-
from threadpoolctl import threadpool_info
|
|
316
|
-
|
|
317
|
-
observed_limits = []
|
|
318
|
-
|
|
319
|
-
def inspecting_solver(x0, **kwargs):
|
|
320
|
-
for lib in threadpool_info():
|
|
321
|
-
observed_limits.append(lib["num_threads"])
|
|
322
|
-
return OptimizeResult(x=x0, fun=0.0, success=True)
|
|
323
|
-
|
|
324
|
-
_run_single(
|
|
325
|
-
run_index=0,
|
|
326
|
-
x0=np.array([1.0]),
|
|
327
|
-
solver=inspecting_solver,
|
|
328
|
-
solver_kwargs={},
|
|
329
|
-
progress=MagicMock(),
|
|
330
|
-
task_id=0,
|
|
331
|
-
blas_cores_per_worker=1,
|
|
332
|
-
)
|
|
333
|
-
if observed_limits:
|
|
334
|
-
assert all(n <= 1 for n in observed_limits)
|
|
335
|
-
|
|
336
|
-
|
|
337
266
|
def test_run_single_propagates_solver_exception():
|
|
338
267
|
def exploding_solver(x0, **kwargs):
|
|
339
268
|
raise RuntimeError("solver exploded")
|
|
@@ -420,18 +349,19 @@ class TestMultiStartResult:
|
|
|
420
349
|
)
|
|
421
350
|
def test_task_description_for_known_solvers(solver_name, expected_label):
|
|
422
351
|
stub = MagicMock(__name__=solver_name)
|
|
423
|
-
ms =
|
|
352
|
+
ms = _MultiStart(solver=stub, x0=[np.zeros(2)], progressbar=False)
|
|
424
353
|
assert ms._task_description == expected_label
|
|
425
354
|
|
|
426
355
|
|
|
427
356
|
def test_task_description_falls_back_for_unknown_solver():
|
|
428
357
|
stub = MagicMock(__name__="my_custom_opt")
|
|
429
|
-
ms =
|
|
358
|
+
ms = _MultiStart(solver=stub, x0=[np.zeros(2)], progressbar=False)
|
|
430
359
|
assert ms._task_description == "My_Custom_Opt"
|
|
431
360
|
|
|
432
361
|
|
|
433
|
-
|
|
434
|
-
|
|
362
|
+
@pytest.mark.parametrize("backend", ["sequential", "loky"])
|
|
363
|
+
def test_multistart_finds_rosenbrock_minimum(backend):
|
|
364
|
+
result = multi_optimize(
|
|
435
365
|
solver=minimize,
|
|
436
366
|
solver_kwargs=dict(f=rosenbrock, method="L-BFGS-B"),
|
|
437
367
|
x0=np.zeros(3),
|
|
@@ -439,8 +369,11 @@ def test_multistart_finds_rosenbrock_minimum():
|
|
|
439
369
|
init_strategy="uniform",
|
|
440
370
|
bounds=(-2, 2),
|
|
441
371
|
seed=7913,
|
|
372
|
+
backend=backend,
|
|
373
|
+
n_jobs=2,
|
|
442
374
|
progressbar=False,
|
|
443
|
-
|
|
375
|
+
blas_cores=None,
|
|
376
|
+
)
|
|
444
377
|
|
|
445
378
|
assert len(result.results) == 8
|
|
446
379
|
assert_allclose(result.x_best, np.ones(3), atol=1e-3)
|
|
@@ -449,12 +382,12 @@ def test_multistart_finds_rosenbrock_minimum():
|
|
|
449
382
|
|
|
450
383
|
def test_multistart_with_explicit_starting_points():
|
|
451
384
|
x0_list = [np.array([1.3, 0.7, 0.8]), np.array([0.5, 0.5, 0.5])]
|
|
452
|
-
result =
|
|
385
|
+
result = multi_optimize(
|
|
453
386
|
solver=minimize,
|
|
454
387
|
solver_kwargs=dict(f=rosenbrock, method="L-BFGS-B"),
|
|
455
388
|
x0=x0_list,
|
|
456
389
|
progressbar=False,
|
|
457
|
-
)
|
|
390
|
+
)
|
|
458
391
|
|
|
459
392
|
assert len(result.results) == 2
|
|
460
393
|
for result_i, expected_x0 in zip(result.results, x0_list):
|
|
@@ -462,7 +395,7 @@ def test_multistart_with_explicit_starting_points():
|
|
|
462
395
|
|
|
463
396
|
|
|
464
397
|
def test_multistart_with_root_solver():
|
|
465
|
-
result =
|
|
398
|
+
result = multi_optimize(
|
|
466
399
|
solver=root,
|
|
467
400
|
solver_kwargs=dict(f=simple_system, method="hybr"),
|
|
468
401
|
sort_key=lambda res: float(np.linalg.norm(res.fun)),
|
|
@@ -472,38 +405,11 @@ def test_multistart_with_root_solver():
|
|
|
472
405
|
init_scale=2.0,
|
|
473
406
|
seed=9173,
|
|
474
407
|
progressbar=False,
|
|
475
|
-
)
|
|
408
|
+
)
|
|
476
409
|
|
|
477
410
|
assert result.fun_best < 1e-6
|
|
478
411
|
|
|
479
412
|
|
|
480
|
-
def test_multistart_x0_list_ignores_n_runs():
|
|
481
|
-
result = MultiStart(
|
|
482
|
-
solver=minimize,
|
|
483
|
-
solver_kwargs=dict(f=rosenbrock, method="nelder-mead"),
|
|
484
|
-
x0=[np.zeros(2), np.ones(2)],
|
|
485
|
-
n_runs=999,
|
|
486
|
-
progressbar=False,
|
|
487
|
-
).run()
|
|
488
|
-
|
|
489
|
-
assert len(result.results) == 2
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
def test_multistart_with_fused_objective():
|
|
493
|
-
result = MultiStart(
|
|
494
|
-
solver=minimize,
|
|
495
|
-
solver_kwargs=dict(f=rosenbrock_fused, method="L-BFGS-B"),
|
|
496
|
-
x0=np.zeros(3),
|
|
497
|
-
n_runs=4,
|
|
498
|
-
init_strategy="uniform",
|
|
499
|
-
bounds=(-2, 2),
|
|
500
|
-
seed=3917,
|
|
501
|
-
progressbar=False,
|
|
502
|
-
).run()
|
|
503
|
-
|
|
504
|
-
assert_allclose(result.x_best, np.ones(3), atol=1e-3)
|
|
505
|
-
|
|
506
|
-
|
|
507
413
|
def test_multistart_pickle_fallback(caplog):
|
|
508
414
|
def unpicklable_closure():
|
|
509
415
|
return None
|
|
@@ -513,14 +419,14 @@ def test_multistart_pickle_fallback(caplog):
|
|
|
513
419
|
return OptimizeResult(x=x0, fun=float(np.sum(x0**2)), success=True)
|
|
514
420
|
|
|
515
421
|
with caplog.at_level("WARNING", logger="better_optimize.multistart"):
|
|
516
|
-
result =
|
|
422
|
+
result = multi_optimize(
|
|
517
423
|
solver=solver_with_closure,
|
|
518
424
|
x0=[np.zeros(2), np.ones(2)],
|
|
519
425
|
backend="loky",
|
|
520
426
|
n_jobs=2,
|
|
521
427
|
progressbar=False,
|
|
522
428
|
blas_cores=None,
|
|
523
|
-
)
|
|
429
|
+
)
|
|
524
430
|
|
|
525
431
|
# Fallback may or may not trigger depending on environment,
|
|
526
432
|
# but either way we must get valid results.
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|