Source code for qiskit.aqua.components.optimizers.optimizer

# -*- coding: utf-8 -*-

# This code is part of Qiskit.
#
# (C) Copyright IBM 2018, 2020.
#
# 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.

"""Optimizer interface"""

from enum import IntEnum
import logging
from abc import ABC, abstractmethod
import numpy as np

logger = logging.getLogger(__name__)

# pylint: disable=invalid-name


[docs]class Optimizer(ABC): """Base class for optimization algorithm.""" class SupportLevel(IntEnum): """ Support Level enum """ not_supported = 0 # Does not support the corresponding parameter in optimize() ignored = 1 # Feature can be passed as non None but will be ignored supported = 2 # Feature is supported required = 3 # Feature is required and must be given, None is invalid @abstractmethod def __init__(self): """ Initialize the optimization algorithm, setting the support level for _gradient_support_level, _bound_support_level, _initial_point_support_level, and empty options. """ self._gradient_support_level = self.get_support_level()['gradient'] self._bounds_support_level = self.get_support_level()['bounds'] self._initial_point_support_level = self.get_support_level()['initial_point'] self._options = {} self._max_evals_grouped = 1
[docs] @abstractmethod def get_support_level(self): """ Return support level dictionary """ raise NotImplementedError
[docs] def set_options(self, **kwargs): """ Sets or updates values in the options dictionary. The options dictionary may be used internally by a given optimizer to pass additional optional values for the underlying optimizer/optimization function used. The options dictionary may be initially populated with a set of key/values when the given optimizer is constructed. Args: kwargs (dict): options, given as name=value. """ for name, value in kwargs.items(): self._options[name] = value logger.debug('options: %s', self._options)
[docs] @staticmethod def gradient_num_diff(x_center, f, epsilon, max_evals_grouped=1): """ We compute the gradient with the numeric differentiation in the parallel way, around the point x_center. Args: x_center (ndarray): point around which we compute the gradient f (func): the function of which the gradient is to be computed. epsilon (float): the epsilon used in the numeric differentiation. max_evals_grouped (int): max evals grouped Returns: grad: the gradient computed """ forig = f(*((x_center,))) grad = [] ei = np.zeros((len(x_center),), float) todos = [] for k in range(len(x_center)): ei[k] = 1.0 d = epsilon * ei todos.append(x_center + d) ei[k] = 0.0 counter = 0 chunk = [] chunks = [] length = len(todos) # split all points to chunks, where each chunk has batch_size points for i in range(length): x = todos[i] chunk.append(x) counter += 1 # the last one does not have to reach batch_size if counter == max_evals_grouped or i == length - 1: chunks.append(chunk) chunk = [] counter = 0 for chunk in chunks: # eval the chunks in order parallel_parameters = np.concatenate(chunk) todos_results = f(parallel_parameters) # eval the points in a chunk (order preserved) if isinstance(todos_results, float): grad.append((todos_results - forig) / epsilon) else: for todor in todos_results: grad.append((todor - forig) / epsilon) return np.array(grad)
[docs] @staticmethod def wrap_function(function, args): """ Wrap the function to implicitly inject the args at the call of the function. Args: function (func): the target function args (tuple): the args to be injected Returns: function_wrapper: wrapper """ def function_wrapper(*wrapper_args): return function(*(wrapper_args + args)) return function_wrapper
@property def setting(self): """ Return setting """ ret = "Optimizer: {}\n".format(self.__class__.__name__) params = "" for key, value in self.__dict__.items(): if key[0] == "_": params += "-- {}: {}\n".format(key[1:], value) ret += "{}".format(params) return ret
[docs] @abstractmethod def optimize(self, num_vars, objective_function, gradient_function=None, variable_bounds=None, initial_point=None): """ Perform optimization. Args: num_vars (int) : Number of parameters to be optimized. objective_function (callable) : A function that computes the objective function. gradient_function (callable) : A function that computes the gradient of the objective function, or None if not available. variable_bounds (list[(float, float)]) : List of variable bounds, given as pairs (lower, upper). None means unbounded. initial_point (numpy.ndarray[float]) : Initial point. Returns: point, value, nfev point: is a 1D numpy.ndarray[float] containing the solution value: is a float with the objective function value nfev: number of objective function calls made if available or None Raises: ValueError: invalid input """ if initial_point is not None and len(initial_point) != num_vars: raise ValueError('Initial point does not match dimension') if variable_bounds is not None and len(variable_bounds) != num_vars: raise ValueError('Variable bounds not match dimension') has_bounds = False if variable_bounds is not None: # If *any* value is *equal* in bounds array to None then the does *not* have bounds has_bounds = not np.any(np.equal(variable_bounds, None)) if gradient_function is None and self.is_gradient_required: raise ValueError('Gradient is required but None given') if not has_bounds and self.is_bounds_required: raise ValueError('Variable bounds is required but None given') if initial_point is None and self.is_initial_point_required: raise ValueError('Initial point is required but None given') if gradient_function is not None and self.is_gradient_ignored: logger.debug( 'WARNING: %s does not support gradient function. It will be ignored.', self.__class__.__name__) if has_bounds and self.is_bounds_ignored: logger.debug( 'WARNING: %s does not support bounds. It will be ignored.', self.__class__.__name__) if initial_point is not None and self.is_initial_point_ignored: logger.debug( 'WARNING: %s does not support initial point. It will be ignored.', self.__class__.__name__) pass
@property def gradient_support_level(self): """ Returns gradient support level """ return self._gradient_support_level @property def is_gradient_ignored(self): """ Returns is gradient ignored """ return self._gradient_support_level == self.SupportLevel.ignored @property def is_gradient_supported(self): """ Returns is gradient supported """ return self._gradient_support_level != self.SupportLevel.not_supported @property def is_gradient_required(self): """ Returns is gradient required """ return self._gradient_support_level == self.SupportLevel.required @property def bounds_support_level(self): """ Returns bounds support level """ return self._bounds_support_level @property def is_bounds_ignored(self): """ Returns is bounds ignored """ return self._bounds_support_level == self.SupportLevel.ignored @property def is_bounds_supported(self): """ Returns is bounds supported """ return self._bounds_support_level != self.SupportLevel.not_supported @property def is_bounds_required(self): """ Returns is bounds required """ return self._bounds_support_level == self.SupportLevel.required @property def initial_point_support_level(self): """ Returns initial point support level """ return self._initial_point_support_level @property def is_initial_point_ignored(self): """ Returns is initial point ignored """ return self._initial_point_support_level == self.SupportLevel.ignored @property def is_initial_point_supported(self): """ Returns is initial point supported """ return self._initial_point_support_level != self.SupportLevel.not_supported @property def is_initial_point_required(self): """ Returns is initial point required """ return self._initial_point_support_level == self.SupportLevel.required
[docs] def print_options(self): """Print algorithm-specific options.""" for name in sorted(self._options): logger.debug('{:s} = {:s}'.format(name, str(self._options[name])))
[docs] def set_max_evals_grouped(self, limit): """ Set max evals grouped """ self._max_evals_grouped = limit