better-optimize 0.3.1__tar.gz → 0.3.2__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 (27) hide show
  1. {better_optimize-0.3.1 → better_optimize-0.3.2}/PKG-INFO +1 -1
  2. {better_optimize-0.3.1 → better_optimize-0.3.2}/better_optimize/_version.py +3 -3
  3. {better_optimize-0.3.1 → better_optimize-0.3.2}/better_optimize/basinhopping.py +3 -1
  4. {better_optimize-0.3.1 → better_optimize-0.3.2}/better_optimize/multi_optimize.py +18 -3
  5. {better_optimize-0.3.1 → better_optimize-0.3.2}/better_optimize/wrapper.py +36 -15
  6. {better_optimize-0.3.1 → better_optimize-0.3.2}/better_optimize.egg-info/PKG-INFO +1 -1
  7. {better_optimize-0.3.1 → better_optimize-0.3.2}/tests/test_multistart.py +51 -12
  8. {better_optimize-0.3.1 → better_optimize-0.3.2}/tests/test_wrapper.py +5 -4
  9. {better_optimize-0.3.1 → better_optimize-0.3.2}/LICENSE +0 -0
  10. {better_optimize-0.3.1 → better_optimize-0.3.2}/README.md +0 -0
  11. {better_optimize-0.3.1 → better_optimize-0.3.2}/better_optimize/__init__.py +0 -0
  12. {better_optimize-0.3.1 → better_optimize-0.3.2}/better_optimize/constants.py +0 -0
  13. {better_optimize-0.3.1 → better_optimize-0.3.2}/better_optimize/minimize.py +0 -0
  14. {better_optimize-0.3.1 → better_optimize-0.3.2}/better_optimize/root.py +0 -0
  15. {better_optimize-0.3.1 → better_optimize-0.3.2}/better_optimize/utilities.py +0 -0
  16. {better_optimize-0.3.1 → better_optimize-0.3.2}/better_optimize.egg-info/SOURCES.txt +0 -0
  17. {better_optimize-0.3.1 → better_optimize-0.3.2}/better_optimize.egg-info/dependency_links.txt +0 -0
  18. {better_optimize-0.3.1 → better_optimize-0.3.2}/better_optimize.egg-info/requires.txt +0 -0
  19. {better_optimize-0.3.1 → better_optimize-0.3.2}/better_optimize.egg-info/top_level.txt +0 -0
  20. {better_optimize-0.3.1 → better_optimize-0.3.2}/pyproject.toml +0 -0
  21. {better_optimize-0.3.1 → better_optimize-0.3.2}/setup.cfg +0 -0
  22. {better_optimize-0.3.1 → better_optimize-0.3.2}/setup.py +0 -0
  23. {better_optimize-0.3.1 → better_optimize-0.3.2}/tests/test_basinhopping.py +0 -0
  24. {better_optimize-0.3.1 → better_optimize-0.3.2}/tests/test_jax_integration.py +0 -0
  25. {better_optimize-0.3.1 → better_optimize-0.3.2}/tests/test_minimize.py +0 -0
  26. {better_optimize-0.3.1 → better_optimize-0.3.2}/tests/test_root.py +0 -0
  27. {better_optimize-0.3.1 → better_optimize-0.3.2}/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.3.2
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
@@ -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-03-20T19:57:42+0100",
12
12
  "dirty": false,
13
13
  "error": null,
14
- "full-revisionid": "0478ed4b401b74272d06dee5e904e77be2916235",
15
- "version": "0.3.1"
14
+ "full-revisionid": "2c8f4f7afbc2b54fa1e8aa8827b4be15e6ea87a9",
15
+ "version": "0.3.2"
16
16
  }
17
17
  ''' # END VERSION_JSON
18
18
 
@@ -1,3 +1,5 @@
1
+ import sys
2
+
1
3
  from collections.abc import Callable, Sequence
2
4
 
3
5
  import numpy as np
@@ -64,7 +66,7 @@ def initialize_progress_bar(progressbar, use_jac=True, use_hess=False):
64
66
  *columns,
65
67
  expand=False,
66
68
  disable=not progressbar,
67
- console=Console(width=CONSOLE_WIDTH),
69
+ console=Console(file=sys.stderr, width=CONSOLE_WIDTH),
68
70
  transient=True,
69
71
  )
70
72
 
@@ -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."""
@@ -1,4 +1,5 @@
1
1
  import logging
2
+ import sys
2
3
 
3
4
  from collections.abc import Callable
4
5
  from functools import partial
@@ -61,7 +62,7 @@ def build_progress_bar(
61
62
  *columns,
62
63
  expand=False,
63
64
  disable=not progressbar,
64
- console=Console(width=CONSOLE_WIDTH),
65
+ console=Console(file=sys.stderr, width=CONSOLE_WIDTH),
65
66
  )
66
67
 
67
68
 
@@ -234,20 +235,40 @@ def optimizer_early_stopping_wrapper(f_optim: partial):
234
235
  "prematurely.",
235
236
  )
236
237
  except Exception as e:
237
- raise e
238
-
239
- outputs = objective.step(final_value)
240
-
241
- if not objective.use_jac and not objective.use_hess:
242
- value = outputs
243
- grad = None
244
- hess = None
245
- elif objective.use_jac and not objective.use_hess:
246
- value, grad = outputs
247
- hess = None
248
- else:
249
- value, grad, hess = outputs
238
+ # Catch any runtime errors from the optimizer (e.g. nan/inf) and return a failed
239
+ # result instead of crashing.
240
+ _log.warning("Optimizer raised %s: %s", type(e).__name__, e)
241
+ final_value = objective.previous_x
242
+ x0 = f_optim.keywords.get("x0")
243
+ if final_value is None:
244
+ final_value = x0
245
+ res = OptimizeResult(
246
+ x=final_value,
247
+ fun=np.inf,
248
+ success=False,
249
+ message=f"Optimizer raised {type(e).__name__}: {e}",
250
+ )
250
251
 
251
- objective.update_progressbar(value, grad, hess, completed=True)
252
+ # Guard against final_value being None (can happen if no finite
253
+ # iterate was ever recorded and no x0 was available).
254
+ if final_value is not None:
255
+ try:
256
+ outputs = objective.step(final_value)
257
+
258
+ if not objective.use_jac and not objective.use_hess:
259
+ value = outputs
260
+ grad = None
261
+ hess = None
262
+ elif objective.use_jac and not objective.use_hess:
263
+ value, grad = outputs
264
+ hess = None
265
+ else:
266
+ value, grad, hess = outputs
267
+
268
+ objective.update_progressbar(value, grad, hess, completed=True)
269
+ except Exception:
270
+ # If the final evaluation itself fails (e.g. NaN inputs),
271
+ # just mark the progress bar as complete.
272
+ _log.debug("Final evaluation failed; skipping progress-bar update.", exc_info=True)
252
273
 
253
274
  return res
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: better_optimize
3
- Version: 0.3.1
3
+ Version: 0.3.2
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
@@ -263,19 +263,27 @@ def test_run_single_attaches_metadata():
263
263
  assert_allclose(result.x0, x0)
264
264
 
265
265
 
266
- def test_run_single_propagates_solver_exception():
266
+ def test_run_single_catches_solver_exception():
267
+ """Solver exceptions are caught and returned as failed OptimizeResults."""
268
+
267
269
  def exploding_solver(x0, **kwargs):
268
- raise RuntimeError("solver exploded")
269
-
270
- with pytest.raises(RuntimeError, match="solver exploded"):
271
- _run_single(
272
- run_index=0,
273
- x0=np.zeros(2),
274
- solver=exploding_solver,
275
- solver_kwargs={},
276
- progress=MagicMock(),
277
- task_id=0,
278
- )
270
+ raise RuntimeError("NaN encountered")
271
+
272
+ x0 = np.array([1.0, 2.0])
273
+ result = _run_single(
274
+ run_index=3,
275
+ x0=x0,
276
+ solver=exploding_solver,
277
+ solver_kwargs={},
278
+ progress=MagicMock(),
279
+ task_id=0,
280
+ )
281
+ assert not result.success
282
+ assert result.fun == np.inf
283
+ assert "RuntimeError" in result.message
284
+ assert result.run_index == 3
285
+ assert_allclose(result.x0, x0)
286
+ assert_allclose(result.x, x0)
279
287
 
280
288
 
281
289
  class TestMultiStartResult:
@@ -431,3 +439,34 @@ def test_multistart_pickle_fallback(caplog):
431
439
  # Fallback may or may not trigger depending on environment,
432
440
  # but either way we must get valid results.
433
441
  assert len(result.results) == 2
442
+
443
+
444
+ def test_multistart_survives_crashing_solver():
445
+ """A solver that raises for some starting points should not kill the campaign."""
446
+ call_count = 0
447
+
448
+ def sometimes_exploding_solver(x0, **kwargs):
449
+ nonlocal call_count
450
+ call_count += 1
451
+ if call_count % 2 == 0:
452
+ raise RuntimeError("NaN in trust region subproblem")
453
+ return OptimizeResult(x=x0, fun=float(np.sum(x0**2)), success=True)
454
+
455
+ x0_list = [np.ones(2) * i for i in range(4)]
456
+ result = multi_optimize(
457
+ solver=sometimes_exploding_solver,
458
+ x0=x0_list,
459
+ backend="sequential",
460
+ progressbar=False,
461
+ blas_cores=None,
462
+ )
463
+
464
+ assert len(result.results) == 4
465
+ n_success = sum(r.success for r in result.results)
466
+ n_failed = sum(not r.success for r in result.results)
467
+ assert n_success == 2
468
+ assert n_failed == 2
469
+ for r in result.results:
470
+ if not r.success:
471
+ assert r.fun == np.inf
472
+ assert "RuntimeError" in r.message
@@ -43,7 +43,8 @@ def test_early_return_from_keyboard_interrupt(root, method):
43
43
 
44
44
 
45
45
  @pytest.mark.parametrize("root", [True, False], ids=["root", "minimize"])
46
- def test_execption_breaks_optimization(root, monkeypatch):
46
+ def test_exception_returns_failed_result(root, monkeypatch):
47
+ """Exceptions raised inside the optimizer should be caught and returned as a failed result."""
47
48
  N_EXEC = 0
48
49
 
49
50
  def f(x: np.ndarray):
@@ -64,9 +65,9 @@ def test_execption_breaks_optimization(root, monkeypatch):
64
65
  callback=objective.callback,
65
66
  )
66
67
 
67
- # Non-KeyboardInterrupt exceptions should break execution.
68
- with pytest.raises(Exception, match="Simulated error"):
69
- optimizer_early_stopping_wrapper(f_optim)
68
+ result = optimizer_early_stopping_wrapper(f_optim)
69
+ assert not result.success
70
+ assert "Simulated error" in result.message
70
71
 
71
72
 
72
73
  def func2(x, a, b):
File without changes