Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Ipopt factory for use with SacessOptimizer #1533

Draft
wants to merge 5 commits into
base: develop
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions pypesto/optimize/ess/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from .refset import RefSet
from .sacess import (
SacessFidesFactory,
SacessIpoptFactory,
SacessOptimizer,
SacessOptions,
get_default_ess_options,
Expand Down
77 changes: 53 additions & 24 deletions pypesto/optimize/ess/ess.py
Original file line number Diff line number Diff line change
Expand Up @@ -545,7 +545,7 @@ def _do_local_search(
self, x_best_children: np.ndarray, fx_best_children: np.ndarray
) -> None:
"""
Perform a local search to refine the next generation.
Perform local searches to refine the next generation.

See [PenasGon2017]_ Algorithm 2.
"""
Expand Down Expand Up @@ -606,29 +606,8 @@ def _do_local_search(
local_search_x0,
local_search_fx0,
) in local_search_x0_fx0_candidates:
optimizer = (
self.local_optimizer
if isinstance(self.local_optimizer, pypesto.optimize.Optimizer)
else self.local_optimizer(
max_eval=self._get_remaining_eval(),
max_walltime_s=self._get_remaining_time(),
)
)
optimizer_result: OptimizerResult = optimizer.minimize(
problem=self.evaluator.problem,
x0=local_search_x0,
id="0",
)
# add function evaluations during local search to our function
# evaluation counter (NOTE: depending on the setup, we might neglect
# gradient evaluations).
self.evaluator.n_eval += optimizer_result.n_fval
self.evaluator.n_eval_round += optimizer_result.n_fval

self.logger.info(
f"Local search: {local_search_fx0} -> {optimizer_result.fval} "
f"took {optimizer_result.time:.3g}s, finished with "
f"{optimizer_result.exitflag}: {optimizer_result.message}"
optimizer_result = self._local_minimize(
x0=local_search_x0, fx0=local_search_fx0
)
if np.isfinite(optimizer_result.fval):
self.local_solutions.append(optimizer_result)
Expand All @@ -647,6 +626,56 @@ def _do_local_search(
self.last_local_search_niter = self.n_iter
self.evaluator.reset_round_counter()

def _local_minimize(self, x0: np.ndarray, fx0: float) -> OptimizerResult:
"""Perform a local search from the given startpoint."""
max_walltime_s = self._get_remaining_time()
max_eval = self._get_remaining_eval()
# If we are out of budget, return a dummy result.
# This prevents issues with optimizer that fail on if the budget is 0,
# e.g., Ipopt.
if max_walltime_s < 1 or max_eval < 1:
msg = "No time or function evaluations left for local search."
self.logger.info(msg)
return OptimizerResult(
id="0",
x=x0,
fval=np.inf,
message=msg,
n_fval=0,
n_grad=0,
time=0,
history=None,
)

# create optimizer instance if necessary
optimizer = (
self.local_optimizer
if isinstance(self.local_optimizer, pypesto.optimize.Optimizer)
else self.local_optimizer(
max_eval=max_eval,
max_walltime_s=max_walltime_s,
)
)
# actual local search
optimizer_result: OptimizerResult = optimizer.minimize(
problem=self.evaluator.problem,
x0=x0,
id="0",
)

# add function evaluations during local search to our function
# evaluation counter (NOTE: depending on the setup, we might neglect
# gradient evaluations).
self.evaluator.n_eval += optimizer_result.n_fval
self.evaluator.n_eval_round += optimizer_result.n_fval

self.logger.info(
f"Local search: {fx0} -> {optimizer_result.fval} "
f"took {optimizer_result.time:.3g}s, finished with "
f"{optimizer_result.exitflag}: {optimizer_result.message}"
)
return optimizer_result

def _maybe_update_global_best(self, x, fx):
"""Update the global best value if the provided value is better."""
if fx < self.fx_best:
Expand Down
47 changes: 47 additions & 0 deletions pypesto/optimize/ess/sacess.py
Original file line number Diff line number Diff line change
Expand Up @@ -1248,6 +1248,53 @@ def __repr__(self):
return f"{self.__class__.__name__}(fides_options={self._fides_options}, fides_kwargs={self._fides_kwargs})"


class SacessIpoptFactory:
"""Factory for :class:`IpoptOptimizer` instances for use with :class:`SacessOptimizer`.

:meth:`__call__` will forward the walltime limit and function evaluation
limit imposed on :class:`SacessOptimizer` to :class:`IpoptOptimizer`.
Besides that, default options are used.


Parameters
----------
ipopt_options:
Options for the :class:`IpoptOptimizer`.
See https://coin-or.github.io/Ipopt/OPTIONS.html.
"""

def __init__(
self,
ipopt_options: dict[str, Any] | None = None,
):
if ipopt_options is None:
ipopt_options = {}

self._ipopt_options = ipopt_options

def __call__(
self, max_walltime_s: float, max_eval: float
) -> pypesto.optimize.IpoptOptimizer:
"""Create a :class:`IpoptOptimizer` instance."""

options = self._ipopt_options.copy()
# only accepts int
if np.isfinite(max_walltime_s):
options["max_wall_time"] = int(max_walltime_s)

# only accepts int
if np.isfinite(max_eval):
raise NotImplementedError(
"Ipopt does not support function evaluation limits."
)
return pypesto.optimize.IpoptOptimizer(options=options)

def __repr__(self):
return (
f"{self.__class__.__name__}(ipopt_options={self._ipopt_options})"
)


@dataclass
class SacessWorkerResult:
"""Container for :class:`SacessWorker` results.
Expand Down
10 changes: 8 additions & 2 deletions test/optimize/test_optimize.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
FunctionEvaluatorMP,
RefSet,
SacessFidesFactory,
SacessIpoptFactory,
SacessOptimizer,
SacessOptions,
get_default_ess_options,
Expand Down Expand Up @@ -462,7 +463,12 @@ def test_history_beats_optimizer():
@pytest.mark.parametrize("ess_type", ["ess", "sacess"])
@pytest.mark.parametrize(
"local_optimizer",
[None, optimize.FidesOptimizer(), SacessFidesFactory()],
[
None,
optimize.FidesOptimizer(),
SacessFidesFactory(),
SacessIpoptFactory(),
],
)
@pytest.mark.flaky(reruns=3)
def test_ess(problem, local_optimizer, ess_type, request):
Expand Down Expand Up @@ -492,7 +498,7 @@ def test_ess(problem, local_optimizer, ess_type, request):
for x in ess_init_args:
x["local_optimizer"] = local_optimizer
ess = SacessOptimizer(
max_walltime_s=1,
max_walltime_s=4,
sacess_loglevel=logging.DEBUG,
ess_loglevel=logging.WARNING,
ess_init_args=ess_init_args,
Expand Down
Loading