# Copyright 2019 Entropica Labs # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """ Different parametrizations of QAOA circuits. This module holds an abstract class to store QAOA parameters in and (so far) five derived classes that allow for more or less degrees of freedom in the QAOA Ansatze Todo ---- - Better default values for ``time`` if ``None`` is passed - Better default parameters for ``fourier`` n_steps - implement ``AbstractParams.linear_ramp_from_hamiltonian()`` and then super() from it """ import copy from typing import Iterable, Union, List, Tuple, Any, Type, Callable import warnings import math from custom_inherit import DocInheritMeta import matplotlib.pyplot as plt import numpy as np from scipy.fftpack import dct, dst from pyquil.paulis import PauliSum # import entropica_qaoa.qaoa._parameter_conversions.converter as converter def _is_iterable_empty(in_iterable): if isinstance(in_iterable, Iterable): # Is Iterable return all(map(_is_iterable_empty, in_iterable)) return False # Not an Iterable class shapedArray(object): """Decorator-Descriptor for arrays that have a fixed shape. This is used to facilitate automatic input checking for all the different internal parameters. Each instance of this class can be removed without replacement and the code should still work, provided the user provides only correct angles to below parameter classes Parameters ---------- shape: Callable[[Any], Tuple]: Returns the shape for self.values Example ------- With this descriptor, the two following are equivalent: .. code-block:: python class foo(): def __init__(self): self.shape = (n, m) self._my_attribute = None @property def my_attribute(self): return _my_attribute @my_attribute.setter def my_attribute(self): try: self._my_attribute = np.reshape(values, self.shape) except ValueError: raise ValueError("my_attribute must have shape " f"{self.shape}") can be simplified to .. code-block:: python class foo(): def __init__(self): self.shape = (n, m) @shapedArray def my_attribute(self): return self.shape """ def __init__(self, shape: Callable[[Any], Tuple]): """The constructor. See class documentation""" self.name = shape.__name__ self.shape = shape def __set__(self, obj, values): """The setter with input checking.""" try: setattr(obj, f"__{self.name}", np.reshape(values, self.shape(obj))) except ValueError: raise ValueError(f"{self.name} must have shape {self.shape(obj)}") def __get__(self, obj, objtype): """The getter.""" return getattr(obj, f"__{self.name}") class AbstractParams(metaclass=DocInheritMeta(style="numpy")): """ An abstract class to hold the parameters of a QAOA run and compute the angles from them. Parameters ---------- hyperparameters: The hyperparameters containing the hamiltonian, the number of steps and possibly more (e.g. the total annealing time). ``hyperparametesr = (hamiltonian, n_steps, ...)`` parameters: Tuple The QAOA parameters, that can be optimized. E.g. the gammas and betas or the annealing timesteps. AbstractParams doesn't implement this, but all child classes do. Attributes ---------- pair_qubit_coeffs : np.array Coefficients of the pair terms in the hamiltonian qubits_pairs : List List of the qubit pairs in the hamiltonian. Same ordering as `pair_qubit_coeffs` single_qubit_coeffs : np.array Coefficients of the bias terms qubits_singles : List List of the bias qubits in the hamiltonian. Same ordering as `single_qubit_coeffs` reg : List List of all qubits for the X-rotations n_steps : int Number of timesteps. Corresponds to p_steps in the literature """ # pylint: disable=too-many-instance-attributes def __init__(self, hyperparameters: Tuple): # Note # ---- # ``AbstractParams.__init__`` doesn't do anything with the # argument ``parameters``. In the child classes we have to super from # them and handle them correctly. Additionally we might need to handle # extra hyperparameters. hamiltonian, self.n_steps = hyperparameters[:2] # extract the qubit lists from the hamiltonian self.reg = hamiltonian.get_qubits() self.qubits_pairs = [] self.qubits_singles = [] # and fill qubits_singles and qubits_pairs according to the terms in the hamiltonian for term in hamiltonian: if len(term) == 1: self.qubits_singles.append(term.get_qubits()[0]) elif len(term) == 2: self.qubits_pairs.append(term.get_qubits()) elif len(term) == 0: pass # could give a notice, that multiples of the identity are # ignored, since unphysical else: raise NotImplementedError( "As of now we can only handle hamiltonians with at most" " two-qubit terms") # extract the cofficients of the terms from the hamiltonian # if creating `ExtendedParams` form this, we can delete this attributes # again. Check if there are complex coefficients and issue a warning. self.single_qubit_coeffs = np.array([ term.coefficient for term in hamiltonian if len(term) == 1]) if np.any(np.iscomplex(self.single_qubit_coeffs)): warnings.warn("hamiltonian contained complex coefficients." " Ignoring imaginary parts") self.single_qubit_coeffs = self.single_qubit_coeffs.real # and the same for the pair qubit coefficients self.pair_qubit_coeffs = np.array([ term.coefficient for term in hamiltonian if len(term) == 2]) if np.any(np.iscomplex(self.pair_qubit_coeffs)): warnings.warn("hamiltonian contained complex coefficients." " Ignoring imaginary parts") self.pair_qubit_coeffs = self.pair_qubit_coeffs.real def __repr__(self): """Return an overview over the parameters and hyperparameters Todo ---- Split this into ``__repr__`` and ``__str__`` with a more verbose output in ``__repr__``. """ string = "Hyperparameters:\n" string += "\tregister: " + str(self.reg) + "\n" string += "\tqubits_singles: " + str(self.qubits_singles) + "\n" string += "\tsingle_qubit_coeffs: " + str(self.single_qubit_coeffs) + "\n" string += "\tqubits_pairs: " + str(self.qubits_pairs) + "\n" string += "\tpair_qubit_coeffs: " + str(self.pair_qubit_coeffs) + "\n" string += "\tn_steps: " + str(self.n_steps) + "\n" return string def __len__(self): """ Returns ------- int: the length of the data produced by self.raw() and accepted by self.update_from_raw() """ raise NotImplementedError() @property def x_rotation_angles(self): """2D array with the X-rotation angles. 1st index goes over n_steps and the 2nd index over the qubits to apply X-rotations on. These are needed by ``qaoa.cost_function.make_qaoa_memory_map`` """ raise NotImplementedError @property def z_rotation_angles(self): """2D array with the ZZ-rotation angles. 1st index goes over the n_steps and the 2nd index over the qubit pairs, to apply ZZ-rotations on. These are needed by ``qaoa.cost_function.make_qaoa_memory_map`` """ raise NotImplementedError @property def zz_rotation_angles(self): """2D array with Z-rotation angles. 1st index goes over the n_steps and the 2nd index over the qubit pairs, to apply Z-rotations on. These are needed by ``qaoa.cost_function.make_qaoa_memory_map`` """ raise NotImplementedError def update_from_raw(self, new_values: Union[list, np.array]): """ Update all the parameters from a 1D array. The input has the same format as the output of ``self.raw()``. This is useful for ``scipy.optimize.minimize`` which expects the parameters that need to be optimized to be a 1D array. Parameters ---------- new_values: A 1D array with the new parameters. Must have length ``len(self)`` and the ordering of the flattend ``parameters`` in ``__init__()``. """ raise NotImplementedError() def raw(self): """ Return the parameters in a 1D array. This 1D array is needed by ``scipy.optimize.minimize`` which expects the parameters that need to be optimized to be a 1D array. Returns ------- np.array: The parameters in a 1D array. Has the same output format as the expected input of ``self.update_from_raw``. Hence corresponds to the flattened `parameters` in `__init__()` """ raise NotImplementedError() def raw_rotation_angles(self): """ Flat array of the rotation angles for the memory map for the parametric circuit. Returns ------- np.array: Returns all single rotation angles in the ordering ``(x_rotation_angles, gamma_singles, zz_rotation_angles)`` where ``x_rotation_angles = (beta_q0_t0, beta_q1_t0, ... , beta_qn_tp)`` and the same for ``z_rotation_angles`` and ``zz_rotation_angles`` """ raw_data = np.concatenate((self.x_rotation_angles.flatten(), self.z_rotation_angles.flatten(), self.zz_rotation_angles.flatten())) return raw_data @classmethod def linear_ramp_from_hamiltonian(cls, hamiltonian: PauliSum, n_steps: int, time: float = None): """Alternative to ``__init__`` that already fills ``parameters``. Calculate initial parameters from a hamiltonian corresponding to a linear ramp annealing schedule and return a ``QAOAParams`` object. Parameters ---------- hamiltonian: ``hamiltonian`` for which to calculate the initial QAOA parameters. n_steps: Number of timesteps. time: Total annealing time. Defaults to ``0.7*n_steps``. Returns ------- Type[AbstractParams] The initial parameters for a linear ramp for ``hamiltonian``. """ raise NotImplementedError() @classmethod def from_AbstractParameters(cls, abstract_params, parameter: Tuple =None): """Alternative to ``__init__`` that takes ``hyperparameters`` from an existing ``AbstractQAOAParameter`` object. Create a ConcreteQAOAParameters instance from an AbstractQAOAParameters instance with hyperparameters from ``abstract_params`` and normal parameters from ``parameters`` Parameters ---------- abstract_params: AbstractParams An AbstractQAOAParameters instance to which to add the parameters parameter: A tuple containing the parameters. Must be the same as in ``__init__`` Returns ------- AbstractParams A copy of itself. This function makes only sense for the child classes of ``AbstractQAOAParameters`` """ params = copy.deepcopy(abstract_params) params.__class__ = cls return params @classmethod def from_other_parameters(cls, params): """Alternative to ``__init__`` that takes parameters with less degrees of freedom as the input. Parameters ---------- params: Type[AbstractParams] The input parameters Returns ------- Type[AbstractParams]: The converted paramters s.t. all the rotation angles of the in and output parameters are the same. Note ---- If all conversion functions for your parametrization are in qaoa._parameter_conversions.py and correctly registered for converter you don't need to override this function in its child classes """ # Todo: Somehow fix this deferred import w/o creating cyclic imports? from entropica_qaoa.qaoa._parameter_conversions import converter return converter(params, cls) @classmethod def empty(cls, hyperparameters: tuple): """Alternative to ``__init__`` that only takes ``hyperparameters`` and fills ``parameters`` via ``np.empty`` Parameters ---------- hyperparameters: Same as the hyperparameters argument for ``cls.__init__()`` Returns ------- Type[AbstractParams] A Parameter object with the parameters filled by ``np.empty`` """ self = AbstractParams(hyperparameters) self.__class__ = cls return self def plot(self, ax=None, **kwargs): """ Plots ``self`` in a sensible way to the canvas ``ax``, if provided. Parameters ---------- ax: matplotlib.axes._subplots.AxesSubplot The canvas to plot itself on kwargs: All remaining keyword arguments are passed forward to the plot function """ raise NotImplementedError() class ExtendedParams(AbstractParams): r""" QAOA parameters in their most general form with different angles for each operator. This means, that at the i-th timestep the evolution hamiltonian is given by .. math:: H(t_i) = \sum_{\textrm{qubits } j} \beta_{ij} X_j + \sum_{\textrm{qubits } j} \gamma_{\textrm{single } ij} Z_j + \sum_{\textrm{qubit pairs} (jk)} \gamma_{\textrm{pair } i(jk)} Z_j Z_k and the complete circuit is then .. math:: U = e^{-i H(t_p)} \cdots e^{-iH(t_1)}. Parameters ---------- hyperparameters: The hyperparameters containing the hamiltonian and the number of steps ``hyperparameters = (hamiltonian, n_steps)`` parameters: Tuple containing ``(betas, gammas_singles, gammas_pairs)`` with dimensions ``((n_steps x nqubits), (n_steps x nsingle_terms), (n_steps x npair_terms))`` Attributes ---------- betas: np.array 2D array with the gammas from above for each timestep and qubit. 1st index goes over the timesteps, 2nd over the qubits. gammas_pairs: np.array 2D array with the gammas_pairs from above for each timestep and coupled pair. 1st index goes over timesteps, 2nd over pairs. gammas_singles: np.array 2D array with the gammas_singles from above for each timestep and bias term. 1st index goes over timesteps, 2nd over bias terms. """ def __init__(self, hyperparameters: Tuple[PauliSum, int], parameters: Tuple): # setup reg, qubits_singles and qubits_pairs super().__init__(hyperparameters) # and add the parameters self.betas, self.gammas_singles, self.gammas_pairs\ = np.array(parameters[0]), np.array(parameters[1]), np.array(parameters[2]) def __repr__(self): string = "Hyperparameters:\n" string += "\tregister: " + str(self.reg) + "\n" string += "\tqubits_singles: " + str(self.qubits_singles) + "\n" string += "\tqubits_pairs: " + str(self.qubits_pairs) + "\n" string += "Parameters:\n" string += "\tbetas: " + str(self.betas).replace("\n", ",") + "\n" string += "\tgammas_singles: " + str(self.gammas_singles)\ .replace("\n", ",") + "\n" string += "\tgammas_pairs: " + str(self.gammas_pairs)\ .replace("\n", ",") + "\n" return string def __len__(self): return self.n_steps * (len(self.reg) + len(self.qubits_pairs) + len(self.qubits_singles)) @shapedArray def betas(self): return (self.n_steps, len(self.reg)) @shapedArray def gammas_singles(self): return (self.n_steps, len(self.qubits_singles)) @shapedArray def gammas_pairs(self): return (self.n_steps, len(self.qubits_pairs)) @property def x_rotation_angles(self): return self.betas @property def z_rotation_angles(self): return self.single_qubit_coeffs * self.gammas_singles @property def zz_rotation_angles(self): return self.pair_qubit_coeffs * self.gammas_pairs def update_from_raw(self, new_values): self.betas = np.array(new_values[:len(self.reg) * self.n_steps]) self.betas = self.betas.reshape((self.n_steps, len(self.reg))) new_values = new_values[self.n_steps * len(self.reg):] self.gammas_singles = np.array(new_values[:len(self.qubits_singles) * self.n_steps]) self.gammas_singles = self.gammas_singles.reshape( (self.n_steps, len(self.qubits_singles))) new_values = new_values[self.n_steps * len(self.qubits_singles):] self.gammas_pairs = np.array(new_values[:len(self.qubits_pairs) * self.n_steps]) self.gammas_pairs = self.gammas_pairs.reshape( (self.n_steps, len(self.qubits_pairs))) new_values = new_values[self.n_steps * len(self.qubits_pairs):] # PEP8 complains, but new_values could be np.array and not list! if not len(new_values) == 0: raise RuntimeWarning( "list to make new gammas and x_rotation_angles out of didn't" "have the right length!") def raw(self): raw_data = np.concatenate((self.betas.flatten(), self.gammas_singles.flatten(), self.gammas_pairs.flatten())) return raw_data @classmethod def linear_ramp_from_hamiltonian(cls, hamiltonian: PauliSum, n_steps: int, time: float = None): """ Returns ------- ExtendedParams The initial parameters according to a linear ramp for ``hamiltonian``. Todo ---- Refactor this s.t. it supers from __init__ """ # create evenly spaced timesteps at the centers of n_steps intervals if time is None: time = float(0.7 * n_steps) dt = time / n_steps term_lengths = [len(t) for t in hamiltonian] n_sing = term_lengths.count(1) n_pairs = term_lengths.count(2) n_betas = len(hamiltonian.get_qubits()) has_higher_order_terms = any(l > 2 for l in term_lengths) if has_higher_order_terms: raise NotImplementedError( "As of now we can only handle hamiltonians with at most " "two-qubit terms") betas = np.linspace((dt / time) * (time * (1 - 0.5 / n_steps)), (dt / time) * (time * 0.5 / n_steps), n_steps) gammas_singles = betas[::-1] gammas_pairs = betas[::-1] betas = betas.repeat(n_betas).reshape(n_steps, n_betas) gammas_singles = gammas_singles.repeat(n_sing).reshape(n_steps, n_sing) gammas_pairs = gammas_pairs.repeat(n_pairs).reshape(n_steps, n_pairs) # wrap it all nicely in a qaoa_parameters object params = cls((hamiltonian, n_steps), (betas, gammas_singles, gammas_pairs)) return params @classmethod def from_AbstractParameters(cls, abstract_params: AbstractParams, parameters: Tuple) -> AbstractParams: """ Returns ------- ExtendedParams A ``GeneralQAOAParameters`` object with the hyperparameters taken from ``abstract_params`` and the normal parameters from ``parameters`` """ out = super().from_AbstractParameters(abstract_params) out.betas, out.gammas_singles, out.gammas_pairs\ = np.array(parameters[0]), np.array(parameters[1]), np.array(parameters[2]) return out @classmethod def empty(cls, hyperparameters): self = super().empty(hyperparameters) (self.betas, self.gammas_singles, self.gammas_pairs)\ = (np.empty((self.n_steps, len(self.reg))), np.empty((self.n_steps, len(self.qubits_singles))), np.empty((self.n_steps, len(self.qubits_pairs)))) return self def get_constraints(self): """Constraints on the parameters for constrained parameters. Returns ------- List[Tuple]: A list of tuples (0, upper_boundary) of constraints on the parameters s.t. we are exploiting the periodicity of the cost function. Useful for constrained optimizers. """ beta_constraints = [(0, 2 * math.pi)] * len(self.betas.flatten()) gamma_pair_constraints = [(0, 2 * math.pi / w) for w in self.pair_qubit_coeffs] gamma_pair_constraints *= self.n_steps gamma_single_constraints = [(0, 2 * math.pi / w) for w in self.single_qubit_coeffs] gamma_single_constraints *= self.n_steps all_constraints = beta_constraints + gamma_single_constraints\ + gamma_pair_constraints return all_constraints def plot(self, ax=None, **kwargs): if ax is None: fig, ax = plt.subplots() ax.plot(self.betas, label="betas", marker="s", ls="", **kwargs) if not _is_iterable_empty(self.gammas_singles): ax.plot(self.gammas_singles, label="gammas_singles", marker="^", ls="", **kwargs) if not _is_iterable_empty(self.gammas_pairs): ax.plot(self.gammas_pairs, label="gammas_pairs", marker="v", ls="", **kwargs) ax.set_xlabel("timestep") ax.legend() class StandardWithBiasParams(AbstractParams): r""" QAOA parameters that implement a state preparation circuit with .. math:: e^{-i \beta_p H_0} e^{-i \gamma_{\textrm{singles}, p} H_{c, \textrm{singles}}} e^{-i \gamma_{\textrm{pairs}, p} H_{c, \textrm{pairs}}} \cdots e^{-i \beta_0 H_0} e^{-i \gamma_{\textrm{singles}, 0} H_{c, \textrm{singles}}} e^{-i \gamma_{\textrm{pairs}, 0} H_{c, \textrm{pairs}}} where the cost hamiltonian is split into :math:`H_{c, \textrm{singles}}` the bias terms, that act on only one qubit, and :math:`H_{c, \textrm{pairs}}` the coupling terms, that act on two qubits. Parameters ---------- hyperparameters: The hyperparameters containing the hamiltonian and the number of steps ``hyperparameters = (hamiltonian, n_steps)`` parameters: Tuple containing ``(betas, gammas_singles, gammas_pairs)`` with dimensions ``(n_steps, n_steps, n_steps)`` Attributes ---------- betas: np.array A 1D array containing the betas from above for each timestep gammas_pairs: np.array A 1D array containing the gammas_singles from above for each timestep gammas_singles: np.array A 1D array containing the gammas_pairs from above for each timestep """ def __init__(self, hyperparameters: Tuple[PauliSum, int], parameters: Tuple): # setup reg, qubits_singles and qubits_pairs super().__init__(hyperparameters) self.betas, self.gammas_singles, self.gammas_pairs\ = np.array(parameters[0]), np.array(parameters[1]), np.array(parameters[2]) def __repr__(self): string = "Hyperparameters:\n" string += "\tregister: " + str(self.reg) + "\n" string += "\tqubits_singles: " + str(self.qubits_singles) + "\n" string += "\tqubits_pairs: " + str(self.qubits_pairs) + "\n" string += "Parameters:\n" string += "\tbetas: " + str(self.betas) + "\n" string += "\tgammas_singles: " + str(self.gammas_singles) + "\n" string += "\tgammas_pairs: " + str(self.gammas_pairs) + "\n" return(string) def __len__(self): return self.n_steps * 3 @shapedArray def betas(self): return self.n_steps @shapedArray def gammas_singles(self): return self.n_steps @shapedArray def gammas_pairs(self): return self.n_steps @property def x_rotation_angles(self): return np.outer(self.betas, np.ones(len(self.reg))) @property def z_rotation_angles(self): return np.outer(self.gammas_singles, self.single_qubit_coeffs) @property def zz_rotation_angles(self): return np.outer(self.gammas_pairs, self.pair_qubit_coeffs) def update_from_raw(self, new_values): # overwrite self.betas with new ones self.betas = np.array(new_values[0:self.n_steps]) new_values = new_values[self.n_steps:] # cut betas from new_values self.gammas_singles = np.array(new_values[0:self.n_steps]) new_values = new_values[self.n_steps:] self.gammas_pairs = np.array(new_values[0:self.n_steps]) new_values = new_values[self.n_steps:] if not len(new_values) == 0: raise RuntimeWarning("list to make new gammas and x_rotation_angles out of" "didn't have the right length!") def raw(self): raw_data = np.concatenate((self.betas, self.gammas_singles, self.gammas_pairs)) return raw_data @classmethod def linear_ramp_from_hamiltonian(cls, hamiltonian: PauliSum, n_steps: int, time: float = None): """ Returns ------- StandardWithBiasParams An `AlternatingOperatorsQAOAParameters` object holding all the parameters """ if time is None: time = float(0.7 * n_steps) dt = time / n_steps # fill betas, gammas_singles and gammas_pairs betas = np.linspace((dt / time) * (time * (1 - 0.5 / n_steps)), (dt / time) * (time * 0.5 / n_steps), n_steps) gammas_singles = betas[::-1] gammas_pairs = betas[::-1] # wrap it all nicely in a qaoa_parameters object params = cls((hamiltonian, n_steps), (betas, gammas_singles, gammas_pairs)) return params @classmethod def from_AbstractParameters(cls, abstract_params: AbstractParams, parameters: Tuple): """ Returns ------- StandardWithBiasParams An ``AlternatatingOperatorsQAOAParameters`` object with the hyperparameters taken from ``abstract_params`` and the normal parameters from ``parameters`` """ out = super().from_AbstractParameters(abstract_params) out.betas, out.gammas_singles, out.gammas_pairs\ = np.array(parameters[0]), np.array(parameters[1]), np.array(parameters[2]) return out @classmethod def empty(cls, hyperparameters): self = super().empty(hyperparameters) (self.betas, self.gammas_singles, self.gammas_pairs)\ = (np.empty((self.n_steps)), np.empty((self.n_steps)), np.empty((self.n_steps))) return self def plot(self, ax=None, **kwargs): if ax is None: fig, ax = plt.subplots() ax.plot(self.betas, label="betas", marker="s", ls="", **kwargs) if not _is_iterable_empty(self.gammas_singles): ax.plot(self.gammas_singles, label="gammas_singles", marker="^", ls="", **kwargs) if not _is_iterable_empty(self.gammas_pairs): ax.plot(self.gammas_pairs, label="gammas_pairs", marker="v", ls="", **kwargs) ax.set_xlabel("timestep") # ax.grid(linestyle='--') ax.legend() class StandardParams(AbstractParams): r""" QAOA parameters that implement a state preparation circuit with .. math:: e^{-i \beta_p H_0} e^{-i \gamma_p H_c} \cdots e^{-i \beta_0 H_0} e^{-i \gamma_0 H_c} This corresponds to the parametrization used by Farhi in his original paper [https://arxiv.org/abs/1411.4028] Parameters ---------- hyperparameters: The hyperparameters containing the hamiltonian and the number of steps ``hyperparameters = (hamiltonian, n_steps)`` parameters: Tuple containing ``(betas, gammas)`` with dimensions ``(n_steps, n_steps)`` Attributes ---------- betas: np.array 1D array with the betas from above gammas: np.array 1D array with the gamma from above """ def __init__(self, hyperparameters: Tuple[PauliSum, int], parameters: Tuple): # setup reg, qubits_singles and qubits_pairs super().__init__(hyperparameters) self.betas, self.gammas\ = np.array(parameters[0]), np.array(parameters[1]) def __repr__(self): string = "Hyperparameters:\n" string += "\tregister: " + str(self.reg) + "\n" string += "\tqubits_singles: " + str(self.qubits_singles) + "\n" string += "\tqubits_pairs: " + str(self.qubits_pairs) + "\n" string += "Parameters:\n" string += "\tbetas: " + str(self.betas) + "\n" string += "\tgammas: " + str(self.gammas) + "\n" return(string) def __len__(self): return self.n_steps * 2 @shapedArray def betas(self): return self.n_steps @shapedArray def gammas(self): return self.n_steps @property def x_rotation_angles(self): return np.outer(self.betas, np.ones(len(self.reg))) @property def z_rotation_angles(self): return np.outer(self.gammas, self.single_qubit_coeffs) @property def zz_rotation_angles(self): return np.outer(self.gammas, self.pair_qubit_coeffs) def update_from_raw(self, new_values): # overwrite self.betas with new ones self.betas = np.array(new_values[0:self.n_steps]) new_values = new_values[self.n_steps:] # cut betas from new_values self.gammas = np.array(new_values[0:self.n_steps]) new_values = new_values[self.n_steps:] if not len(new_values) == 0: raise RuntimeWarning("list to make new gammas and x_rotation_angles out of" "didn't have the right length!") def raw(self): raw_data = np.concatenate((self.betas, self.gammas)) return raw_data @classmethod def linear_ramp_from_hamiltonian(cls, hamiltonian: PauliSum, n_steps: int, time: float = None): """ Returns ------- StandardParams A ``StandardParams`` object with parameters according to a linear ramp schedule for ``hamiltonian`` """ if time is None: time = float(0.7 * n_steps) # create evenly spaced timesteps at the centers of n_steps intervals dt = time / n_steps # fill betas, gammas_singles and gammas_pairs betas = np.linspace((dt / time) * (time * (1 - 0.5 / n_steps)), (dt / time) * (time * 0.5 / n_steps), n_steps) gammas = betas[::-1] # wrap it all nicely in a qaoa_parameters object params = cls((hamiltonian, n_steps), (betas, gammas)) return params @classmethod def from_AbstractParameters(cls, abstract_params: AbstractParams, parameters: Tuple): """ Returns ------- StandardParams A ``StandardParams`` object with the hyperparameters taken from ``abstract_params`` and the normal parameters from ``parameters`` """ out = super().from_AbstractParameters(abstract_params) out.betas, out.gammas\ = np.array(parameters[0]), np.array(parameters[1]) return out @classmethod def empty(cls, hyperparameters): self = super().empty(hyperparameters) (self.betas, self.gammas) = (np.empty((self.n_steps)), np.empty((self.n_steps))) return self def plot(self, ax=None, **kwargs): if ax is None: fig, ax = plt.subplots() ax.plot(self.betas, label="betas", marker="s", ls="", **kwargs) ax.plot(self.gammas, label="gammas", marker="^", ls="", **kwargs) ax.set_xlabel("timestep", fontsize=12) ax.legend() class AnnealingParams(AbstractParams): """ QAOA parameters that implement a state preparation circuit of the form .. math:: U = e^{-i (1-s(t_p)) H_M \Delta t} e^{-i s(t_p) H_C \Delta t} \cdots e^{-i(1-s(t_1)H_M \Delta t} e^{-i s(t_1) H_C \Delta t} where the :math:`s(t_i) =: s_i` are the variable parameters and :math:`\\Delta t= \\frac{T}{N}`. So the schedule is specified by specifying s(t) at evenly spaced timesteps. Parameters ---------- hyperparameters: The hyperparameters containing the hamiltonian, the number of steps and the total annealing time ``hyperparameters = (hamiltonian, n_steps, time)`` parameters : Tuple Tuple containing ``(schedule values)`` of length ``n_steps`` Attributes ---------- schedule: np.array An 1D array holding the values of the schedule function at each timestep. """ def __init__(self, hyperparameters: Tuple[PauliSum, int, float], parameters: List): # setup reg, qubits_singles and qubits_pairs super().__init__(hyperparameters) self._annealing_time = hyperparameters[2] self.schedule = np.array(parameters) def __repr__(self): string = "Hyperparameters:\n" string += "\tregister: " + str(self.reg) + "\n" string += "\tqubits_singles: " + str(self.qubits_singles) + "\n" string += "\tqubits_pairs: " + str(self.qubits_pairs) + "\n" string += "Parameters:\n" string += "\tschedule: " + str(self.schedule) return string def __len__(self): return self.n_steps @shapedArray def schedule(self): return self.n_steps @property def x_rotation_angles(self): dt = self._annealing_time / self.n_steps tmp = (1 - self.schedule) * dt return np.outer(tmp, np.ones(len(self.reg))) @property def z_rotation_angles(self): dt = self._annealing_time / self.n_steps tmp = self.schedule * dt return np.outer(tmp, self.single_qubit_coeffs) @property def zz_rotation_angles(self): dt = self._annealing_time / self.n_steps tmp = self.schedule * dt return np.outer(tmp, self.pair_qubit_coeffs) def update_from_raw(self, new_values): if len(new_values) != self.n_steps: raise RuntimeWarning( "the new times should have length n_steps+1") self.schedule = np.array(new_values) def raw(self): return self.schedule @classmethod def linear_ramp_from_hamiltonian(cls, hamiltonian: PauliSum, n_steps: int, time: float = None): """ Returns ------- AnnealingParams : An ``AnnealingParams`` object corresponding to a linear ramp schedule. """ if time is None: time = 0.7 * n_steps schedule = np.linspace(0.5 / n_steps, 1 - 0.5 / n_steps, n_steps) # wrap it all nicely in a qaoa_parameters object params = cls((hamiltonian, n_steps, time), (schedule)) return params @classmethod def from_AbstractParameters(cls, abstract_params: AbstractParams, parameters: Tuple, time: float = None): """ Returns ------- AnnealingParams An ``AnnealingParams`` object with the hyperparameters taken from ``abstract_params`` and the normal parameters from ``parameters`` """ out = super().from_AbstractParameters(abstract_params) if time is None: time = 0.7 * out.n_steps out._annealing_time = time out.schedule = np.array(parameters) return out @classmethod def empty(cls, hyperparameters): self = super().empty(hyperparameters) self._annealing_time = hyperparameters[2] self.schedule = np.empty((self.n_steps)) return self def plot(self, ax=None, **kwargs): if ax is None: fig, ax = plt.subplots() ax.plot(self.schedule, marker="s", **kwargs) ax.set_xlabel("timestep number", fontsize=14) ax.set_ylabel("s(t)", fontsize=14) class FourierParams(AbstractParams): """ The QAOA parameters as the sine/cosine transform of the original gammas and x_rotation_angles. See "Quantum Approximate Optimization Algorithm: Performance, Mechanism, and Implementation on Near-Term Devices" [https://arxiv.org/abs/1812.01041] for a detailed description. Parameters ---------- hyperparameters: The hyperparameters containing the hamiltonian, the number of steps and the total annealing time ``hyperparameters = (hamiltonian, n_steps, q)``. ``q`` is the number of fourier coefficients. For ``q == n_steps`` we have the full expressivity of ``StandardParams``. More are redundant. parameters: Tuple containing ``(v, u)`` with dimensions ``(q, q)`` Attributes ---------- q : int The number of coefficients for the discrete sine and cosine transforms below u : np.array The discrete sine transform of the ``gammas`` in ``StandardParams`` v : np.array The discrete cosine transform of the ``betas`` in ``StandardParams`` """ def __init__(self, hyperparameters: Tuple[PauliSum, int, float], parameters: Tuple): # setup reg, qubits_singles and qubits_pairs super().__init__(hyperparameters) self.q = hyperparameters[2] self.v, self.u = parameters def __repr__(self): string = "Hyperparameters:\n" string += "\tregister: " + str(self.reg) + "\n" string += "\tqubits_singles" + str(self.qubits_singles) + "\n" string += "\tqubits_pairs" + str(self.qubits_pairs) + "\n" string += "Parameters:\n" string += "\tv: " + str(self.v) + "\n" string += "\tu: " + str(self.u) + "\n" return(string) def __len__(self): return 2 * self.q @shapedArray def v(self): return self.q @shapedArray def u(self): return self.q @property def x_rotation_angles(self): betas = dct(self.v, n=self.n_steps) return np.outer(betas, np.ones(len(self.reg))) @property def z_rotation_angles(self): gammas = dst(self.u, n=self.n_steps) return np.outer(gammas, self.single_qubit_coeffs) @property def zz_rotation_angles(self): gammas = dst(self.u, n=self.n_steps) return np.outer(gammas, self.pair_qubit_coeffs) def update_from_raw(self, new_values): # overwrite x_rotation_angles with new ones self.v = np.array(new_values[0:self.q]) # cut x_rotation_angles from new_values new_values = new_values[self.q:] self.u = np.array(new_values[0:self.q]) new_values = new_values[self.q:] if not len(new_values) == 0: raise RuntimeWarning("list to make new u's and v's out of" "didn't have the right length!") def raw(self): raw_data = np.concatenate((self.v, self.u)) return raw_data @classmethod def linear_ramp_from_hamiltonian(cls, hamiltonian: PauliSum, n_steps: int, q: int = 4, time: float = None): """ NOTE: rather than implement an exact linear schedule, this instead implements the lowest frequency component, i.e. a sine curve for gammas, and a cosine for betas. Parameters ---------- hamiltonian: The hamiltonian n_steps: number of timesteps q: Number of Fourier coefficients. Defaults to 4 time: total time. Set to ``0.7*n_steps`` if ``None`` is passed. Returns ------- FourierParams: A ``FourierParams`` object with initial parameters corresponding to a the 0th order Fourier component (a sine curve for gammas, cosine for betas) ToDo ---- Make a more informed choice of the default value for ``q``. Probably depending on ``n_qubits`` """ if time is None: time = 0.7 * n_steps # fill x_rotation_angles, z_rotation_angles and zz_rotation_angles # Todo make this an easier expresssion v = np.zeros(q) v[0] = 0.5 * time / n_steps u = np.copy(v) # wrap it all nicely in a qaoa_parameters object params = cls((hamiltonian, n_steps, q), (v, u)) return params @classmethod def from_AbstractParameters(cls, abstract_params: AbstractParams, parameters: Tuple, q: int = 4): """ Parameters ---------- abstract_params: An AbstractQAOAParameters instance to which to add the parameters parameters: Same as ``parameters`` in ``.__init__()`` q: Number of fourier coefficients. Defaults to 4 Returns ------- FourierParams A ``FourierParams`` object with the hyperparameters taken from ``abstract_params`` and the normal parameters from ``parameters`` """ out = super().from_AbstractParameters(abstract_params) out.q = q out.v, out.u =\ np.array(parameters[0]), np.array(parameters[1]) return out @classmethod def empty(cls, hyperparameters): self = super().empty(hyperparameters) self.q = hyperparameters[2] self.v, self.u = np.empty((self.q)), np.empty((self.q)) return self def plot(self, ax=None, **kwargs): warnings.warn("Plotting the gammas and x_rotation_angles through DCT " "and DST. If you are interested in v, u you can access " "them via params.v, params.u") if ax is None: fig, ax = plt.subplots() ax.plot(dct(self.v, n=self.n_steps), label="betas", marker="s", ls="", **kwargs) ax.plot(dst(self.u, n=self.n_steps), label="gammas", marker="v", ls="", **kwargs) ax.set_xlabel("timestep") ax.legend() class FourierWithBiasParams(AbstractParams): """ The QAOA parameters as the sine/cosine transform of the original gammas and x_rotation_angles. See "Quantum Approximate Optimization Algorithm: Performance, Mechanism, and Implementation on Near-Term Devices" [https://arxiv.org/abs/1812.01041] for a detailed description. Parameters ---------- hyperparameters: The hyperparameters containing the hamiltonian, the number of steps and the total annealing time ``hyperparameters = (hamiltonian, n_steps, q)``. ``q`` is the number of fourier coefficients. For ``q == n_steps`` we have the full expressivity of ``StandardWithBiasParams``. More are redundant. parameters: Tuple containing ``(v, u_singles, u_pairs)`` with dimensions ``(q, q, q)`` Attributes ---------- q : int The number of coefficients for the discrete sine and cosine transforms below u_pairs : np.array The discrete sine transform of the ``gammas_pairs`` in ``StandardWithBiasParams`` u_singles : np.array The discrete sine transform of the ``gammas_singles`` in ``StandardWithBiasParams`` v : np.array The discrete cosine transform of the betas in ``StandardWithBiasParams`` """ def __init__(self, hyperparameters: Tuple[PauliSum, int, float], parameters: Tuple): # setup reg, qubits_singles and qubits_pairs super().__init__(hyperparameters) self.q = hyperparameters[2] self.v, self.u_singles, self.u_pairs = parameters def __repr__(self): string = "Hyperparameters:\n" string += "\tregister: " + str(self.reg) + "\n" string += "Parameters:\n" string += "\tu_singles: " + str(self.u_singles) + "\n" string += "\tu_pairs: " + str(self.u_pairs) + "\n" string += "\tv: " + str(self.v) + "\n" return(string) def __len__(self): return 3 * self.q @shapedArray def v(self): return self.q @shapedArray def u_singles(self): return self.q @shapedArray def u_pairs(self): return self.q @property def x_rotation_angles(self): betas = dct(self.v, n=self.n_steps) return np.outer(betas, np.ones(len(self.reg))) @property def z_rotation_angles(self): gammas_singles = dst(self.u_singles, n=self.n_steps) return np.outer(gammas_singles, self.single_qubit_coeffs) @property def zz_rotation_angles(self): gammas_pairs = dst(self.u_pairs, n=self.n_steps) return np.outer(gammas_pairs, self.pair_qubit_coeffs) def update_from_raw(self, new_values): # overwrite x_rotation_angles with new ones self.v = np.array(new_values[0:self.q]) # cut x_rotation_angles from new_values new_values = new_values[self.q:] self.u_singles = np.array(new_values[0:self.q]) new_values = new_values[self.q:] self.u_pairs = np.array(new_values[0:self.q]) new_values = new_values[self.q:] if not len(new_values) == 0: raise RuntimeWarning("list to make new u's and v's out of" "didn't have the right length!") def raw(self): raw_data = np.concatenate((self.v, self.u_singles, self.u_pairs)) return raw_data @classmethod def linear_ramp_from_hamiltonian(cls, hamiltonian: PauliSum, n_steps: int, q: int = 4, time: float = None): """ Parameters ---------- hamiltonian: The hamiltonian n_steps: number of timesteps q: Number of Fourier coefficients. Defaults to 4 time: total time. Set to ``0.7*n_steps`` if ``None`` is passed. Returns ------- FourierWithBiasParams: A ``FourierWithBiasParams`` object with initial parameters corresponding to a linear ramp annealing schedule ToDo ---- Make a more informed choice of the default value for ``q``. Probably depending on ``n_qubits`` """ if time is None: time = 0.7 * n_steps v = np.zeros(q) v[0] = 0.5 * time / n_steps u_singles = np.copy(v) u_pairs = np.copy(v) # wrap it all nicely in a qaoa_parameters object params = cls((hamiltonian, n_steps, q), (v, u_singles, u_pairs)) return params @classmethod def from_AbstractParameters(cls, abstract_params: AbstractParams, parameters: Tuple, q: int = 4): """ Parameters ---------- abstract_params: An AbstractQAOAParameters instance to which to add the parameters parameters: Same as ``parameters`` in ``.__init__()`` q: Number of fourier coefficients. Defaults to 4 Returns ------- FourierWithBiasParams A ``FourierWithBiasParams`` object with the hyperparameters taken from ``abstract_params`` and the normal parameters from ``parameters`` """ out = super().from_AbstractParameters(abstract_params) if q is None: q = 4 out.q = q out.v, out.u_singles, out.u_pairs =\ np.array(parameters[0]), np.array(parameters[1]), np.array(parameters[2]) return out @classmethod def empty(cls, hyperparameters): self = super().empty(hyperparameters) self.q = hyperparameters[2] self.v, self.u_singles, self.u_pairs\ = np.empty((self.q)), np.empty((self.q)), np.empty((self.q)) return self def plot(self, ax=None, **kwargs): warnings.warn("Plotting the gammas and x_rotation_angles through DCT " "and DST. If you are interested in v, u_singles and " "u_pairs you can access them via params.v, " "params.u_singles, params.u_pairs") if ax is None: fig, ax = plt.subplots() ax.plot(dct(self.v, n=self.n_steps), label="betas", marker="s", ls="", **kwargs) if not _is_iterable_empty(self.u_singles): ax.plot(dst(self.u_singles, n=self.n_steps), label="gammas_singles", marker="^", ls="", **kwargs) if not _is_iterable_empty(self.u_pairs): ax.plot(dst(self.u_pairs, n=self.n_steps), label="gammas_pairs", marker="v", ls="", **kwargs) ax.set_xlabel("timestep") ax.legend() class FourierExtendedParams(AbstractParams): r""" The Fourier pendant to ExtendedParams. Parameters ---------- hyperparameters: The hyperparameters containing the hamiltonian and the number of steps and the number of fourier coefficients ``hyperparameters = (hamiltonian, n_steps, q)`` parameters: Tuple containing ``(v, u_singles, u_pairs)`` with dimensions ``((q x nqubits), (q x nsingle_terms), (q x npair_terms))`` Attributes ---------- q : int The number of coefficients for the discrete sine and cosine transforms below v: np.array The discrete cosine transform of the ``betas`` in ``ExtendedParams`` u_singles: np.array The discrete sine transform of the ``gammas_singles`` in ``ExtendedParams`` u_pairs: np.array The discrete sine transform of the ``gammas_pairs`` in ``ExtendedParams`` """ def __init__(self, hyperparameters: Tuple[PauliSum, int], parameters: Tuple): # setup reg, qubits_singles and qubits_pairs super().__init__(hyperparameters) self.q = hyperparameters[2] self.v, self.u_singles, self.u_pairs = (np.array(parameters[0]), np.array(parameters[1]), np.array(parameters[2])) def __repr__(self): string = "Hyperparameters:\n" string += "\tregister: " + str(self.reg) + "\n" string += "\tqubits_singles: " + str(self.qubits_singles) + "\n" string += "\tqubits_pairs: " + str(self.qubits_pairs) + "\n" string += "Parameters:\n" string += "\tv: " + str(self.v).replace("\n", ",") + "\n" string += "\tu_singles: " + str(self.u_singles)\ .replace("\n", ",") + "\n" string += "\tu_pairs: " + str(self.u_pairs)\ .replace("\n", ",") + "\n" return string def __len__(self): return self.q * (len(self.reg) + len(self.qubits_pairs) + len(self.qubits_singles)) @shapedArray def v(self): return (self.q, len(self.reg)) @shapedArray def u_singles(self): return (self.q, len(self.qubits_singles)) @shapedArray def u_pairs(self): return (self.q, len(self.qubits_pairs)) @property def x_rotation_angles(self): betas = dct(self.v, n=self.n_steps, axis=0) return betas @property def z_rotation_angles(self): if self.u_singles.size > 0: gammas_singles = dst(self.u_singles, n=self.n_steps, axis=0) else: gammas_singles = np.empty(shape=(self.n_steps, 0)) return self.single_qubit_coeffs * gammas_singles @property def zz_rotation_angles(self): if self.u_pairs.size > 0: gammas_pairs = dst(self.u_pairs, n=self.n_steps, axis=0) else: gammas_pairs = np.empty(shape=(self.n_steps, 0)) return self.pair_qubit_coeffs * gammas_pairs def update_from_raw(self, new_values): self.v = np.array(new_values[:len(self.reg) * self.q]) self.v = self.v.reshape((self.q, len(self.reg))) new_values = new_values[self.q * len(self.reg):] self.u_singles = np.array(new_values[:len(self.qubits_singles) * self.q]) self.u_singles = self.u_singles.reshape((self.q, len(self.qubits_singles))) new_values = new_values[self.q * len(self.qubits_singles):] self.u_pairs = np.array(new_values[:len(self.qubits_pairs) * self.q]) self.u_pairs = self.u_pairs.reshape((self.q, len(self.qubits_pairs))) new_values = new_values[self.q * len(self.qubits_pairs):] # PEP8 complains, but new_values could be np.array and not list! if not len(new_values) == 0: raise RuntimeWarning( "list to make new u's and v's out of didn't" "have the right length!") def raw(self): raw_data = np.concatenate((self.v.flatten(), self.u_singles.flatten(), self.u_pairs.flatten())) return raw_data @classmethod def linear_ramp_from_hamiltonian(cls, hamiltonian: PauliSum, n_steps: int, q: int = 4, time: float = None): """ Returns ------- FourierExtendedParams The initial parameters according to a linear ramp for ``hamiltonian``. """ # create evenly spaced timesteps at the centers of n_steps intervals if time is None: time = float(0.7 * n_steps) term_lengths = [len(t) for t in hamiltonian] n_sing = term_lengths.count(1) n_pairs = term_lengths.count(2) n_betas = len(hamiltonian.get_qubits()) has_higher_order_terms = any(l > 2 for l in term_lengths) if has_higher_order_terms: raise NotImplementedError( "As of now we can only handle hamiltonians with at most " "two-qubit terms") v = np.zeros(q) v[0] = 0.5 * time / n_steps u_singles = np.copy(v) u_pairs = np.copy(v) v = v.repeat(n_betas).reshape(q, n_betas) u_singles = u_singles.repeat(n_sing).reshape(q, n_sing) u_pairs = u_pairs.repeat(n_pairs).reshape(q, n_pairs) # wrap it all nicely in a qaoa_parameters object params = cls((hamiltonian, n_steps, q), (v, u_singles, u_pairs)) return params @classmethod def from_AbstractParameters(cls, abstract_params: AbstractParams, parameters: Tuple, q: int = 4) -> AbstractParams: """ Returns ------- FourierExtendedParams A ``FourierExtendedParams`` object with the hyperparameters taken from ``abstract_params`` and the normal parameters from ``parameters`` """ out = super().from_AbstractParameters(abstract_params) if q is None: q = 4 out.q = q out.v, out.u_singles, out.u_pairs = (np.array(parameters[0]), np.array(parameters[1]), np.array(parameters[2])) return out @classmethod def empty(cls, hyperparameters): self = super().empty(hyperparameters) self.q = hyperparameters[2] (self.v, self.u_singles, self.u_pairs)\ = (np.empty((self.q, len(self.reg))), np.empty((self.q, len(self.qubits_singles))), np.empty((self.q, len(self.qubits_pairs)))) return self def plot(self, ax=None, **kwargs): if ax is None: fig, ax = plt.subplots() ax.plot(dct(self.v, n=self.n_steps, axis=0), label="betas", marker="s", ls="", **kwargs) if not _is_iterable_empty(self.u_singles): ax.plot(dst(self.u_singles, n=self.n_steps), label="gammas_singles", marker="^", ls="", **kwargs) if not _is_iterable_empty(self.u_pairs): ax.plot(dst(self.u_pairs, n=self.n_steps), label="gammas_pairs", marker="v", ls="", **kwargs) ax.set_xlabel("timestep") ax.legend() class QAOAParameterIterator: """An iterator to sweep one parameter over a range in a QAOAParameter object. Parameters ---------- qaoa_params: The initial QAOA parameters, where one of them is swept over the_parameter: A string specifying, which parameter should be varied. It has to be of the form ``<attr_name>[i]`` where ``<attr_name>`` is the name of the _internal_ list and ``i`` the index, at which it sits. E.g. if ``qaoa_params`` is of type ``AnnealingParams`` and we want to vary over the second timestep, it is ``the_parameter = "times[1]"``. the_range: The range, that ``the_parameter`` should be varied over Todo ---- - Add checks, that the number of indices in ``the_parameter`` matches the dimensions of ``the_parameter`` - Add checks, that the index is not too large Example ------- Assume qaoa_params is of type ``StandardWithBiasParams`` and has `n_steps >= 2`. Then the following code produces a loop that sweeps ``gammas_singles[1]`` over the range ``(0, 1)`` in 4 steps: .. code-block:: python the_range = np.arange(0, 1, 0.4) the_parameter = "gammas_singles[1]" param_iterator = QAOAParameterIterator(qaoa_params, the_parameter, the_range) for params in param_iterator: # do what ever needs to be done. # we have type(params) == type(qaoa_params) """ def __init__(self, qaoa_params: Type[AbstractParams], the_parameter: str, the_range: Iterable[float]): """See class documentation for details""" self.params = qaoa_params self.iterator = iter(the_range) self.the_parameter, *indices = the_parameter.split("[") indices = [i.replace(']', '') for i in indices] if len(indices) == 1: self.index0 = int(indices[0]) self.index1 = False elif len(indices) == 2: self.index0 = int(indices[0]) self.index1 = int(indices[1]) else: raise ValueError("the_parameter has to many indices") def __iter__(self): return self def __next__(self): # get next value from the_range value = next(self.iterator) # 2d list or 1d list? if self.index1 is not False: getattr(self.params, self.the_parameter)[self.index0][self.index1] = value else: getattr(self.params, self.the_parameter)[self.index0] = value return self.params