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.
Files changed (34) hide show
  1. {better_optimize-0.3.1 → better_optimize-0.4}/PKG-INFO +2 -2
  2. {better_optimize-0.3.1 → better_optimize-0.4}/better_optimize/__init__.py +10 -1
  3. {better_optimize-0.3.1 → better_optimize-0.4}/better_optimize/_version.py +3 -3
  4. better_optimize-0.4/better_optimize/basinhopping/__init__.py +11 -0
  5. better_optimize-0.3.1/better_optimize/basinhopping.py → better_optimize-0.4/better_optimize/basinhopping/core.py +2 -79
  6. better_optimize-0.4/better_optimize/basinhopping/progress.py +53 -0
  7. better_optimize-0.4/better_optimize/basinhopping/runner.py +33 -0
  8. {better_optimize-0.3.1 → better_optimize-0.4}/better_optimize/constants.py +17 -0
  9. better_optimize-0.4/better_optimize/differential_evolution.py +245 -0
  10. {better_optimize-0.3.1 → better_optimize-0.4}/better_optimize/minimize.py +1 -0
  11. {better_optimize-0.3.1 → better_optimize-0.4}/better_optimize/multi_optimize.py +24 -12
  12. better_optimize-0.4/better_optimize/sequential_optimize.py +477 -0
  13. {better_optimize-0.3.1 → better_optimize-0.4}/better_optimize/utilities.py +16 -4
  14. {better_optimize-0.3.1 → better_optimize-0.4}/better_optimize/wrapper.py +87 -34
  15. {better_optimize-0.3.1 → better_optimize-0.4}/better_optimize.egg-info/PKG-INFO +2 -2
  16. {better_optimize-0.3.1 → better_optimize-0.4}/better_optimize.egg-info/SOURCES.txt +8 -1
  17. {better_optimize-0.3.1 → better_optimize-0.4}/pyproject.toml +2 -2
  18. {better_optimize-0.3.1 → better_optimize-0.4}/tests/test_basinhopping.py +5 -6
  19. better_optimize-0.4/tests/test_differential_evolution.py +232 -0
  20. {better_optimize-0.3.1 → better_optimize-0.4}/tests/test_multistart.py +51 -12
  21. better_optimize-0.4/tests/test_sequential_optimize.py +444 -0
  22. {better_optimize-0.3.1 → better_optimize-0.4}/tests/test_wrapper.py +5 -4
  23. {better_optimize-0.3.1 → better_optimize-0.4}/LICENSE +0 -0
  24. {better_optimize-0.3.1 → better_optimize-0.4}/README.md +0 -0
  25. {better_optimize-0.3.1 → better_optimize-0.4}/better_optimize/root.py +0 -0
  26. {better_optimize-0.3.1 → better_optimize-0.4}/better_optimize.egg-info/dependency_links.txt +0 -0
  27. {better_optimize-0.3.1 → better_optimize-0.4}/better_optimize.egg-info/requires.txt +0 -0
  28. {better_optimize-0.3.1 → better_optimize-0.4}/better_optimize.egg-info/top_level.txt +0 -0
  29. {better_optimize-0.3.1 → better_optimize-0.4}/setup.cfg +0 -0
  30. {better_optimize-0.3.1 → better_optimize-0.4}/setup.py +0 -0
  31. {better_optimize-0.3.1 → better_optimize-0.4}/tests/test_jax_integration.py +0 -0
  32. {better_optimize-0.3.1 → better_optimize-0.4}/tests/test_minimize.py +0 -0
  33. {better_optimize-0.3.1 → better_optimize-0.4}/tests/test_root.py +0 -0
  34. {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.1
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.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-20T00:48:11+0100",
11
+ "date": "2026-04-21T12:33:06-0500",
12
12
  "dirty": false,
13
13
  "error": null,
14
- "full-revisionid": "0478ed4b401b74272d06dee5e904e77be2916235",
15
- "version": "0.3.1"
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.constants import CONSOLE_WIDTH
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
- with limiter:
180
- result = solver(x0=x0, progressbar=progress, progress_task=task_id, **solver_kwargs)
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
- use_hess = False
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
- use_hess = (
412
- mode_info.get("uses_hess", False)
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
- use_hess=use_hess,
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. hess_norm when no hess column) are silently ignored.
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
- hess_norm=0.0,
446
+ rayleigh=0.0,
435
447
  )
436
448
  for _ in self._starting_points
437
449
  ]