Working with the Parameter classes

Here we illustrate the use of the various QAOA parameter classes in the EntropicaQAOA package.

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

# import the neccesary pyquil modules
from pyquil.paulis import PauliSum, PauliTerm

# import the QAOAParameters classes and cost functions
from entropica_qaoa.qaoa.cost_function import QAOACostFunctionOnWFSim
from entropica_qaoa.qaoa.parameters import AbstractParams, ExtendedParams, StandardParams, QAOAParameterIterator

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 EntropicaQAOA, there are several ways of creating the problem Hamiltonian of interest. Ultimately, it should be in the form of a Pyquil PauliSum object: see Rigetti’s documentation on the PauliSum and PauliTerm classes for more information on working with these.

For example, let’s create the simple Hamiltonian

\[H = 0.7Z_0Z_1 + 1.2Z_0Z_2 -0.5Z_0\]

Using PauliTerm and PauliSum directly, this can be implemented as follows.

# create a hamiltonian on 3 qubits with 2 coupling terms and 1 bias term
Term1 = PauliTerm("Z", 0, 0.7)*PauliTerm("Z", 1)
Term2 = PauliTerm("Z", 0, 1.2)*PauliTerm("Z", 2)
Term3 = PauliTerm("Z", 0, -0.5)

hamiltonian = PauliSum([Term1,Term2,Term3])
print("hamiltonian =", hamiltonian)
hamiltonian = (0.7+0j)*Z0*Z1 + (1.2+0j)*Z0*Z2 + (-0.5+0j)*Z0

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 resepct 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 StandardParams 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 \(\beta^{(q)}\) and \(\gamma^{(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(\beta_1,\beta_2,\gamma_1,\gamma_2) = \exp\left(-i\beta^{(2)} H_{M}\right)\exp\left(-i\gamma^{(2)} H_{C}\right)\exp\left(-i\beta^{(1)} H_{M}\right)\exp\left(-i\gamma^{(1)} H_{C}\right)\]

where the mixer Hamiltonian is given by \(H_M = -\sum_j X_j\), and the cost Hamiltonian is given by \(H_C = \sum_j h_j Z_j + (1/2)\sum_{j,k} g_{j,k} Z_jZ_k\).

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

    • \(\mbox{betas} = \{\beta_0^{(1)},...,\beta_{n-1}^{(1)},\beta_0^{(2)},...,\beta_{n-1}^{(2)},...,\beta_0^{(p)},...,\beta_{n-1}^{(p)}\}\), where \(\beta_i^{(q)}\) denotes the mixer Hamiltonian angle for qubit \(i\) in the QAOA step \(q\).

    • \(\mbox{gammas_singles} = \left\{ \{\gamma_s^{(1)}\}, \{\gamma_s^{(2)}\},...,\{\gamma_s^{(p)}\} \right\}\), where where \(s\) is the set of qubits with bias terms in the cost Hamiltonian, and \(\{\gamma_s^{(q)}\}\) denotes the set of angles corresponding to those bias terms in QAOA step \(q\).

    • \(\mbox{gammas_pairs} = \left\{ \{\Gamma_{\Pi}^{(1)}\}, \{\Gamma_{\Pi}^{(2)}\},...,\{\Gamma_{\Pi}^{(p)}\} \right\}\), where where \(\Pi\) is the set of qubits with bias terms in the cost Hamiltonian, and \(\{\Gamma_{\Pi}^{(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{split}U\left(\vec{\beta},\vec{\gamma},\vec{\Gamma}\right) = \exp\left(i\sum_{j}\beta_{j}^{(2)}X_j\right)\exp\left(-i\sum_{j\in s} \gamma_{j}^{(2)}h_{j}Z_j - (i/2)\sum_{j,k \in \Pi}\Gamma_{jk}^{(2)}g_{jk}Z_jZ_k\right) \\ \qquad\qquad\times\exp\left(i\sum_{j}\beta_{j}^{(1)}X_j\right)\exp\left(-i\sum_{j\in s} \gamma_{j}^{(1)}h_{j}Z_j - (i/2)\sum_{j,k \in \Pi}\Gamma_{jk}^{(1)}g_{jk}Z_jZ_k\right)\end{split}\]

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

  • AnnealingParams: 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.

  • FourierParams: a heuristic parametrisation proposed by Zhou et al in reference Ref 2. The idea is that the optimal \(\beta\) and \(\gamma\) 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 EntropicaQAOA. Depending on the situation at hand, one method may be preferable to another.

Building parameters from the AbstractParams class

All of the QAOA parameter classes listed above are descendants of the parent class AbstractParams. An object belonging to this class is characterised by the problem hyperparameters. We create such an object as follows:

p = 2
abstract_params = AbstractParams([hamiltonian,p])
print(abstract_params)
Hyperparameters:
    register: [0, 1, 2]
    qubits_singles: [0]
    single_qubit_coeffs: [-0.5]
    qubits_pairs: [[0, 1], [0, 2]]
    pair_qubit_coeffs: [0.7 1.2]
    n_steps: 2

Subsequently, we can initalise any of the sets of variable parameters described above by making use of the AbstractParams object. For example, let’s set up ExtendedParams using the AbstractParams we have just defined. To do this, we call the .from_AbstractParameters method.

# Specify some angles
betas          = [[0.0, 0.1, 0.3], [0.5, 0.2, 1.2]]
gammas_singles = [[0.0], [0.5]]
gammas_pairs   = [[0.1, 0.3], [0.2, 1.2]]
parameters = (betas, gammas_singles, gammas_pairs)

extended_params = ExtendedParams.from_AbstractParameters(abstract_params,parameters)
print(extended_params)
Hyperparameters:
    register: [0, 1, 2]
    qubits_singles: [0]
    qubits_pairs: [[0, 1], [0, 2]]
Parameters:
    betas: [[0.  0.1 0.3], [0.5 0.2 1.2]]
    gammas_singles: [[0. ], [0.5]]
    gammas_pairs: [[0.1 0.3], [0.2 1.2]]

One benefit of this method is that it allows multiple different parametrisations to be set up from the same underlying set of hyperparameters.

Building parameters directly

We can also create ExtendedParams directly, without first setting up AbstractParams. For the case considered above, we would simply do the following:

extended_direct = ExtendedParams([hamiltonian,2],parameters)
print(extended_direct)
Hyperparameters:
    register: [0, 1, 2]
    qubits_singles: [0]
    qubits_pairs: [[0, 1], [0, 2]]
Parameters:
    betas: [[0.  0.1 0.3], [0.5 0.2 1.2]]
    gammas_singles: [[0. ], [0.5]]
    gammas_pairs: [[0.1 0.3], [0.2 1.2]]

We can set up an object of any of the other parameter classes analogously to the case we have illustrated above for ExtendedParams, either directly, or through AbstractParams.

In a standard workflow, with the parameters in hand, we would then pass them to a cost_function object, built with one of the classes QAOACostFunctionOnQVM or with QAOACostFunctionOnWFSim. To do so, we first arrange the params in a single raw list of parameters in the form [betas,gammas_singles,gammas_pairs]:

extended_direct_raw = extended_direct.raw()
extended_direct_raw
array([0. , 0.1, 0.3, 0.5, 0.2, 1.2, 0. , 0.5, 0.1, 0.3, 0.2, 1.2])

Further examples of the use of cost function objects and optimisers are given in the First steps: An example workflow notebook.

Using .empty()

Both of the methods above require us to explicitly specify a set of angles at the time of instantiation. However, in some cases it may not be possible or convenient to do so. In these instances, we can instead make use of the .empty() method, in which we need only specify the problem hyperparameters. A set of variable parameters is then created under the hood using Numpy’s empty method: the shape of the resulting arrays is consistent with the hyperparameters specified.

For example, using the Hamiltonian we created above, let’s set up ExtendedParams for a circuit of depth \(p=4\), without explicitly committing ourselves to particular values for the angles.

extended_empty = ExtendedParams.empty((hamiltonian, 4))
extended_empty
Hyperparameters:
    register: [0, 1, 2]
    qubits_singles: [0]
    qubits_pairs: [[0, 1], [0, 2]]
Parameters:
    betas: [[0.  0.1 0.3], [0.5 0.2 1.2], [0.  0.5 0.1], [0.3 0.2 1.2]]
    gammas_singles: [[0.1], [0.3], [0.2], [1.2]]
    gammas_pairs: [[6.91997069e-310 6.91997069e-310], [0.00000000e+000 0.00000000e+000], [0.00000000e+000 0.00000000e+000], [0.00000000e+000 0.00000000e+000]]

We see that arrays of the correct shape have been created for betas, gammas_singles, and gammas_pairs. (Note that the actual values contained in these arrays correspond to whatever happens to be in the memory location used by Numpy. If a memory location we have previously used is being recycled, the numbers may appear familiar. Otherwise, they may be some general floating point number).

“Linear ramp from Hamiltonian” parametrisation

For all parameter classes, we also provide the .linear_ramp_from_hamiltonian() method. This automatically determines a set of betas, 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_from_hamiltonian() method simply assumes that we are performing a discretised form of quantum annealing, with a linear schedule function \(s(t) \propto t\), with the slope dependent on the number of steps \(p\).

Let’s look at a specific example with the StandardParams class. For reference, we reproduce the corresponding code snippet from qaoa.parameters.py here.

# create evenly spaced n_steps at the centers of n_steps intervals
dt = time / n_steps
times = np.linspace(time * (0.5 / n_steps), time * (1 - 0.5 / n_steps), n_steps)

# fill betas, gammas_singles and gammas_pairs
betas = np.array([dt * (1 - t / time) for t in times])
gammas_singles = np.array([dt * t / time for t in times])
gammas_pairs = np.array([dt * t / time for t in times])

Let’s set up parameters for the case n_steps (i.e. \(p\)) = 2, with the Hamiltonian from above.

p = 2
T = 1 # total time T of the annealing schedule
params = StandardParams.linear_ramp_from_hamiltonian(hamiltonian, n_steps=p, time=T)
print(params)
Hyperparameters:
    register: [0, 1, 2]
    qubits_singles: [0]
    qubits_pairs: [[0, 1], [0, 2]]
Parameters:
    betas: [0.375 0.125]
    gammas: [0.125 0.375]

As expected for StandardParams, 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\), and the times at which we apply them are times = 0.25 and 0.75 here.

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\times (1 - 0.25), 0.5\times (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 time to the .linear_ramp_from_hamiltonian method, a value is determined automatically from the number of steps \(p\) specified. Empirically, we have found that a value \(T = 0.7\times 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 parametrisations

We also provide methods allowing parameters belonging to one class (e.g. StandardParams) to be converted to parameters of another class (e.g. ExtendedParams). For instance, let’s convert the Standard parameters we created in the previous section (from the linear_ramp method) to a set of corresponding Extended parameters.

extended_from_std = ExtendedParams.from_other_parameters(params)
extended_from_std
Hyperparameters:
    register: [0, 1, 2]
    qubits_singles: [0]
    qubits_pairs: [[0, 1], [0, 2]]
Parameters:
    betas: [[0.375 0.375 0.375], [0.125 0.125 0.125]]
    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 ExtendedParams to StandardParams is ill-defined: if we try to do this, we get an error message, as shown in this code snippet:

standard_from_extended = StandardParams.from_other_parameters(extended_from_std)

(Suppressed Traceback)

TypeError: Conversion from <class 'entropica_qaoa.qaoa.parameters.ExtendedParams'>
    to <class 'entropica_qaoa.qaoa.parameters.StandardParams'> not supported.

The general scheme of allowed parameter conversions is illustrated in the following figure:

Title

QAOA Parameter class type tree

Working with parametrisations

This section provides more detail on the way parameters may be used, and how they work internally.

Parameters under the hood

When we printed out the parameters in the examples above, we obtained a list of the angles, and the qubits or qubit pairs to which they correspond. We can also have a look at the internal x_rotation_angles, z_rotation_angles and zz_rotation_angles, which are automatically calculated under the hood. These are the rotation angles for the different Pauli operators in the actual execution of the quantum circuit: they are determined by products of the variable parameters (betas, gammas_singles, gammas_pairs) and the coefficients in the Hamiltonian itself (single_qubit_coeffs, pair_qubit_coeffs).

print("\n x_rotation_angles:\n", extended_direct.x_rotation_angles)
print("\n z_rotation_angles:\n", extended_direct.z_rotation_angles)
print("\n zz_rotation_angles:\n", extended_direct.zz_rotation_angles)
x_rotation_angles:
[[0.  0.1 0.3]
[0.5 0.2 1.2]]

z_rotation_angles:
[[-0.  ]
[-0.25]]

zz_rotation_angles:
[[0.07 0.36]
[0.14 1.44]]

Modifying parameters

We may wish to modify the parameters that have been set up, before running QAOA with them. The two ways we can do this are to modify the params or the raw_params.

# Current params and under-the-hood equivalents
print(params)
print("\n x_rotation_angles:\n", params.x_rotation_angles)
print("\n z_rotation_angles:\n", params.z_rotation_angles)
print("\n zz_rotation_angles:\n", params.zz_rotation_angles)
Hyperparameters:
    register: [0, 1, 2]
    qubits_singles: [0]
    qubits_pairs: [[0, 1], [0, 2]]
Parameters:
    betas: [0.375 0.125]
    gammas: [0.125 0.375]


 x_rotation_angles:
 [[0.375 0.375 0.375]
 [0.125 0.125 0.125]]

 z_rotation_angles:
 [[-0.0625]
 [-0.1875]]

 zz_rotation_angles:
 [[0.0875 0.15  ]
 [0.2625 0.45  ]]

Let’s modify the first betas parameter:

params.betas[0] = np.pi
print(params)
print("\n x_rotation_angles:\n", params.x_rotation_angles)
print("\n z_rotation_angles:\n", params.z_rotation_angles)
print("\n zz_rotation_angles:\n", params.zz_rotation_angles)
Hyperparameters:
    register: [0, 1, 2]
    qubits_singles: [0]
    qubits_pairs: [[0, 1], [0, 2]]
Parameters:
    betas: [3.14159265 0.125     ]
    gammas: [0.125 0.375]


 x_rotation_angles:
 [[3.14159265 3.14159265 3.14159265]
 [0.125      0.125      0.125     ]]

 z_rotation_angles:
 [[-0.0625]
 [-0.1875]]

 zz_rotation_angles:
 [[0.0875 0.15  ]
 [0.2625 0.45  ]]

If we know the index of the parameter(s) we wish to vary in the raw list, we can also modify them there.

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 ExtendedParams for a 3-qubit problem with \(p=2\) timesteps. The shape of the betas array we input should be \(2\times 3\), but what happens if we instead pass in an array of shape \(1\times 6\)?

betas = np.random.rand(6) # Wrong shape, should be 2x3
gammas_singles = [0.5, 0.5] # Correct shape
gammas_pairs = [[0.5, 0.9, 0.4, 0.2]] # Wrong shape, should be 2x2

parameters = (betas, gammas_singles, gammas_pairs)

extended_params_error = ExtendedParams([hamiltonian,2],parameters)
print(extended_params_error)
Hyperparameters:
    register: [0, 1, 2]
    qubits_singles: [0]
    qubits_pairs: [[0, 1], [0, 2]]
Parameters:
    betas: [[0.14847342 0.3263844  0.77025306], [0.4935528  0.22538528 0.08746349]]
    gammas_singles: [[0.5], [0.5]]
    gammas_pairs: [[0.5 0.9], [0.4 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

betas = np.random.rand(6) # Wrong shape, should be 2x3
gammas_singles = [0.5, 0.5] # Correct shape
gammas_pairs = [[0.5], [0.9, 0.4, 0.2]] # Likely to be a more systematic error on the user's part
parameters = (betas, gammas_singles, gammas_pairs)
extended_params_error = ExtendedParams([hamiltonian,2],parameters)

we obtain the error message ValueError: gammas_pairs must have shape (2, 2).

Iterating over parameter ranges

We may sometimes want to investigate what happens if we vary one of the parameters over some specfied range, keeping all others fixed. The QAOAParameterIterator class gives a convenient way of doing this.

For the parameters above, we could for example take one of the gammas to iterate over. Suppose we take the second one, gammas[1], and we want to vary it in steps of size 1/3 between 0 and 1:

the_range = np.arange(0, 1, 0.3334)
the_parameter = "gammas[1]"
iterator= QAOAParameterIterator(params, the_parameter, the_range)
for i,p in zip(range(len(the_range)),iterator):
    print('Parameters at step' + str(i) + ':')
    print(p)
Parameters at step0:
Hyperparameters:
    register: [0, 1, 2]
    qubits_singles: [0]
    qubits_pairs: [[0, 1], [0, 2]]
Parameters:
    betas: [3.14159265 0.125     ]
    gammas: [0.125 0.   ]

Parameters at step1:
Hyperparameters:
    register: [0, 1, 2]
    qubits_singles: [0]
    qubits_pairs: [[0, 1], [0, 2]]
Parameters:
    betas: [3.14159265 0.125     ]
    gammas: [0.125  0.3334]

Parameters at step2:
Hyperparameters:
    register: [0, 1, 2]
    qubits_singles: [0]
    qubits_pairs: [[0, 1], [0, 2]]
Parameters:
    betas: [3.14159265 0.125     ]
    gammas: [0.125  0.6668]

As expected, in the loop all parameters stay constant except for gammas[1] which went up in steps of thirds.

Of course, we can also nest these iterators to sweep two-dimensional paramter landscapes. For instance, suppose we now sweep over parameter ranges for both gammas[1] and betas[0]:

range1 = np.arange(0,1,0.5)
range2 = np.arange(2,3,0.5)
iterator1 = QAOAParameterIterator(params, "gammas[1]", range1)
for j in iterator1:
    for p in QAOAParameterIterator(j, "betas[0]", range2):
        print("betas =", p.betas, ",", "gammas =", p.gammas)
betas = [2.    0.125] , gammas = [0.125 0.   ]
betas = [2.5   0.125] , gammas = [0.125 0.   ]
betas = [2.    0.125] , gammas = [0.125 0.5  ]
betas = [2.5   0.125] , gammas = [0.125 0.5  ]

We can nest iterators arbitrarily, which allows for parameter sweeps and subsequent landscape analysis of any desired set of parameters. This functionality will be demoed in a separate notebook.

# Sweep over three parameters

iterator1 = QAOAParameterIterator(params,"betas[0]",np.arange(0,1,0.5))
for p1 in iterator1:
    iterator2 = QAOAParameterIterator(p1,"betas[1]",np.arange(0,1,0.5))
    for p2 in iterator2:
        iterator3 = QAOAParameterIterator(p2,"gammas[0]",np.arange(0,1,0.5))
        for j in iterator3:
            print("betas =", j.betas, ",", "gammas =", j.gammas)
betas = [0. 0.] , gammas = [0.  0.5]
betas = [0. 0.] , gammas = [0.5 0.5]
betas = [0.  0.5] , gammas = [0.  0.5]
betas = [0.  0.5] , gammas = [0.5 0.5]
betas = [0.5 0. ] , gammas = [0.  0.5]
betas = [0.5 0. ] , gammas = [0.5 0.5]
betas = [0.5 0.5] , gammas = [0.  0.5]
betas = [0.5 0.5] , gammas = [0.5 0.5]

Parameter iterator use case: Landscape sweeps

This section shows how to use the QAOAParameterIterator class for analysis of the cost function landscape. Here, one or two parameters of interest are varied within some range of interest, while all others are kept fixed. We then compute the cost function value (and/or its standard deviation) within the specified domain.

Let’s work with a simple 2-qubit problem, in the ExtendedParams class, with \(p=3\) timesteps.

h_test = []
h_test.append(PauliTerm("Z", 0, 0.7)*PauliTerm("Z", 1))
h_test.append(PauliTerm("Z", 0, -0.5))
h_test = PauliSum(h_test)

We require 3x2 betas parameters, 3x1 gammas_singles and 3x1 gammas_pairs parameters, which we will initialise randomly:

betas = np.random.rand(3,2)
gammas_singles = np.random.rand(3,1)
gammas_pairs = np.random.rand(3,1)
parameters = (betas,gammas_singles,gammas_pairs)
extendedparams = ExtendedParams([h_test,3],parameters)
print(extendedparams)
Hyperparameters:
    register: [0, 1]
    qubits_singles: [0]
    qubits_pairs: [[0, 1]]
Parameters:
    betas: [[0.8977305  0.64438783], [0.52453506 0.57953221], [0.68973134 0.46763266]]
    gammas_singles: [[0.53866581], [0.62619672], [0.84929365]]
    gammas_pairs: [[0.63640435], [0.58841693], [0.18309801]]

We have a total of 12 parameters; as an example, let’s focus on how the cost function value changes when we vary the beta angle on the second qubit in the second QAOA step (p=2). This is the [1][1] entry in the betas list. We will vary its range from 0 to \(2\pi\).

range_ = np.linspace(0,2*np.pi,100)
param_ = "betas[1][1]"
iterator = QAOAParameterIterator(extendedparams, param_, range_)

We will now compute the cost function value with all the different values of the specified parameter of interest.

cost_vals = np.zeros((len(range_,)))
dev_vals = np.zeros((len(range_,)))
ind = 0
for i,params in zip(range(len(range_)),iterator):

    cost_function = QAOACostFunctionOnWFSim(h_test,params=params)
    val = cost_function(params.raw())
    cost_vals[i] = val

Below we plot the energy landscape as a function of the parameter of interest in the specified range. A more thorough set of methods for visualising energy landscapes will be demonstrated in a separate notebook.

plt.plot(range_,cost_vals,'b')
plt.xlabel('betas[1][1]')
plt.ylabel('Energy')
plt.show()
../_images/2_ParameterClasses_57_0.png

Footnotes

  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.