04 - Working with the QAOA Variational Parameter Classes

Here we illustrate the use of the various QAOA variational parameter classes in the OpenQAOA package. Note that in this notebook example we will be using manual mode of OpenQAOA.

# import the standard modules from python
import numpy as np
import matplotlib.pyplot as plt

# import the OpenQAOA Parameterisation classes manually: Manual Mode
from openqaoa.qaoa_components import (PauliOp, Hamiltonian, QAOADescriptor,
create_qaoa_variational_params, QAOAVariationalStandardParams, QAOAVariationalExtendedParams)

# import the other OpenQAOA modules required for this example
from openqaoa.utilities import X_mixer_hamiltonian

Creating the problem Hamiltonian and setting up hyperparameters

In general, the QAOA consists of two different types of parameters, which we will refer to as hyperparameters and variable parameters. This section covers the hyperparameters, while the section below focuses on the variable parameters. The hyperparameters are those parameters that remain fixed throughout our computation, while the variable parameters are those that we modify in seeking the optimial problem solution.

In the simplest implementation of QAOA, the hyperparameters may in turn be divided into two sets (see Footnote 1 for a third example of hyperparameters):

  1. Those originating from the cost Hamiltonian:

  • the qubit register (the qubits to be used in the algorithm);

  • the qubits with a bias term (their own Z term in the Hamiltonian), and the corresponding coefficients;

  • the qubit pairs that interact (through a ZZ term in the Hamiltonian), along with the corresponding ‘coupling’ coefficients.

  1. The number of QAOA steps we wish to perform, frequently referred to as the QAOA ‘p’ parameter.

In OpenQAOA, there are several ways to creating the problem Hamiltonian of interest. Any problem will have to be interpreted as a Hamiltonian object consisting of PauliOp terms.

For example, let’s create the simple Hamiltonian

\[H = 0.7 Z_0 Z_1 + 1.2 Z_0 Z_2 - 0.5 Z_0\]

This can be implemented as follows:

# Create a hamiltonian on 3 qubits with 2 coupling terms and 1 bias term
Term1 = PauliOp('ZZ', (0, 1))
Term2 = PauliOp('ZZ', (0, 2))
Term3 = PauliOp('Z', (0, ))

hamiltonian = Hamiltonian([Term1, Term2, Term3], [0.7, 1.2, -0.5], 0.0)
print("hamiltonian =", hamiltonian)
hamiltonian = 0.7*Z_{0}Z_{1} + 1.2*Z_{0}Z_{2} + -0.5*Z_{0} + 0.0

QAOA variable parameter classes

Having specified the problem Hamiltonian, we next move on to defining the QAOA parameters we want to use in our quantum circuit.

For a general variational quantum algorithm such as VQE, to fully define a problem we must specify a circuit ansatz (a sequence of gates to be performed) and a corresponding parametrisation (how the parameters over which we intend to optimise are related to the sequence of gates). In QAOA, the circuit ansatz is fixed to be the alternate application of the mixer operator (also referred to as the driver or reference operator) and the cost operator (sometimes also referred to as the phase separation operator). The goal is then to find the parameters that optimise the cost function when evaluated with respect to the quantum state produced by the circuit.

We have a considerable degree of flexibility in choosing how to parametrise a QAOA problem. We can choose a smaller set of parameters, where the optimisation landscape has lower dimension at the expense of reduced expressivity. Or, we can choose a larger set, where we can generate a wider set of quantum states with lower circuit depth, but the corresponding optimisation landscape has a higher dimension. The larger the set of parameters we have to optimise over, the more difficult it can become to find the optimal solution.

The variable parameters are those we wish to optimise, which in turn depend on the specific parametrisation for the circuit we have chosen.

  • The QAOAVariationalStandardParams class implements the original and conventional form of the QAOA, as described in Ref 1. In time step \(q\) of the algorithm, the mixer and cost Hamiltonians are applied with coefficients \(β^{(q)}\) and \(γ^{(q)}\), respectively, giving a total of \(2p\) parameters over which we need to optimise. For example for a depth-2 (\(p=2\)) circuit, the unitary operator corresponding to the QAOA circuit would take the form

\[U(β_1, β_2, γ_1, γ_2) = exp(-iβ^{(2)}H_M)exp(-iγ^{(2)}H_M)exp(-iβ^{(1)}H_M)exp(-iγ^{(1)}H_M)\]

where the mixer Hamiltonian is given by \(H_M=−∑_jX_j\), and the cost Hamiltonian is given by \(H_C=∑_jh_jZ_j+(1/2)∑_{j,k}g_{j,k}Z_jZ_k\).

  • For the QAOAVariationalExtendedParams class, each operator in both the cost and mixer Hamiltonians has its own angle, so that the set of variable parameters are:

    • \(betas\_singles={β^{(1)}_0,...,β^{(1)}_{n−1},β^{(2)}_0,...,β^{(2)}_{n−1},...,β^{(p)}_0,...,β^{(p)}_{n−1}}\) , where \(β^q_i\) denotes the mixer Hamiltonian angle for qubit \(i\) in the QAOA step \(q\).

    • \(betas\_pairs={B^{(1)}_{0, 1},...,B^{(1)}_{i, j},B^{(2)}_{0, 1},...,B^{(2)}_{i, j},...,B^{(p)}_{0, 1},...,B^{(p)}_{i, j}}\) , where \(B^q_{i, j}\) denotes the mixer Hamiltonian angle for qubit pairs \(ij\), where \(i \neq j\), in the QAOA step \(q\). This is only relevant for mixer Hamiltonians with 2-qubit interactions.

    • \(gammas\_singles ={{γ^{(1)}_s},{γ^{(2)}_s},...,{γ^{(p)}_s}}\), where where \(s\) is the set of qubits with bias terms in the cost Hamiltonian, and \({γ^{(q)}_s}\) denotes the set of angles corresponding to those bias terms in QAOA step \(q\).

    • \(gammas\_pairs={{Γ^{(1)}_Π},{Γ^{(2)}_Π},...,{Γ^{(p)}_Π}}\), where where \(Π\) is the set of qubits with bias terms in the cost Hamiltonian, and \({Γ^{(q)}_Π}\) denotes the set of angles corresponding to those bias terms in QAOA step \(q\).

For instance, for a depth-2 circuit the corresponding unitary operator would then become:

\begin{multline} U(\beta ,\gamma ,\Gamma )=exp(i\sum_j\beta^{(2)}_jX_j)exp(−i\sum_{j∈s}\gamma^{(2)}_jh_jZ_j− \\ (i/2)\sum_{j,k∈\Pi}\Gamma^{(2)}_{jk}g_{jk}Z_jZ_k)exp(i\sum_j\beta^{(1)}_jX_j) \\ exp(−i\sum_{j∈s}\gamma^{(1)}_jh_jZ_j−(i/2)\sum_{j,k∈\Pi}\Gamma^{(1)}_{jk}g_{jk}Z_jZ_k) \end{multline}

The above example only shows the unitary operator whose mixer hamiltonian consists only of 1-qubit mixers.

We currently provide two additional parameter classes that may be of interest, either for didactic or practical purposes.

QAOAVariationalAnnealingParams: basically a discretised form of quantum annealing, with a schedule function \(s(t)\); the coefficient of the mixer Hamiltonian is \((1−s(t))\), and the coefficient of the cost Hamiltonian is \(s(t)\). Unlike QAOA, therefore, the coefficients of the two Hamiltonians are necessarily related to one another.

QAOAVariationalFourierParams: a heuristic parametrisation proposed by Zhou et al in reference Ref 2. The idea is that the optimal \(β\) and \(γ\) parameters sometimes empirically appear to be described by relatively smooth functions, meaning that one can consider working instead with the Fourier decompositions of those functions. By keeping only a fixed number of low-frequency Fourier components, the parameter space over which one must optimise can be significantly reduced.

The use of these latter two parameter classes is demonstrated in the separate notebook Advanced QAOA parameter classes.

Parameter creation and conversion routines

This section demonstrates the different methods available for setting up parameters in OpenQAOA. Depending on the situation at hand, one method may be preferable to another.

Creation of QAOADescriptor Class

This object is required before creating a Parameter Class Object. This object contains the hyperparameters used for the problem. Since the hyperparameters are fixed throughout the computation, OpenQAOA separates them from the variable parameters.

# Create Circuit Params Class
mixer_hamiltonian = X_mixer_hamiltonian(n_qubits = 3)
qaoa_descriptor = QAOADescriptor(cost_hamiltonian = hamiltonian, mixer_block = mixer_hamiltonian, p=1)

Building parameters from the create_qaoa_variational_params function

All of the QAOA variational parameter classes listed above can be created easily using the create_qaoa_variational_params. We create the Variational Parameter Objects as follows:

# To create a Variational Parameter Class with the Standard Parameterisation and Random Initialisation
create_qaoa_variational_params(qaoa_descriptor = qaoa_descriptor, params_type = 'standard', init_type = 'rand')
Standard Parameterisation:
        p: 1
Variational Parameters:
        betas: [0.67825497]
        gammas: [1.33848011]

In order to create a Variational Parameter Object, the user is required to pass a QAOADescriptor object, a string specifying the type of parameterisation to be used and the way in which the variational parameters will be initialised.

OpenQAOA has support for the following parameterisation and parameter initialisation strategies.

  • Supported Parameterisations:

    1. standard : For the original form of QAOA.

    2. standard_w_bias : For the original form of QAOA with a bias term.

    3. extended : Every operator in the mixer and cost hamiltonian has its own angle.

    4. fourier : The sine/cosine transformation of the betas and gammas used in the standard parameterisation.

    5. fourier_extended : The sine/cosine transformation of the betas and gammas used in the extended parameterisation.

    6. fourier_w_bias : The sine/cosine transformation of the betas and gammas used in the standard parameterisation with a bias term.

    7. annealing : A discretised form of quantum annealing where the coefficient of the cost and mixer hamiltonian are related to one another.

  • Supported Initialisations:

    1. rand : Angles are initialised randomly.

    2. ramp : Linear Ramp Initlisation.

    3. custom : Initalise the angles with custom values.

The same set of hyperparameters encapsulated by the QAOADescriptor Class can be use to initialise different parameterisations.

# To create a Variational Parameter Class with the Extended Parameterisation and Random Initialisation
create_qaoa_variational_params(qaoa_descriptor = qaoa_descriptor, params_type = 'extended', init_type = 'rand')
Extended Parameterisation:
        p: 1
        betas_singles: [[1.93258376 1.75922895 2.027917  ]]
        betas_pairs: []
        gammas_singles: [[0.79981656]]
        gammas_pairs: [[1.25608449 2.33059942]]

We can also input our own initial values of betas and gammas as follows:

create_qaoa_variational_params(qaoa_descriptor = qaoa_descriptor,
                               params_type = 'standard', init_type = 'custom',
                               variational_params_dict = {'betas': [0.1], 'gammas': [0.1]})
Standard Parameterisation:
        p: 1
Variational Parameters:
        betas: [0.1]
        gammas: [0.1]

Note that when using the custom initilisation strategy, we will have to provide the appropriate number of betas and gammas depending on the number of variational parameters. ( Which depends on the p-value and parameterisation type )

Linear Ramp Initalisation

For all parameter classes, we also provide linear ramp initialisation strategy. This automatically determines a set of betas (beta_singles and beta_pairs), gammas_singles and gammas_pairs by analogy to a quantum annealing schedule, as we now explain.

As usual, we specify the desired number of circuit iterations \(p\) we wish to perform. If we were to view this QAOA circuit as a discretised quantum annealing procedure over a total time \(T\), we would need to specify \(p\) values for the annealing schedule function \(s(t)\) - one for each timestep. See the Advanced QAOA parameter classes for a more detailed explanation of quantum annealing, and the meaning of the function \(s(t)\).

If we choose the annealing schedule \(s(t)\) to be a linear function of \(t\), then the betas will linearly decrease from 0 to \(T\), while the gammas_singles and gammas_pairs (which, in annealing, are not distinguished as separate parameters) will linearly increase. The linear ramp initialisation strategy simply assumes that we are performing a discretised form of quantum annealing, with a linear schedule function \(s(t)∝t\), with the slope dependent on the number of steps \(p\).

Let’s look at a specific example with the QAOAVariationalStandardParams class. For reference, we reproduce the corresponding code snippet from openqaoa.qaoa_parameters.standardparams.py here.

linear_ramp_time = 1
p = 2
# create evenly spaced timelayers at the centers of p intervals
dt = linear_ramp_time / p

# fill betas, gammas_singles and gammas_pairs
betas = np.linspace((dt / linear_ramp_time) * (linear_ramp_time * (1 - 0.5 / p)),
                    (dt / linear_ramp_time) * (linear_ramp_time * 0.5 / p), p)
gammas = betas[::-1]

Let’s set up parameters for the case \(p=2\) with the Hamiltonian from above

p = 2
T = 1 # total time T of the annealing schedule
qaoa_descriptor = QAOADescriptor(hamiltonian, mixer_hamiltonian, p=2)
params = create_qaoa_variational_params(qaoa_descriptor = qaoa_descriptor,
                                        params_type = 'standard', init_type = 'ramp',
                                        linear_ramp_time = 1)
Standard Parameterisation:
        p: 2
Variational Parameters:
        betas: [0.375 0.125]
        gammas: [0.125 0.375]

As expected for QAOAVariationalStandardParams, we get one value for each of betas and gammas for each timestep up to our specified value of p. The timestep length (the duration of each pulse in the circuit) is dt=0.5.

In the annealing Hamiltonian, in our convention the coefficient of the mixer Hamiltonian is \((1−s(t))\), hence the angles beta we obtain here are \([0.5×(1−0.25),0.5×(1−0.75)]=[0.375,0.125]\). In a similar way, we obtain gammas$ =[0.125,0.375]$ from the fact that the coefficient of the cost Hamiltonian in the annealing process is s(t).

In this example, we explicitly passed in a total annealing time \(T\) as an argument to the method. As an important point, if the total annealing time is very large compared to the number of steps \(p\), then the QAOA will not perform well, since it would deviate far from the very notion of an adiabatic path between the ground states of the mixer and cost Hamiltonians. Likewise, a short total annealing time would correspond to a rapidly executed schedule, which is also likely to perform poorly.

If the user does not pass a value for linear_ramp_time, a value is determined automatically from the number of steps \(p\) specified. Empirically, we have found that a value \(T=0.7×p\) appears to strike a reasonable balance between the two extremes described above, and this is therefore the value we have chosen to implement.

Converting between parameterisations

We also provide methods allowing parameters belonging to one class (e.g. QAOAVariationalStandardParams) to be converted to parameters of another class (e.g. QAOAVariationalExtendedParams). For instance, let’s convert the Standard parameters we created in the previous section (with the linear ramp initialisation) to a set of corresponding Extended parameters.

from openqaoa.qaoa_components.variational_parameters import qaoa_variational_params_converter
Standard Parameterisation:
        p: 2
Variational Parameters:
        betas: [0.375 0.125]
        gammas: [0.125 0.375]

new_params = qaoa_variational_params_converter(target_params_type = 'extended',
                        current_params_obj = params)
Extended Parameterisation:
        p: 2
        betas_singles: [[0.375 0.375 0.375], [0.125 0.125 0.125]]
        betas_pairs: []
        gammas_singles: [[0.125], [0.375]]
        gammas_pairs: [[0.125 0.125], [0.375 0.375]]

The betas, gammas_singles and gammas_pairs have been created from the angles contained in the original set of Standard parameters (params).

Note that such conversion schemes cannot be implemented between any arbitrary pair of parameter classes. For instance, the conversion from QAOAVariationalExtendedParams to QAOAVariationalStandardParams is ill-defined: if we try to do this, we get an error message, as shown in this code snippet:

extended_params = create_qaoa_variational_params(qaoa_descriptor = qaoa_descriptor, params_type = 'extended', init_type = 'rand')

# An exception is raised when the conversion is not available.
    qaoa_variational_params_converter('standard', extended_params)
except Exception as e:
    print(type(e).__name__, ':', e)
TypeError : Conversion from <class 'openqaoa.qaoa_components.variational_parameters.extendedparams.QAOAVariationalExtendedParams'> to <class 'openqaoa.qaoa_components.variational_parameters.standardparams.QAOAVariationalStandardParams'> not supported.
   ExtendedParams   <--------- FourierExtendedParams
          ^                         ^
          |                         |
StandardWithBiasParams <------ FourierWithBiasParams
          ^                         ^
          |                         |
    StandardParams  <----------- FourierParams

Erroneous parameter catching

If the user accidentally enters a set of variable parameters that are inconsistent with the problem hyperparameters, the error is either automatically corrected, or flagged as an error for further investigation. For example, suppose we use QAOAVariationalExtendedParams for a 3-qubit problem with p=2 timesteps. The shape of the betas_singles array we input should be 2×3, but what happens if we instead pass in an array of shape 1×6?

qaoa_descriptor = QAOADescriptor(hamiltonian, mixer_hamiltonian, p=2)
create_qaoa_variational_params(qaoa_descriptor = qaoa_descriptor,
                               params_type = 'extended', init_type = 'custom',
                               variational_params_dict = {'betas_singles': [0.1, 0.1, 0.1, 0.2, 0.2, 0.2],
                                                          'betas_pairs': [],
                                                          'gammas_singles': [0.1, 0.2],
                                                          'gammas_pairs': [0.1, 0.2, 0.1, 0.2]})
Extended Parameterisation:
        p: 2
        betas_singles: [[0.1 0.1 0.1], [0.2 0.2 0.2]]
        betas_pairs: []
        gammas_singles: [[0.1], [0.2]]
        gammas_pairs: [[0.1 0.2], [0.1 0.2]]

The parameters have been reshaped into arrays of the correct dimensions, consistent with the hyperparameters. If, however, we pass in an array whose incorrect shape is likely to result from a more systematic error, this is flagged to the user.

For example, if we try to run the following code

create_qaoa_variational_params(qaoa_descriptor = qaoa_descriptor, params_type = 'extended', init_type = 'custom', variational_params_dict = {'betas_singles': [0.1, 0.1, 0.1, 0.2], 'betas_pairs': [], 'gammas_singles': [0.1, 0.2], 'gammas_pairs': [0.1, 0.2, 0.1, 0.2]})

we obtained the error message ValueError: cannot reshape array of size 4 into shape (2,3)


  1. There are other hyperparameters that we will not consider in this notebook. For example, here we have assumed that the mixer Hamiltonian is simply the sum of Pauli X operators on all qubits. However, one could clearly consider other types of mixers - see for example Ref 3.


    1. Farhi et al, A Quantum Approximate Optimization Algorithm

    1. Zhou et al, Quantum Approximate Optimization Algorithm: Performance, Mechanism, and Implementation on Near-Term Devices

    1. Wang et al, XY-mixers: analytical and numerical results for QAOA