# This code is part of a Qiskit project.
#
# (C) Copyright IBM 2020, 2023.
#
# This code is licensed under the Apache License, Version 2.0. You may
# obtain a copy of this license in the LICENSE.txt file in the root directory
# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.
#
# Any modifications or derivative works of this code must retain this
# copyright notice, and modified files need to carry a notice indicating
# that they have been altered from the originals.
"""Defines an abstract class for multi start optimizers. A multi start optimizer is an optimizer
that may run minimization algorithm for the several time with different initial guesses to achieve
better results. This implementation is suitable for local optimizers."""
import logging
import time
from abc import ABC
from typing import Callable, Tuple, Any, Optional
import numpy as np
from scipy.stats import uniform
from ..problems.quadratic_program import QuadraticProgram
from ..infinity import INFINITY
from .optimization_algorithm import OptimizationAlgorithm, OptimizationResult
from ..converters import MaximizeToMinimize
logger = logging.getLogger(__name__)
# we disable a warning: "Method 'a method' is abstract in class 'OptimizationAlgorithm' but
# is not overridden (abstract-method) since this class is not intended for instantiation
# pylint: disable=W0223
[docs]class MultiStartOptimizer(OptimizationAlgorithm, ABC):
"""
An abstract class that implements multi start optimization and should be sub-classed by
other optimizers.
"""
def __init__(self, trials: int = 1, clip: float = 100.0) -> None:
"""
Constructs an instance of this optimizer.
Args:
trials: The number of trials for multi-start method. The first trial is solved with
the initial guess of zero. If more than one trial is specified then
initial guesses are uniformly drawn from ``[lowerbound, upperbound]``
with potential clipping.
clip: Clipping parameter for the initial guesses in the multi-start method.
If a variable is unbounded then the lower bound and/or upper bound are replaced
with the ``-clip`` or ``clip`` values correspondingly for the initial guesses.
Raises:
ValueError: if the variable trials has a value smaller than 1.
"""
super().__init__()
if trials < 1:
raise ValueError(f"Number of trials should be 1 or higher, but was {trials}")
self._trials = trials
self._clip = clip
[docs] def multi_start_solve(
self,
minimize: Callable[[np.ndarray], Tuple[np.ndarray, Any]],
problem: QuadraticProgram,
) -> OptimizationResult:
"""Applies a multi start method given a local optimizer.
Args:
minimize: A callable object that minimizes the problem specified
problem: A problem to solve
Returns:
The result of the multi start algorithm applied to the problem.
"""
fval_sol = INFINITY
x_sol: Optional[np.ndarray] = None
rest_sol: Optional[Tuple] = None
# we deal with minimization in the optimizer, so turn the problem to minimization
max2min = MaximizeToMinimize()
original_problem = problem
problem = self._convert(problem, max2min)
# Implementation of multi-start optimizer
for trial in range(self._trials):
x_0 = np.zeros(problem.get_num_vars())
if trial > 0:
for i, var in enumerate(problem.variables):
lowerbound = var.lowerbound if var.lowerbound > -INFINITY else -self._clip
upperbound = var.upperbound if var.upperbound < INFINITY else self._clip
x_0[i] = uniform.rvs(lowerbound, (upperbound - lowerbound))
# run optimization
t_0 = time.time()
x, rest = minimize(x_0)
logger.debug("minimize done in: %s seconds", str(time.time() - t_0))
fval = problem.objective.evaluate(x)
# we minimize the objective
if fval < fval_sol:
fval_sol = fval
x_sol = x
rest_sol = rest
# eventually convert back minimization to maximization
return self._interpret(
x_sol, problem=original_problem, converters=max2min, raw_results=rest_sol
)
@property
def trials(self) -> int:
"""Returns the number of trials for this optimizer.
Returns:
The number of trials.
"""
return self._trials
@trials.setter
def trials(self, trials: int) -> None:
"""Sets the number of trials.
Args:
trials: The number of trials to set.
Raises:
ValueError: if the variable trials have a value smaller than 1.
"""
if trials < 1:
raise ValueError(f"Number of trials should be 1 or higher, but was {trials}")
self._trials = trials
@property
def clip(self) -> float:
"""Returns the clip value for this optimizer.
Returns:
The clip value.
"""
return self._clip
@clip.setter
def clip(self, clip: float) -> None:
"""Sets the clip value.
Args:
clip: The clip value to set.
"""
self._clip = clip