# 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):

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.

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

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

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:

## 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()
```

## Footnotes¶

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.