better-optimize 0.3.2__tar.gz → 0.4.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. {better_optimize-0.3.2 → better_optimize-0.4.1}/PKG-INFO +2 -2
  2. {better_optimize-0.3.2 → better_optimize-0.4.1}/better_optimize/__init__.py +10 -1
  3. {better_optimize-0.3.2 → better_optimize-0.4.1}/better_optimize/_version.py +3 -3
  4. better_optimize-0.4.1/better_optimize/basinhopping/__init__.py +11 -0
  5. better_optimize-0.3.2/better_optimize/basinhopping.py → better_optimize-0.4.1/better_optimize/basinhopping/core.py +2 -81
  6. better_optimize-0.4.1/better_optimize/basinhopping/progress.py +53 -0
  7. better_optimize-0.4.1/better_optimize/basinhopping/runner.py +33 -0
  8. {better_optimize-0.3.2 → better_optimize-0.4.1}/better_optimize/constants.py +17 -0
  9. better_optimize-0.4.1/better_optimize/differential_evolution.py +244 -0
  10. {better_optimize-0.3.2 → better_optimize-0.4.1}/better_optimize/minimize.py +1 -0
  11. {better_optimize-0.3.2 → better_optimize-0.4.1}/better_optimize/multi_optimize.py +6 -9
  12. better_optimize-0.4.1/better_optimize/sequential_optimize.py +477 -0
  13. {better_optimize-0.3.2 → better_optimize-0.4.1}/better_optimize/utilities.py +16 -4
  14. {better_optimize-0.3.2 → better_optimize-0.4.1}/better_optimize/wrapper.py +55 -23
  15. {better_optimize-0.3.2 → better_optimize-0.4.1}/better_optimize.egg-info/PKG-INFO +2 -2
  16. {better_optimize-0.3.2 → better_optimize-0.4.1}/better_optimize.egg-info/SOURCES.txt +8 -1
  17. {better_optimize-0.3.2 → better_optimize-0.4.1}/pyproject.toml +2 -2
  18. {better_optimize-0.3.2 → better_optimize-0.4.1}/tests/test_basinhopping.py +5 -6
  19. better_optimize-0.4.1/tests/test_differential_evolution.py +232 -0
  20. better_optimize-0.4.1/tests/test_sequential_optimize.py +444 -0
  21. {better_optimize-0.3.2 → better_optimize-0.4.1}/LICENSE +0 -0
  22. {better_optimize-0.3.2 → better_optimize-0.4.1}/README.md +0 -0
  23. {better_optimize-0.3.2 → better_optimize-0.4.1}/better_optimize/root.py +0 -0
  24. {better_optimize-0.3.2 → better_optimize-0.4.1}/better_optimize.egg-info/dependency_links.txt +0 -0
  25. {better_optimize-0.3.2 → better_optimize-0.4.1}/better_optimize.egg-info/requires.txt +0 -0
  26. {better_optimize-0.3.2 → better_optimize-0.4.1}/better_optimize.egg-info/top_level.txt +0 -0
  27. {better_optimize-0.3.2 → better_optimize-0.4.1}/setup.cfg +0 -0
  28. {better_optimize-0.3.2 → better_optimize-0.4.1}/setup.py +0 -0
  29. {better_optimize-0.3.2 → better_optimize-0.4.1}/tests/test_jax_integration.py +0 -0
  30. {better_optimize-0.3.2 → better_optimize-0.4.1}/tests/test_minimize.py +0 -0
  31. {better_optimize-0.3.2 → better_optimize-0.4.1}/tests/test_multistart.py +0 -0
  32. {better_optimize-0.3.2 → better_optimize-0.4.1}/tests/test_root.py +0 -0
  33. {better_optimize-0.3.2 → better_optimize-0.4.1}/tests/test_utilities.py +0 -0
  34. {better_optimize-0.3.2 → better_optimize-0.4.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.2
3
+ Version: 0.4.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
@@ -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.10
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__ = ["minimize", "root", "basinhopping", "multi_optimize"]
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-03-20T19:57:42+0100",
11
+ "date": "2026-04-21T14:03:17-0500",
12
12
  "dirty": false,
13
13
  "error": null,
14
- "full-revisionid": "2c8f4f7afbc2b54fa1e8aa8827b4be15e6ea87a9",
15
- "version": "0.3.2"
14
+ "full-revisionid": "ac00b49724e28bdbe53d12fcd222d003decfbd67",
15
+ "version": "0.4.1"
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.constants import CONSOLE_WIDTH
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,244 @@
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
+ )
131
+ owns_progress = True
132
+ else:
133
+ progress = progressbar
134
+ owns_progress = False
135
+
136
+ if progress_task is None:
137
+ de_task = progress.add_task(
138
+ description="Differential Evolution",
139
+ total=maxiter,
140
+ f_value=float("inf"),
141
+ grad_norm=0.0,
142
+ hess_norm=0.0,
143
+ )
144
+ else:
145
+ de_task = progress_task
146
+ progress.update(de_task, total=maxiter, f_value=float("inf"))
147
+
148
+ iter_counter = [0]
149
+ best_fun_seen = [float("inf")]
150
+
151
+ def progress_callback(intermediate_result):
152
+ """Drive progress bar + forward to user's callback.
153
+
154
+ scipy passes an OptimizeResult-like object since scipy 1.13 (or (xk, convergence)
155
+ in older versions). Support both.
156
+ """
157
+ if hasattr(intermediate_result, "x"):
158
+ xk = np.asarray(intermediate_result.x, dtype=np.float64)
159
+ convergence = float(getattr(intermediate_result, "convergence", 0.0))
160
+ else: # legacy tuple-style
161
+ xk = np.asarray(intermediate_result, dtype=np.float64)
162
+ convergence = 0.0
163
+
164
+ try:
165
+ f_val = float(f_cached.value(xk))
166
+ except Exception:
167
+ f_val = best_fun_seen[0]
168
+
169
+ if f_val < best_fun_seen[0]:
170
+ best_fun_seen[0] = f_val
171
+
172
+ iter_counter[0] += 1
173
+ progress.update(de_task, advance=1, f_value=best_fun_seen[0], refresh=True)
174
+
175
+ if callback is not None:
176
+ user_stop = callback(xk, convergence=convergence)
177
+ if user_stop:
178
+ raise StopIteration("user callback returned True")
179
+ return False
180
+
181
+ scipy_kwargs: dict[str, Any] = {
182
+ "args": args,
183
+ "strategy": strategy,
184
+ "maxiter": maxiter,
185
+ "popsize": popsize,
186
+ "tol": tol,
187
+ "mutation": mutation,
188
+ "recombination": recombination,
189
+ "rng": rng,
190
+ "callback": progress_callback,
191
+ "disp": False,
192
+ "polish": False,
193
+ "init": init,
194
+ "atol": atol,
195
+ "updating": updating,
196
+ "workers": workers,
197
+ "constraints": constraints,
198
+ "integrality": integrality,
199
+ "vectorized": vectorized,
200
+ }
201
+ if x0 is not None:
202
+ scipy_kwargs["x0"] = np.asarray(x0, dtype=np.float64)
203
+
204
+ fallback_x = np.asarray(x0, dtype=np.float64) if x0 is not None else sample_x
205
+
206
+ progress_ctx = progress if owns_progress else _nullcontext()
207
+ with progress_ctx:
208
+ try:
209
+ res = sp_differential_evolution(f_cached.value, bounds_arr, **scipy_kwargs)
210
+ except (KeyboardInterrupt, StopIteration) as e:
211
+ _log.warning("differential_evolution interrupted: %s", e)
212
+ res = OptimizeResult(
213
+ x=fallback_x,
214
+ fun=best_fun_seen[0],
215
+ success=False,
216
+ message=f"interrupted: {type(e).__name__}",
217
+ nit=iter_counter[0],
218
+ nfev=f_cached.cache_misses,
219
+ )
220
+ except (FloatingPointError, ArithmeticError, np.linalg.LinAlgError) as e:
221
+ _log.warning("differential_evolution raised %s: %s", type(e).__name__, e)
222
+ res = OptimizeResult(
223
+ x=fallback_x,
224
+ fun=float("inf"),
225
+ success=False,
226
+ message=f"differential_evolution raised {type(e).__name__}: {e}",
227
+ nit=iter_counter[0],
228
+ nfev=f_cached.cache_misses,
229
+ )
230
+ else:
231
+ progress.update(de_task, completed=maxiter, f_value=float(res.fun), refresh=True)
232
+
233
+ if verbose:
234
+ _log.info(
235
+ "differential_evolution finished: fun=%.6e, nit=%d, success=%s",
236
+ float(getattr(res, "fun", float("inf"))),
237
+ int(getattr(res, "nit", 0) or 0),
238
+ bool(getattr(res, "success", False)),
239
+ )
240
+
241
+ return res
242
+
243
+
244
+ __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
- use_hess = False
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
- use_hess = (
427
- mode_info.get("uses_hess", False)
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
- use_hess=use_hess,
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. hess_norm when no hess column) are silently ignored.
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
- hess_norm=0.0,
446
+ rayleigh=0.0,
450
447
  )
451
448
  for _ in self._starting_points
452
449
  ]