better-optimize 0.3.1__tar.gz → 0.4__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.1 → better_optimize-0.4}/PKG-INFO +2 -2
- {better_optimize-0.3.1 → better_optimize-0.4}/better_optimize/__init__.py +10 -1
- {better_optimize-0.3.1 → better_optimize-0.4}/better_optimize/_version.py +3 -3
- better_optimize-0.4/better_optimize/basinhopping/__init__.py +11 -0
- better_optimize-0.3.1/better_optimize/basinhopping.py → better_optimize-0.4/better_optimize/basinhopping/core.py +2 -79
- better_optimize-0.4/better_optimize/basinhopping/progress.py +53 -0
- better_optimize-0.4/better_optimize/basinhopping/runner.py +33 -0
- {better_optimize-0.3.1 → better_optimize-0.4}/better_optimize/constants.py +17 -0
- better_optimize-0.4/better_optimize/differential_evolution.py +245 -0
- {better_optimize-0.3.1 → better_optimize-0.4}/better_optimize/minimize.py +1 -0
- {better_optimize-0.3.1 → better_optimize-0.4}/better_optimize/multi_optimize.py +24 -12
- better_optimize-0.4/better_optimize/sequential_optimize.py +477 -0
- {better_optimize-0.3.1 → better_optimize-0.4}/better_optimize/utilities.py +16 -4
- {better_optimize-0.3.1 → better_optimize-0.4}/better_optimize/wrapper.py +87 -34
- {better_optimize-0.3.1 → better_optimize-0.4}/better_optimize.egg-info/PKG-INFO +2 -2
- {better_optimize-0.3.1 → better_optimize-0.4}/better_optimize.egg-info/SOURCES.txt +8 -1
- {better_optimize-0.3.1 → better_optimize-0.4}/pyproject.toml +2 -2
- {better_optimize-0.3.1 → better_optimize-0.4}/tests/test_basinhopping.py +5 -6
- better_optimize-0.4/tests/test_differential_evolution.py +232 -0
- {better_optimize-0.3.1 → better_optimize-0.4}/tests/test_multistart.py +51 -12
- better_optimize-0.4/tests/test_sequential_optimize.py +444 -0
- {better_optimize-0.3.1 → better_optimize-0.4}/tests/test_wrapper.py +5 -4
- {better_optimize-0.3.1 → better_optimize-0.4}/LICENSE +0 -0
- {better_optimize-0.3.1 → better_optimize-0.4}/README.md +0 -0
- {better_optimize-0.3.1 → better_optimize-0.4}/better_optimize/root.py +0 -0
- {better_optimize-0.3.1 → better_optimize-0.4}/better_optimize.egg-info/dependency_links.txt +0 -0
- {better_optimize-0.3.1 → better_optimize-0.4}/better_optimize.egg-info/requires.txt +0 -0
- {better_optimize-0.3.1 → better_optimize-0.4}/better_optimize.egg-info/top_level.txt +0 -0
- {better_optimize-0.3.1 → better_optimize-0.4}/setup.cfg +0 -0
- {better_optimize-0.3.1 → better_optimize-0.4}/setup.py +0 -0
- {better_optimize-0.3.1 → better_optimize-0.4}/tests/test_jax_integration.py +0 -0
- {better_optimize-0.3.1 → better_optimize-0.4}/tests/test_minimize.py +0 -0
- {better_optimize-0.3.1 → better_optimize-0.4}/tests/test_root.py +0 -0
- {better_optimize-0.3.1 → better_optimize-0.4}/tests/test_utilities.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: better_optimize
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4
|
|
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
|
|
@@ -15,7 +15,7 @@ Classifier: Programming Language :: Python :: 3.12
|
|
|
15
15
|
Classifier: Programming Language :: Python :: 3.13
|
|
16
16
|
Classifier: Topic :: Scientific/Engineering
|
|
17
17
|
Classifier: Topic :: Scientific/Engineering :: Mathematics
|
|
18
|
-
Requires-Python: >=3.
|
|
18
|
+
Requires-Python: >=3.12
|
|
19
19
|
Description-Content-Type: text/markdown
|
|
20
20
|
License-File: LICENSE
|
|
21
21
|
Requires-Dist: numpy
|
|
@@ -2,9 +2,11 @@ import logging
|
|
|
2
2
|
|
|
3
3
|
from better_optimize._version import get_versions
|
|
4
4
|
from better_optimize.basinhopping import basinhopping
|
|
5
|
+
from better_optimize.differential_evolution import differential_evolution
|
|
5
6
|
from better_optimize.minimize import minimize
|
|
6
7
|
from better_optimize.multi_optimize import multi_optimize
|
|
7
8
|
from better_optimize.root import root
|
|
9
|
+
from better_optimize.sequential_optimize import sequential_optimize
|
|
8
10
|
|
|
9
11
|
_log = logging.getLogger(__name__)
|
|
10
12
|
|
|
@@ -16,4 +18,11 @@ if not logging.root.handlers:
|
|
|
16
18
|
|
|
17
19
|
|
|
18
20
|
__version__ = get_versions()["version"]
|
|
19
|
-
__all__ = [
|
|
21
|
+
__all__ = [
|
|
22
|
+
"basinhopping",
|
|
23
|
+
"differential_evolution",
|
|
24
|
+
"minimize",
|
|
25
|
+
"multi_optimize",
|
|
26
|
+
"root",
|
|
27
|
+
"sequential_optimize",
|
|
28
|
+
]
|
|
@@ -8,11 +8,11 @@ import json
|
|
|
8
8
|
|
|
9
9
|
version_json = '''
|
|
10
10
|
{
|
|
11
|
-
"date": "2026-
|
|
11
|
+
"date": "2026-04-21T12:33:06-0500",
|
|
12
12
|
"dirty": false,
|
|
13
13
|
"error": null,
|
|
14
|
-
"full-revisionid": "
|
|
15
|
-
"version": "0.
|
|
14
|
+
"full-revisionid": "f195a9db17359043c86b3be5f33a34494f635e58",
|
|
15
|
+
"version": "0.4"
|
|
16
16
|
}
|
|
17
17
|
''' # END VERSION_JSON
|
|
18
18
|
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
from better_optimize.basinhopping.core import basinhopping
|
|
2
|
+
from better_optimize.basinhopping.runner import (
|
|
3
|
+
AllowFailureBasinHoppingRunner,
|
|
4
|
+
AllowFailureStorage,
|
|
5
|
+
)
|
|
6
|
+
|
|
7
|
+
__all__ = [
|
|
8
|
+
"AllowFailureBasinHoppingRunner",
|
|
9
|
+
"AllowFailureStorage",
|
|
10
|
+
"basinhopping",
|
|
11
|
+
]
|
|
@@ -2,102 +2,26 @@ from collections.abc import Callable, Sequence
|
|
|
2
2
|
|
|
3
3
|
import numpy as np
|
|
4
4
|
|
|
5
|
-
from rich.console import Console
|
|
6
|
-
from rich.progress import (
|
|
7
|
-
BarColumn,
|
|
8
|
-
MofNCompleteColumn,
|
|
9
|
-
TextColumn,
|
|
10
|
-
TimeElapsedColumn,
|
|
11
|
-
)
|
|
12
|
-
from rich.table import Column
|
|
13
5
|
from scipy.optimize import OptimizeResult
|
|
14
6
|
from scipy.optimize._basinhopping import (
|
|
15
7
|
AdaptiveStepsize,
|
|
16
|
-
BasinHoppingRunner,
|
|
17
8
|
Metropolis,
|
|
18
9
|
MinimizerWrapper,
|
|
19
10
|
RandomDisplacement,
|
|
20
|
-
Storage,
|
|
21
11
|
check_random_state,
|
|
22
12
|
)
|
|
23
13
|
|
|
24
|
-
from better_optimize.
|
|
14
|
+
from better_optimize.basinhopping.progress import initialize_progress_bar
|
|
15
|
+
from better_optimize.basinhopping.runner import AllowFailureBasinHoppingRunner
|
|
25
16
|
from better_optimize.minimize import minimize
|
|
26
17
|
from better_optimize.utilities import (
|
|
27
18
|
LRUCache1,
|
|
28
|
-
ToggleableProgress,
|
|
29
19
|
check_f_is_fused_minimize,
|
|
30
20
|
determine_maxiter,
|
|
31
21
|
validate_provided_functions_minimize,
|
|
32
22
|
)
|
|
33
23
|
|
|
34
24
|
|
|
35
|
-
def initialize_progress_bar(progressbar, use_jac=True, use_hess=False):
|
|
36
|
-
description = "Process"
|
|
37
|
-
name_column = TextColumn("{task.fields[name]}", table_column=Column(description, ratio=2))
|
|
38
|
-
bar_column = BarColumn(bar_width=None, table_column=Column("", ratio=2))
|
|
39
|
-
time_column = TimeElapsedColumn(table_column=Column("Elapsed", ratio=1))
|
|
40
|
-
n_iters = MofNCompleteColumn(table_column=Column("Iteration"))
|
|
41
|
-
|
|
42
|
-
step_size = TextColumn("{task.fields[step_size]:0.3f}", table_column=Column("Step", ratio=1))
|
|
43
|
-
accept_rate = TextColumn(
|
|
44
|
-
"{task.fields[accept_rate]:0.3f}", table_column=Column("Accept Rate", ratio=1)
|
|
45
|
-
)
|
|
46
|
-
|
|
47
|
-
objective_name = "Objective"
|
|
48
|
-
obj_column = TextColumn(
|
|
49
|
-
"{task.fields[f_value]:0.5f}", table_column=Column(objective_name, ratio=1)
|
|
50
|
-
)
|
|
51
|
-
|
|
52
|
-
columns = [name_column, bar_column, time_column, n_iters, step_size, accept_rate, obj_column]
|
|
53
|
-
|
|
54
|
-
if use_jac:
|
|
55
|
-
columns += [
|
|
56
|
-
TextColumn("{task.fields[grad_norm]:0.5f}", table_column=Column("||grad||", ratio=1))
|
|
57
|
-
]
|
|
58
|
-
if use_hess:
|
|
59
|
-
columns += [
|
|
60
|
-
TextColumn("{task.fields[hess_norm]:0.8f}", table_column=Column("||hess||", ratio=1))
|
|
61
|
-
]
|
|
62
|
-
|
|
63
|
-
return ToggleableProgress(
|
|
64
|
-
*columns,
|
|
65
|
-
expand=False,
|
|
66
|
-
disable=not progressbar,
|
|
67
|
-
console=Console(width=CONSOLE_WIDTH),
|
|
68
|
-
transient=True,
|
|
69
|
-
)
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
class AllowFailureStorage(Storage):
|
|
73
|
-
"""
|
|
74
|
-
Subclass of Storage that allows the minimizer to fail, but still updates the global minimum
|
|
75
|
-
if the new point is better than the current global minimum.
|
|
76
|
-
"""
|
|
77
|
-
|
|
78
|
-
def update(self, minres):
|
|
79
|
-
if minres.fun < self.minres.fun:
|
|
80
|
-
self._add(minres)
|
|
81
|
-
return True
|
|
82
|
-
else:
|
|
83
|
-
return False
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
class AllowFailureBasinHoppingRunner(BasinHoppingRunner):
|
|
87
|
-
"""
|
|
88
|
-
A subclass of BasinHoppingRunner that allows the minimizer to fail, but still updates the
|
|
89
|
-
global minimum if the new point is better than the current global minimum.
|
|
90
|
-
"""
|
|
91
|
-
|
|
92
|
-
def __init__(
|
|
93
|
-
self, x0, minimizer, step_taking, accept_tests, accept_on_minimizer_fail=False, disp=False
|
|
94
|
-
):
|
|
95
|
-
super().__init__(x0, minimizer, step_taking, accept_tests, disp=disp)
|
|
96
|
-
|
|
97
|
-
if accept_on_minimizer_fail:
|
|
98
|
-
self.storage = AllowFailureStorage(self.storage.minres)
|
|
99
|
-
|
|
100
|
-
|
|
101
25
|
def basinhopping(
|
|
102
26
|
func: Callable[..., float | np.ndarray],
|
|
103
27
|
x0: Sequence[float],
|
|
@@ -279,7 +203,6 @@ def basinhopping(
|
|
|
279
203
|
else:
|
|
280
204
|
take_step_wrapped = take_step
|
|
281
205
|
else:
|
|
282
|
-
# use default
|
|
283
206
|
displace = RandomDisplacement(stepsize=stepsize, rng=rng)
|
|
284
207
|
take_step_wrapped = AdaptiveStepsize(
|
|
285
208
|
displace,
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
|
|
3
|
+
from rich.console import Console
|
|
4
|
+
from rich.progress import (
|
|
5
|
+
BarColumn,
|
|
6
|
+
MofNCompleteColumn,
|
|
7
|
+
TextColumn,
|
|
8
|
+
TimeElapsedColumn,
|
|
9
|
+
)
|
|
10
|
+
from rich.table import Column
|
|
11
|
+
|
|
12
|
+
from better_optimize.constants import CONSOLE_WIDTH
|
|
13
|
+
from better_optimize.utilities import ToggleableProgress
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def initialize_progress_bar(progressbar, use_jac=True, use_hess=False):
|
|
17
|
+
description = "Process"
|
|
18
|
+
name_column = TextColumn("{task.fields[name]}", table_column=Column(description, ratio=2))
|
|
19
|
+
bar_column = BarColumn(bar_width=None, table_column=Column("", ratio=2))
|
|
20
|
+
time_column = TimeElapsedColumn(table_column=Column("Elapsed", ratio=1))
|
|
21
|
+
n_iters = MofNCompleteColumn(table_column=Column("Iteration"))
|
|
22
|
+
|
|
23
|
+
step_size = TextColumn("{task.fields[step_size]:0.3f}", table_column=Column("Step", ratio=1))
|
|
24
|
+
accept_rate = TextColumn(
|
|
25
|
+
"{task.fields[accept_rate]:0.3f}", table_column=Column("Accept Rate", ratio=1)
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
objective_name = "Objective"
|
|
29
|
+
obj_column = TextColumn(
|
|
30
|
+
"{task.fields[f_value]:0.5f}", table_column=Column(objective_name, ratio=1)
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
columns = [name_column, bar_column, time_column, n_iters, step_size, accept_rate, obj_column]
|
|
34
|
+
|
|
35
|
+
if use_jac:
|
|
36
|
+
columns += [
|
|
37
|
+
TextColumn("{task.fields[grad_norm]:0.5f}", table_column=Column("||grad||", ratio=1))
|
|
38
|
+
]
|
|
39
|
+
if use_hess:
|
|
40
|
+
columns += [
|
|
41
|
+
TextColumn("{task.fields[hess_norm]:0.8f}", table_column=Column("||hess||", ratio=1))
|
|
42
|
+
]
|
|
43
|
+
|
|
44
|
+
return ToggleableProgress(
|
|
45
|
+
*columns,
|
|
46
|
+
expand=False,
|
|
47
|
+
disable=not progressbar,
|
|
48
|
+
console=Console(file=sys.stderr, width=CONSOLE_WIDTH),
|
|
49
|
+
transient=True,
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
__all__ = ["initialize_progress_bar"]
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
from scipy.optimize._basinhopping import BasinHoppingRunner, Storage
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class AllowFailureStorage(Storage):
|
|
5
|
+
"""
|
|
6
|
+
Subclass of Storage that allows the minimizer to fail, but still updates the global minimum
|
|
7
|
+
if the new point is better than the current global minimum.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
def update(self, minres):
|
|
11
|
+
if minres.fun < self.minres.fun:
|
|
12
|
+
self._add(minres)
|
|
13
|
+
return True
|
|
14
|
+
else:
|
|
15
|
+
return False
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class AllowFailureBasinHoppingRunner(BasinHoppingRunner):
|
|
19
|
+
"""
|
|
20
|
+
A subclass of BasinHoppingRunner that allows the minimizer to fail, but still updates the
|
|
21
|
+
global minimum if the new point is better than the current global minimum.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
def __init__(
|
|
25
|
+
self, x0, minimizer, step_taking, accept_tests, accept_on_minimizer_fail=False, disp=False
|
|
26
|
+
):
|
|
27
|
+
super().__init__(x0, minimizer, step_taking, accept_tests, disp=disp)
|
|
28
|
+
|
|
29
|
+
if accept_on_minimizer_fail:
|
|
30
|
+
self.storage = AllowFailureStorage(self.storage.minres)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
__all__ = ["AllowFailureBasinHoppingRunner", "AllowFailureStorage"]
|
|
@@ -33,6 +33,23 @@ root_method = Literal[
|
|
|
33
33
|
TOLERANCES = ["xtol", "ftol", "gtol", "fatol", "xatol"]
|
|
34
34
|
CONSOLE_WIDTH = 100
|
|
35
35
|
|
|
36
|
+
DE_STRATEGY_OPTIONS = (
|
|
37
|
+
"best1bin",
|
|
38
|
+
"best1exp",
|
|
39
|
+
"rand1bin",
|
|
40
|
+
"rand1exp",
|
|
41
|
+
"randtobest1bin",
|
|
42
|
+
"randtobest1exp",
|
|
43
|
+
"currenttobest1bin",
|
|
44
|
+
"currenttobest1exp",
|
|
45
|
+
"best2bin",
|
|
46
|
+
"best2exp",
|
|
47
|
+
"rand2bin",
|
|
48
|
+
"rand2exp",
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
DE_INIT_OPTIONS = ("sobol", "halton", "latinhypercube", "random")
|
|
52
|
+
|
|
36
53
|
MINIMIZE_MODE_KWARGS = {
|
|
37
54
|
"nelder-mead": {
|
|
38
55
|
"uses_grad": False,
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
import contextlib
|
|
2
|
+
import logging
|
|
3
|
+
|
|
4
|
+
from collections.abc import Callable, Sequence
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
import numpy as np
|
|
8
|
+
|
|
9
|
+
from rich.progress import TaskID
|
|
10
|
+
from scipy.optimize import OptimizeResult
|
|
11
|
+
from scipy.optimize import differential_evolution as sp_differential_evolution
|
|
12
|
+
|
|
13
|
+
from better_optimize.constants import DE_INIT_OPTIONS, DE_STRATEGY_OPTIONS
|
|
14
|
+
from better_optimize.utilities import LRUCache1, check_f_is_fused_minimize
|
|
15
|
+
from better_optimize.wrapper import build_progress_bar
|
|
16
|
+
|
|
17
|
+
_log = logging.getLogger(__name__)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _nullcontext():
|
|
21
|
+
return contextlib.nullcontext()
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _bounds_to_array(bounds) -> np.ndarray:
|
|
25
|
+
arr = np.asarray(bounds, dtype=np.float64)
|
|
26
|
+
if arr.ndim != 2 or arr.shape[1] != 2:
|
|
27
|
+
raise ValueError(
|
|
28
|
+
f"bounds must be a sequence of (low, high) pairs or an (n, 2) array; "
|
|
29
|
+
f"got shape {arr.shape}"
|
|
30
|
+
)
|
|
31
|
+
if np.any(arr[:, 0] > arr[:, 1]):
|
|
32
|
+
raise ValueError("bounds low > high for at least one dimension")
|
|
33
|
+
return arr
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _sample_x_from_bounds(bounds_arr: np.ndarray) -> np.ndarray:
|
|
37
|
+
"""Generate one midpoint within bounds for fused-function sniffing."""
|
|
38
|
+
return 0.5 * (bounds_arr[:, 0] + bounds_arr[:, 1])
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _default_maxiter(d: int) -> int:
|
|
42
|
+
return max(1000, 200 * d)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def differential_evolution(
|
|
46
|
+
f: Callable,
|
|
47
|
+
bounds: Sequence[tuple[float, float]] | np.ndarray,
|
|
48
|
+
args: tuple = (),
|
|
49
|
+
strategy: str | Callable = "best1bin",
|
|
50
|
+
maxiter: int | None = None,
|
|
51
|
+
popsize: int = 15,
|
|
52
|
+
tol: float = 0.01,
|
|
53
|
+
mutation: float | tuple[float, float] = (0.5, 1.0),
|
|
54
|
+
recombination: float = 0.7,
|
|
55
|
+
callback: Callable | None = None,
|
|
56
|
+
init: str | np.ndarray = "sobol",
|
|
57
|
+
atol: float = 0.0,
|
|
58
|
+
updating: str = "immediate",
|
|
59
|
+
workers: int | Callable = 1,
|
|
60
|
+
constraints: tuple = (),
|
|
61
|
+
x0: Sequence[float] | np.ndarray | None = None,
|
|
62
|
+
integrality: Sequence[bool] | np.ndarray | None = None,
|
|
63
|
+
vectorized: bool = False,
|
|
64
|
+
progressbar: bool = True,
|
|
65
|
+
progress_task: TaskID | None = None,
|
|
66
|
+
verbose: bool = False,
|
|
67
|
+
rng: int | np.random.Generator | None = None,
|
|
68
|
+
) -> OptimizeResult:
|
|
69
|
+
"""Differential evolution with progress bar, graceful failure, and fused-function support.
|
|
70
|
+
|
|
71
|
+
See scipy's :func:`scipy.optimize.differential_evolution` documentation for parameter
|
|
72
|
+
semantics. Polish customization (method choice, tolerance) is not exposed here by design —
|
|
73
|
+
chain a dedicated ``minimize`` stage via :func:`sequential_optimize` instead.
|
|
74
|
+
|
|
75
|
+
Parameters
|
|
76
|
+
----------
|
|
77
|
+
f : Callable
|
|
78
|
+
Scalar objective, or fused ``(f, grad)`` / ``(f, grad, hess)``.
|
|
79
|
+
bounds : sequence of (low, high) or (d, 2) array
|
|
80
|
+
Search region per dimension. Required.
|
|
81
|
+
progressbar : bool or ToggleableProgress
|
|
82
|
+
If True, show a live rich progress bar with best-so-far each generation. If a
|
|
83
|
+
``ToggleableProgress`` instance is passed (e.g. from ``sequential_optimize``), reuse
|
|
84
|
+
it instead of creating a new one.
|
|
85
|
+
progress_task : TaskID, optional
|
|
86
|
+
When ``sequential_optimize`` pre-registers a task on the shared progress and passes
|
|
87
|
+
the id here, that task is updated instead of creating a new one. Ignored for
|
|
88
|
+
standalone calls.
|
|
89
|
+
rng : int, numpy.random.Generator, or None
|
|
90
|
+
Seed / generator for reproducibility. Forwarded to scipy as the ``rng`` kwarg.
|
|
91
|
+
|
|
92
|
+
Returns
|
|
93
|
+
-------
|
|
94
|
+
scipy.optimize.OptimizeResult
|
|
95
|
+
The usual scipy result. On internal failure, ``success=False`` and ``fun=np.inf``.
|
|
96
|
+
"""
|
|
97
|
+
bounds_arr = _bounds_to_array(bounds)
|
|
98
|
+
d = bounds_arr.shape[0]
|
|
99
|
+
|
|
100
|
+
if isinstance(strategy, str) and strategy not in DE_STRATEGY_OPTIONS:
|
|
101
|
+
raise ValueError(
|
|
102
|
+
f"strategy must be one of {DE_STRATEGY_OPTIONS} or a callable; got {strategy!r}"
|
|
103
|
+
)
|
|
104
|
+
if isinstance(init, str) and init not in DE_INIT_OPTIONS:
|
|
105
|
+
raise ValueError(f"init must be one of {DE_INIT_OPTIONS} or an array; got {init!r}")
|
|
106
|
+
|
|
107
|
+
if maxiter is None:
|
|
108
|
+
maxiter = _default_maxiter(d)
|
|
109
|
+
|
|
110
|
+
sample_x = (
|
|
111
|
+
np.asarray(x0, dtype=np.float64) if x0 is not None else _sample_x_from_bounds(bounds_arr)
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
has_fused_f_and_grad, has_fused_f_grad_hess = check_f_is_fused_minimize(f, sample_x, args or ())
|
|
115
|
+
f_returns_list = has_fused_f_and_grad or has_fused_f_grad_hess
|
|
116
|
+
f_cached = LRUCache1(
|
|
117
|
+
f,
|
|
118
|
+
f_returns_list=f_returns_list,
|
|
119
|
+
copy_x=False,
|
|
120
|
+
dtype=sample_x.dtype.name if sample_x.dtype != object else None,
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
owns_progress = not isinstance(progressbar, object.__class__) and isinstance(progressbar, bool)
|
|
124
|
+
if isinstance(progressbar, bool):
|
|
125
|
+
progress = build_progress_bar(
|
|
126
|
+
description="Differential Evolution",
|
|
127
|
+
progressbar=progressbar,
|
|
128
|
+
root=False,
|
|
129
|
+
use_jac=False,
|
|
130
|
+
use_hess=False,
|
|
131
|
+
)
|
|
132
|
+
owns_progress = True
|
|
133
|
+
else:
|
|
134
|
+
progress = progressbar
|
|
135
|
+
owns_progress = False
|
|
136
|
+
|
|
137
|
+
if progress_task is None:
|
|
138
|
+
de_task = progress.add_task(
|
|
139
|
+
description="Differential Evolution",
|
|
140
|
+
total=maxiter,
|
|
141
|
+
f_value=float("inf"),
|
|
142
|
+
grad_norm=0.0,
|
|
143
|
+
hess_norm=0.0,
|
|
144
|
+
)
|
|
145
|
+
else:
|
|
146
|
+
de_task = progress_task
|
|
147
|
+
progress.update(de_task, total=maxiter, f_value=float("inf"))
|
|
148
|
+
|
|
149
|
+
iter_counter = [0]
|
|
150
|
+
best_fun_seen = [float("inf")]
|
|
151
|
+
|
|
152
|
+
def progress_callback(intermediate_result):
|
|
153
|
+
"""Drive progress bar + forward to user's callback.
|
|
154
|
+
|
|
155
|
+
scipy passes an OptimizeResult-like object since scipy 1.13 (or (xk, convergence)
|
|
156
|
+
in older versions). Support both.
|
|
157
|
+
"""
|
|
158
|
+
if hasattr(intermediate_result, "x"):
|
|
159
|
+
xk = np.asarray(intermediate_result.x, dtype=np.float64)
|
|
160
|
+
convergence = float(getattr(intermediate_result, "convergence", 0.0))
|
|
161
|
+
else: # legacy tuple-style
|
|
162
|
+
xk = np.asarray(intermediate_result, dtype=np.float64)
|
|
163
|
+
convergence = 0.0
|
|
164
|
+
|
|
165
|
+
try:
|
|
166
|
+
f_val = float(f_cached.value(xk))
|
|
167
|
+
except Exception:
|
|
168
|
+
f_val = best_fun_seen[0]
|
|
169
|
+
|
|
170
|
+
if f_val < best_fun_seen[0]:
|
|
171
|
+
best_fun_seen[0] = f_val
|
|
172
|
+
|
|
173
|
+
iter_counter[0] += 1
|
|
174
|
+
progress.update(de_task, advance=1, f_value=best_fun_seen[0], refresh=True)
|
|
175
|
+
|
|
176
|
+
if callback is not None:
|
|
177
|
+
user_stop = callback(xk, convergence=convergence)
|
|
178
|
+
if user_stop:
|
|
179
|
+
raise StopIteration("user callback returned True")
|
|
180
|
+
return False
|
|
181
|
+
|
|
182
|
+
scipy_kwargs: dict[str, Any] = {
|
|
183
|
+
"args": args,
|
|
184
|
+
"strategy": strategy,
|
|
185
|
+
"maxiter": maxiter,
|
|
186
|
+
"popsize": popsize,
|
|
187
|
+
"tol": tol,
|
|
188
|
+
"mutation": mutation,
|
|
189
|
+
"recombination": recombination,
|
|
190
|
+
"rng": rng,
|
|
191
|
+
"callback": progress_callback,
|
|
192
|
+
"disp": False,
|
|
193
|
+
"polish": False,
|
|
194
|
+
"init": init,
|
|
195
|
+
"atol": atol,
|
|
196
|
+
"updating": updating,
|
|
197
|
+
"workers": workers,
|
|
198
|
+
"constraints": constraints,
|
|
199
|
+
"integrality": integrality,
|
|
200
|
+
"vectorized": vectorized,
|
|
201
|
+
}
|
|
202
|
+
if x0 is not None:
|
|
203
|
+
scipy_kwargs["x0"] = np.asarray(x0, dtype=np.float64)
|
|
204
|
+
|
|
205
|
+
fallback_x = np.asarray(x0, dtype=np.float64) if x0 is not None else sample_x
|
|
206
|
+
|
|
207
|
+
progress_ctx = progress if owns_progress else _nullcontext()
|
|
208
|
+
with progress_ctx:
|
|
209
|
+
try:
|
|
210
|
+
res = sp_differential_evolution(f_cached.value, bounds_arr, **scipy_kwargs)
|
|
211
|
+
except (KeyboardInterrupt, StopIteration) as e:
|
|
212
|
+
_log.warning("differential_evolution interrupted: %s", e)
|
|
213
|
+
res = OptimizeResult(
|
|
214
|
+
x=fallback_x,
|
|
215
|
+
fun=best_fun_seen[0],
|
|
216
|
+
success=False,
|
|
217
|
+
message=f"interrupted: {type(e).__name__}",
|
|
218
|
+
nit=iter_counter[0],
|
|
219
|
+
nfev=f_cached.cache_misses,
|
|
220
|
+
)
|
|
221
|
+
except (FloatingPointError, ArithmeticError, np.linalg.LinAlgError) as e:
|
|
222
|
+
_log.warning("differential_evolution raised %s: %s", type(e).__name__, e)
|
|
223
|
+
res = OptimizeResult(
|
|
224
|
+
x=fallback_x,
|
|
225
|
+
fun=float("inf"),
|
|
226
|
+
success=False,
|
|
227
|
+
message=f"differential_evolution raised {type(e).__name__}: {e}",
|
|
228
|
+
nit=iter_counter[0],
|
|
229
|
+
nfev=f_cached.cache_misses,
|
|
230
|
+
)
|
|
231
|
+
else:
|
|
232
|
+
progress.update(de_task, completed=maxiter, f_value=float(res.fun), refresh=True)
|
|
233
|
+
|
|
234
|
+
if verbose:
|
|
235
|
+
_log.info(
|
|
236
|
+
"differential_evolution finished: fun=%.6e, nit=%d, success=%s",
|
|
237
|
+
float(getattr(res, "fun", float("inf"))),
|
|
238
|
+
int(getattr(res, "nit", 0) or 0),
|
|
239
|
+
bool(getattr(res, "success", False)),
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
return res
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
__all__ = ["differential_evolution"]
|
|
@@ -98,6 +98,7 @@ def minimize(
|
|
|
98
98
|
f=f_cached.value_and_grad if has_fused_f_and_grad else f_cached.value,
|
|
99
99
|
jac=jac,
|
|
100
100
|
hess=hess if use_hess else None,
|
|
101
|
+
hessp=hessp if use_hessp else None,
|
|
101
102
|
args=args,
|
|
102
103
|
progressbar=progressbar,
|
|
103
104
|
progressbar_update_interval=progressbar_update_interval,
|
|
@@ -2,6 +2,7 @@ import contextlib
|
|
|
2
2
|
import logging
|
|
3
3
|
import multiprocessing as mp
|
|
4
4
|
import pickle
|
|
5
|
+
import sys
|
|
5
6
|
import threading
|
|
6
7
|
|
|
7
8
|
from collections.abc import Callable
|
|
@@ -176,8 +177,22 @@ def _run_single(
|
|
|
176
177
|
if blas_cores_per_worker is not None
|
|
177
178
|
else contextlib.nullcontext()
|
|
178
179
|
)
|
|
179
|
-
|
|
180
|
-
|
|
180
|
+
try:
|
|
181
|
+
with limiter:
|
|
182
|
+
result = solver(x0=x0, progressbar=progress, progress_task=task_id, **solver_kwargs)
|
|
183
|
+
except Exception as exc:
|
|
184
|
+
_log.warning(
|
|
185
|
+
"Run %d failed with %s: %s",
|
|
186
|
+
run_index,
|
|
187
|
+
type(exc).__name__,
|
|
188
|
+
exc,
|
|
189
|
+
)
|
|
190
|
+
result = OptimizeResult(
|
|
191
|
+
x=x0,
|
|
192
|
+
fun=np.inf,
|
|
193
|
+
success=False,
|
|
194
|
+
message=f"Solver raised {type(exc).__name__}: {exc}",
|
|
195
|
+
)
|
|
181
196
|
|
|
182
197
|
result.run_index = run_index
|
|
183
198
|
result.x0 = x0
|
|
@@ -249,7 +264,7 @@ class MultiStartResult:
|
|
|
249
264
|
|
|
250
265
|
for rank, res in enumerate(display, 1):
|
|
251
266
|
table.add_row(*self._format_result_row(rank, res))
|
|
252
|
-
Console().print(table)
|
|
267
|
+
Console(file=sys.stderr).print(table)
|
|
253
268
|
|
|
254
269
|
def _result_to_dict(self, rank: int, res: OptimizeResult) -> dict:
|
|
255
270
|
"""Convert a single ``OptimizeResult`` into a flat dict for DataFrame export."""
|
|
@@ -404,34 +419,31 @@ class _MultiStart:
|
|
|
404
419
|
if self._is_root:
|
|
405
420
|
mode_info = ROOT_MODE_KWARGS.get(method, {})
|
|
406
421
|
use_jac = mode_info.get("uses_jac", False) or "jac" in self._solver_kwargs
|
|
407
|
-
|
|
422
|
+
use_rayleigh = False
|
|
408
423
|
else:
|
|
409
424
|
mode_info = MINIMIZE_MODE_KWARGS.get(method, {})
|
|
410
425
|
use_jac = mode_info.get("uses_grad", False) or "jac" in self._solver_kwargs
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
or "hess" in self._solver_kwargs
|
|
414
|
-
or "hessp" in self._solver_kwargs
|
|
415
|
-
)
|
|
426
|
+
has_hess = "hess" in self._solver_kwargs or "hessp" in self._solver_kwargs
|
|
427
|
+
use_rayleigh = use_jac and has_hess
|
|
416
428
|
|
|
417
429
|
return build_progress_bar(
|
|
418
430
|
description=self._task_description,
|
|
419
431
|
progressbar=self._progressbar,
|
|
420
432
|
root=self._is_root,
|
|
421
433
|
use_jac=use_jac,
|
|
422
|
-
|
|
434
|
+
use_rayleigh=use_rayleigh,
|
|
423
435
|
)
|
|
424
436
|
|
|
425
437
|
def _register_tasks(self, progress: ToggleableProgress) -> list[TaskID | None]:
|
|
426
438
|
# Include all possible fields so columns never hit a missing key.
|
|
427
|
-
# Unused fields (e.g.
|
|
439
|
+
# Unused fields (e.g. rayleigh when no curvature column) are silently ignored.
|
|
428
440
|
return [
|
|
429
441
|
progress.add_task(
|
|
430
442
|
description=self._task_description,
|
|
431
443
|
total=None,
|
|
432
444
|
f_value=0.0,
|
|
433
445
|
grad_norm=0.0,
|
|
434
|
-
|
|
446
|
+
rayleigh=0.0,
|
|
435
447
|
)
|
|
436
448
|
for _ in self._starting_points
|
|
437
449
|
]
|