better-optimize 0.3.2__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.2 → better_optimize-0.4}/PKG-INFO +2 -2
- {better_optimize-0.3.2 → better_optimize-0.4}/better_optimize/__init__.py +10 -1
- {better_optimize-0.3.2 → 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.2/better_optimize/basinhopping.py → better_optimize-0.4/better_optimize/basinhopping/core.py +2 -81
- 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.2 → 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.2 → better_optimize-0.4}/better_optimize/minimize.py +1 -0
- {better_optimize-0.3.2 → better_optimize-0.4}/better_optimize/multi_optimize.py +6 -9
- better_optimize-0.4/better_optimize/sequential_optimize.py +477 -0
- {better_optimize-0.3.2 → better_optimize-0.4}/better_optimize/utilities.py +16 -4
- {better_optimize-0.3.2 → better_optimize-0.4}/better_optimize/wrapper.py +55 -23
- {better_optimize-0.3.2 → better_optimize-0.4}/better_optimize.egg-info/PKG-INFO +2 -2
- {better_optimize-0.3.2 → better_optimize-0.4}/better_optimize.egg-info/SOURCES.txt +8 -1
- {better_optimize-0.3.2 → better_optimize-0.4}/pyproject.toml +2 -2
- {better_optimize-0.3.2 → better_optimize-0.4}/tests/test_basinhopping.py +5 -6
- better_optimize-0.4/tests/test_differential_evolution.py +232 -0
- better_optimize-0.4/tests/test_sequential_optimize.py +444 -0
- {better_optimize-0.3.2 → better_optimize-0.4}/LICENSE +0 -0
- {better_optimize-0.3.2 → better_optimize-0.4}/README.md +0 -0
- {better_optimize-0.3.2 → better_optimize-0.4}/better_optimize/root.py +0 -0
- {better_optimize-0.3.2 → better_optimize-0.4}/better_optimize.egg-info/dependency_links.txt +0 -0
- {better_optimize-0.3.2 → better_optimize-0.4}/better_optimize.egg-info/requires.txt +0 -0
- {better_optimize-0.3.2 → better_optimize-0.4}/better_optimize.egg-info/top_level.txt +0 -0
- {better_optimize-0.3.2 → better_optimize-0.4}/setup.cfg +0 -0
- {better_optimize-0.3.2 → better_optimize-0.4}/setup.py +0 -0
- {better_optimize-0.3.2 → better_optimize-0.4}/tests/test_jax_integration.py +0 -0
- {better_optimize-0.3.2 → better_optimize-0.4}/tests/test_minimize.py +0 -0
- {better_optimize-0.3.2 → better_optimize-0.4}/tests/test_multistart.py +0 -0
- {better_optimize-0.3.2 → better_optimize-0.4}/tests/test_root.py +0 -0
- {better_optimize-0.3.2 → better_optimize-0.4}/tests/test_utilities.py +0 -0
- {better_optimize-0.3.2 → better_optimize-0.4}/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
|
+
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
|
+
]
|
|
@@ -1,105 +1,27 @@
|
|
|
1
|
-
import sys
|
|
2
|
-
|
|
3
1
|
from collections.abc import Callable, Sequence
|
|
4
2
|
|
|
5
3
|
import numpy as np
|
|
6
4
|
|
|
7
|
-
from rich.console import Console
|
|
8
|
-
from rich.progress import (
|
|
9
|
-
BarColumn,
|
|
10
|
-
MofNCompleteColumn,
|
|
11
|
-
TextColumn,
|
|
12
|
-
TimeElapsedColumn,
|
|
13
|
-
)
|
|
14
|
-
from rich.table import Column
|
|
15
5
|
from scipy.optimize import OptimizeResult
|
|
16
6
|
from scipy.optimize._basinhopping import (
|
|
17
7
|
AdaptiveStepsize,
|
|
18
|
-
BasinHoppingRunner,
|
|
19
8
|
Metropolis,
|
|
20
9
|
MinimizerWrapper,
|
|
21
10
|
RandomDisplacement,
|
|
22
|
-
Storage,
|
|
23
11
|
check_random_state,
|
|
24
12
|
)
|
|
25
13
|
|
|
26
|
-
from better_optimize.
|
|
14
|
+
from better_optimize.basinhopping.progress import initialize_progress_bar
|
|
15
|
+
from better_optimize.basinhopping.runner import AllowFailureBasinHoppingRunner
|
|
27
16
|
from better_optimize.minimize import minimize
|
|
28
17
|
from better_optimize.utilities import (
|
|
29
18
|
LRUCache1,
|
|
30
|
-
ToggleableProgress,
|
|
31
19
|
check_f_is_fused_minimize,
|
|
32
20
|
determine_maxiter,
|
|
33
21
|
validate_provided_functions_minimize,
|
|
34
22
|
)
|
|
35
23
|
|
|
36
24
|
|
|
37
|
-
def initialize_progress_bar(progressbar, use_jac=True, use_hess=False):
|
|
38
|
-
description = "Process"
|
|
39
|
-
name_column = TextColumn("{task.fields[name]}", table_column=Column(description, ratio=2))
|
|
40
|
-
bar_column = BarColumn(bar_width=None, table_column=Column("", ratio=2))
|
|
41
|
-
time_column = TimeElapsedColumn(table_column=Column("Elapsed", ratio=1))
|
|
42
|
-
n_iters = MofNCompleteColumn(table_column=Column("Iteration"))
|
|
43
|
-
|
|
44
|
-
step_size = TextColumn("{task.fields[step_size]:0.3f}", table_column=Column("Step", ratio=1))
|
|
45
|
-
accept_rate = TextColumn(
|
|
46
|
-
"{task.fields[accept_rate]:0.3f}", table_column=Column("Accept Rate", ratio=1)
|
|
47
|
-
)
|
|
48
|
-
|
|
49
|
-
objective_name = "Objective"
|
|
50
|
-
obj_column = TextColumn(
|
|
51
|
-
"{task.fields[f_value]:0.5f}", table_column=Column(objective_name, ratio=1)
|
|
52
|
-
)
|
|
53
|
-
|
|
54
|
-
columns = [name_column, bar_column, time_column, n_iters, step_size, accept_rate, obj_column]
|
|
55
|
-
|
|
56
|
-
if use_jac:
|
|
57
|
-
columns += [
|
|
58
|
-
TextColumn("{task.fields[grad_norm]:0.5f}", table_column=Column("||grad||", ratio=1))
|
|
59
|
-
]
|
|
60
|
-
if use_hess:
|
|
61
|
-
columns += [
|
|
62
|
-
TextColumn("{task.fields[hess_norm]:0.8f}", table_column=Column("||hess||", ratio=1))
|
|
63
|
-
]
|
|
64
|
-
|
|
65
|
-
return ToggleableProgress(
|
|
66
|
-
*columns,
|
|
67
|
-
expand=False,
|
|
68
|
-
disable=not progressbar,
|
|
69
|
-
console=Console(file=sys.stderr, width=CONSOLE_WIDTH),
|
|
70
|
-
transient=True,
|
|
71
|
-
)
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
class AllowFailureStorage(Storage):
|
|
75
|
-
"""
|
|
76
|
-
Subclass of Storage that allows the minimizer to fail, but still updates the global minimum
|
|
77
|
-
if the new point is better than the current global minimum.
|
|
78
|
-
"""
|
|
79
|
-
|
|
80
|
-
def update(self, minres):
|
|
81
|
-
if minres.fun < self.minres.fun:
|
|
82
|
-
self._add(minres)
|
|
83
|
-
return True
|
|
84
|
-
else:
|
|
85
|
-
return False
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
class AllowFailureBasinHoppingRunner(BasinHoppingRunner):
|
|
89
|
-
"""
|
|
90
|
-
A subclass of BasinHoppingRunner that allows the minimizer to fail, but still updates the
|
|
91
|
-
global minimum if the new point is better than the current global minimum.
|
|
92
|
-
"""
|
|
93
|
-
|
|
94
|
-
def __init__(
|
|
95
|
-
self, x0, minimizer, step_taking, accept_tests, accept_on_minimizer_fail=False, disp=False
|
|
96
|
-
):
|
|
97
|
-
super().__init__(x0, minimizer, step_taking, accept_tests, disp=disp)
|
|
98
|
-
|
|
99
|
-
if accept_on_minimizer_fail:
|
|
100
|
-
self.storage = AllowFailureStorage(self.storage.minres)
|
|
101
|
-
|
|
102
|
-
|
|
103
25
|
def basinhopping(
|
|
104
26
|
func: Callable[..., float | np.ndarray],
|
|
105
27
|
x0: Sequence[float],
|
|
@@ -281,7 +203,6 @@ def basinhopping(
|
|
|
281
203
|
else:
|
|
282
204
|
take_step_wrapped = take_step
|
|
283
205
|
else:
|
|
284
|
-
# use default
|
|
285
206
|
displace = RandomDisplacement(stepsize=stepsize, rng=rng)
|
|
286
207
|
take_step_wrapped = AdaptiveStepsize(
|
|
287
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,
|
|
@@ -419,34 +419,31 @@ class _MultiStart:
|
|
|
419
419
|
if self._is_root:
|
|
420
420
|
mode_info = ROOT_MODE_KWARGS.get(method, {})
|
|
421
421
|
use_jac = mode_info.get("uses_jac", False) or "jac" in self._solver_kwargs
|
|
422
|
-
|
|
422
|
+
use_rayleigh = False
|
|
423
423
|
else:
|
|
424
424
|
mode_info = MINIMIZE_MODE_KWARGS.get(method, {})
|
|
425
425
|
use_jac = mode_info.get("uses_grad", False) or "jac" in self._solver_kwargs
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
or "hess" in self._solver_kwargs
|
|
429
|
-
or "hessp" in self._solver_kwargs
|
|
430
|
-
)
|
|
426
|
+
has_hess = "hess" in self._solver_kwargs or "hessp" in self._solver_kwargs
|
|
427
|
+
use_rayleigh = use_jac and has_hess
|
|
431
428
|
|
|
432
429
|
return build_progress_bar(
|
|
433
430
|
description=self._task_description,
|
|
434
431
|
progressbar=self._progressbar,
|
|
435
432
|
root=self._is_root,
|
|
436
433
|
use_jac=use_jac,
|
|
437
|
-
|
|
434
|
+
use_rayleigh=use_rayleigh,
|
|
438
435
|
)
|
|
439
436
|
|
|
440
437
|
def _register_tasks(self, progress: ToggleableProgress) -> list[TaskID | None]:
|
|
441
438
|
# Include all possible fields so columns never hit a missing key.
|
|
442
|
-
# Unused fields (e.g.
|
|
439
|
+
# Unused fields (e.g. rayleigh when no curvature column) are silently ignored.
|
|
443
440
|
return [
|
|
444
441
|
progress.add_task(
|
|
445
442
|
description=self._task_description,
|
|
446
443
|
total=None,
|
|
447
444
|
f_value=0.0,
|
|
448
445
|
grad_norm=0.0,
|
|
449
|
-
|
|
446
|
+
rayleigh=0.0,
|
|
450
447
|
)
|
|
451
448
|
for _ in self._starting_points
|
|
452
449
|
]
|