Skip to content

Commit dbabf08

Browse files
committed
Support walltime limit in ScipyOptimizer
SciPy optimizers don't support wall time limits directly. However, this can be achieved through callbacks. This is done here.
1 parent c5acea7 commit dbabf08

File tree

2 files changed

+81
-8
lines changed

2 files changed

+81
-8
lines changed

pypesto/optimize/optimizer.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -439,6 +439,8 @@ def __init__(
439439
if self.options is None:
440440
self.options = ScipyOptimizer.get_default_options(self)
441441
self.tol = tol
442+
#: maximum walltime in seconds
443+
self._maxtime_seconds: float | None = None
442444

443445
def __repr__(self) -> str:
444446
rep = f"<{self.__class__.__name__} method={self.method}"
@@ -601,6 +603,20 @@ def fun(x):
601603
if hessp is not None:
602604
hess = None
603605

606+
# Set callback for handling timelimit if necessary
607+
callback = None
608+
if self._maxtime_seconds is not None and np.isfinite(
609+
self._maxtime_seconds
610+
):
611+
start_time = time.time()
612+
613+
def callback(*args, **kwargs):
614+
elapsed_time = time.time() - start_time
615+
if elapsed_time >= self._maxtime_seconds:
616+
raise StopIteration(
617+
f"Maximum time {self._maxtime_seconds}s exceeded."
618+
)
619+
604620
# optimize
605621
res = scipy.optimize.minimize(
606622
fun=fun,
@@ -612,6 +628,7 @@ def fun(x):
612628
bounds=bounds,
613629
options=self.options,
614630
tol=self.tol,
631+
callback=callback,
615632
)
616633
# extract fval/grad from result
617634
grad = getattr(res, "jac", None)
@@ -670,6 +687,41 @@ def set_maxiter(self, iterations: int) -> None:
670687
else:
671688
self.options["maxiter"] = iterations
672689

690+
def supports_maxtime(self) -> bool:
691+
"""
692+
Check whether optimizer supports time limits.
693+
694+
Returns
695+
-------
696+
True if optimizer supports setting a maximum wall time,
697+
False otherwise.
698+
"""
699+
# TNC neither supports time limits nor callback functions
700+
return self.method.lower() != "tnc"
701+
702+
def set_maxtime(self, seconds: float) -> None:
703+
"""
704+
Set the maximum wall time for optimization.
705+
706+
Parameters
707+
----------
708+
seconds
709+
Maximum wall time in seconds.
710+
711+
Raises
712+
------
713+
NotImplementedError
714+
If the optimizer does not support time limits.
715+
"""
716+
if not self.supports_maxtime():
717+
raise NotImplementedError(
718+
f"{self.__class__.__name__} method {self.method} does not "
719+
"support time limits. "
720+
f"Check supports_maxtime() before calling set_maxtime()."
721+
)
722+
723+
self._maxtime_seconds = seconds
724+
673725

674726
class IpoptOptimizer(Optimizer):
675727
"""Use Ipopt (https://pypi.org/project/cyipopt/) for optimization."""

test/optimize/test_optimizer_common_interface.py

Lines changed: 29 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,16 @@
11
"""Test unified interface for optimizer limits (time, iterations, evaluations)."""
22

3+
import numpy as np
34
import pytest
5+
import scipy.optimize as so
46

7+
import pypesto
58
import pypesto.optimize as optimize
69

710

811
class TestOptimizerMaxtimeInterface:
912
"""Test the unified maxtime interface for optimizers."""
1013

11-
def test_scipy_optimizer_no_support(self):
12-
"""Test that ScipyOptimizer does not support time limits."""
13-
optimizer = optimize.ScipyOptimizer()
14-
assert optimizer.supports_maxtime() is False
15-
16-
with pytest.raises(NotImplementedError):
17-
optimizer.set_maxtime(10.0)
18-
1914
def test_ipopt_optimizer_support(self):
2015
"""Test IpoptOptimizer time limit support."""
2116
optimizer = optimize.IpoptOptimizer()
@@ -71,6 +66,32 @@ def test_ess_optimizer_support(self):
7166
optimizer.set_maxtime(10.0)
7267
assert optimizer.max_walltime_s == 10.0
7368

69+
def test_scipy_optimizer_support(self):
70+
"""Test ScipyOptimizer time limit support."""
71+
optimizer = optimize.ScipyOptimizer()
72+
assert optimizer.supports_maxtime() is True
73+
optimizer.set_maxtime(1.0)
74+
75+
def fun(x):
76+
import time
77+
78+
time.sleep(0.1)
79+
return so.rosen(x)
80+
81+
objective = pypesto.Objective(fun=fun, grad=so.rosen_der)
82+
dim_full = 2
83+
lb = -5 * np.ones((dim_full, 1))
84+
ub = 5 * np.ones((dim_full, 1))
85+
86+
problem = pypesto.Problem(
87+
objective=objective,
88+
lb=lb,
89+
ub=ub,
90+
)
91+
92+
result = optimizer.minimize(problem, id="test", x0=np.zeros(dim_full))
93+
assert "StopIteration" in result.message
94+
7495

7596
class TestOptimizerMaxiterInterface:
7697
"""Test the unified maxiter interface for optimizers."""

0 commit comments

Comments
 (0)