In the previous chapter, we learned how to find the minimum of an energy function using a quantum annealer. This is a single-purpose solver, where any problem that can be converted into a QUBO can be solved on a D-Wave quantum annealer or other QUBO solvers. In this chapter, we will move to a more general class of quantum computers that are intended to solve any kind of mathematical problem. It would be accurate to say that a classical computer that allows any kind of calculation through a basic set of logical gates is a universal computer. Similarly, a quantum computer that allows the use of a set of logical gates that, in combination, can be used to solve any kind of mathematical problem, would be called a Universal Quantum Computer. This chapter deals with the general set of quantum computers that we often hear about, which is referred to as universal or gate-based quantum computers. These utilize a combination of gates or a quantum circuit to solve some specific problem.
Since the audience of this book is someone who’s new to Amazon Braket but somewhat familiar with the basic elements of quantum computing, such as qubits or quantum gates, we will learn how to implement matrix mathematics and equivalent gates on Amazon Braket. This will include a brief discussion about what a qubit is and how it is represented using a Bloch sphere. These concepts will be implemented in a practical way using Amazon Braket’s Circuit() function, where we will experiment with a circuit inspired by the Google Supremacy experiment.
In this chapter, we will cover the following topics:
The source code for this chapter is available in the following GitHub repository:
The online refreshed code includes run on Rigetti M-2 since M-1 is no longer available.
The concepts of vectors, orthogonality, identity matrix, matrix multiplication, tensor product, and the equations shown in this chapter come from linear algebra and trigonometry, so it might be helpful to review those concepts as needed. There are several excellent books on this subject and two are included in the Further reading section if you wish to dive deeper into the mathematics of quantum computing.
Quantum circuits are needed to “program” or tell a quantum computer what to do. These programs are dramatically different from most code that we are familiar with, but we still use Python or other languages to assemble the circuit before giving it to the quantum computer to execute. The execution itself involves transpiling or converting the instructions into a simpler set of logical gate operations and then, finally, into laser or microwave signals that are sent to individual qubits.
In this section, we will cover the basics of qubit representation and matrix mathematics as a foundation for understanding quantum gates. Next, we will look at a basic set of quantum gates and how to put them in a quantum circuit. Then, we will look at multiple qubit circuits and how to do the equivalent matrix multiplication to get the same results as that produced from a quantum circuit.
Finally, we will create a special circuit inspired by the Google Supremacy experiment. There, we will alternatively combine a set of randomly selected single-qubit gates and then entangle random pairs of qubits using a two-qubit gate. We will also execute this on real quantum devices to verify the results, and then compare the speed and accuracy of the results from simulators and the Rigetti and IonQ quantum devices.
A qubit is dramatically different from a classical bit because it stores more information than the single binary value that a classical bit can store. A quantum bit can be pictured as a sphere with a vector pointing at any point on that sphere. The top of the sphere is the zero-state, shown as |0⟩, while the bottom of the sphere is the one-state, shown as |1⟩. These two states can be represented in matrix form as follows:
However, we said that a qubit represents all the points on a sphere, called a Bloch sphere. So, these states can be represented as a combination of two state vectors:
Here, α0 and α1 are complex numbers.
Since α0 and α1 are complex numbers, they include both a real component and an imaginary phase component. We will represent this as a rotation about the vertical z-axis of the Bloch sphere:
Here, ? is an angle of rotation (or the phase angle) in radians from 0 to 2π, and from trigonometry the relationship between α0 and α1 is given by:
This is necessary to ensure the vector’s length is always 1.
Note that the vector, |ν⟩, is a resulting vector, which is the superposition of two complex numbers, and , that represent the two orthogonal states, |0⟩ and |1⟩. Orthogonality usually refers to two vectors that are perpendicular to each other:
Figure 6.1 – A Bloch sphere is used to represent one Qubit
We will show the representation of the qubit as a Bloch sphere initially for one qubit. Note that two or more qubits cannot be accurately represented using a Bloch sphere, so we will not use it for more than one qubit.
Now, let’s look at some qubit states both in matrix and Bloch sphere form:
Import numpy as np arr_0=np.array([[1+0j],[0+0j]])
draw_bloch(arr_0)
In this case, the state vector is pointing to the top of the Bloch sphere (or to the North pole), as shown here:
Figure 6.2 – Bloch sphere showing the state of |0⟩
Arr_1=np.array([[0+0j],[1+0j]]) draw_bloch(arr_1)
This is what the Bloch sphere will look like at this point:
Figure 6.3 – Bloch sphere showing the state of|1⟩
Now, let’s learn how a qubit state can be manipulated using gates. Gates can also be represented by specific matrices. Now, let’s review these matrices and use them to change the state of the qubit.
To understand the way gate operations work on quantum computers, it is important to understand two key ways of performing matrix mathematics. The first is to use regular matrix multiplication, which can be implemented in Python code using the @ symbol. The other is a tensor product of two matrices, which can be implemented in Python using the NumPy kron() function and is represented by the ⊗ symbol.
When an operator acts on a qubit state, we use matrix multiplication. Typically, we call this matrix a unitary operator because the matrices that can perform gate operations must meet the criteria that they are unitary and do not change the size of the vector from a length of 1. Though not critical for our purpose, in linear algebra, by definition, this means that the matrix multiplied by its Adjoint (Transpose Complex Conjugate) must result in an identity matrix.
In this section, we are going to define some unitary operators and show their behavior as they act on a single-qubit state:
# I operator arr_i=np.array([[1,0],[0,1]])
draw_bloch(arr_i @ arr_0)
Output:
Matrix: [[1.+0.j] [0.+0.j]]
The initial matrix did not change.
# x operator arr_x=np.array([[0,1],[1,0]])
draw_bloch(arr_x @ arr_0)
Output:
Matrix: [[0.+0.j] [1.+0.j]]
Notice that the state has changed to the |1⟩ state. This is also represented by the vector pointing to the South pole on the Bloch sphere, as shown in the following diagram:
Figure 6.4 – Bloch sphere showing the |1⟩ state
# y operator arr_y=np.array([[0,-1j],[1j,0]]) # z operator arr_z=np.array([[1,0],[0,-1]]) # h operator arr_h=(1/np.sqrt(2))*(np.array([[1, 1],[1, -1]])) # t operator arr_t=np.array([[1,0],[0,np.exp(1j*np.pi/4)]]) # s operator arr_s=np.array([[1,0],[0,np.exp(1j*np.pi/2)]])
The y operator rotates the vector similar to the x operator, only the rotation is around the Y-axis. This becomes more apparent when the initial vector does not start at the |0⟩ state. The z operator rotates the qubit state by π radians around the Z-axis. First, we would need to move the initial state away from |0⟩ to see any effect of this. The h operator (which also represents the Hadamard gate) rotates the state from the |0⟩ state to the |+⟩ state, which is on the front of the Bloch sphere on the X-axis, as shown in Figure 6.1.
This is equivalent to the following matrix multiplication:
draw_bloch(arr_h @ arr_0)
Output:
Matrix: [[0.70710678+0.j] [0.70710678+0.j]]
The fraction shows that the final state is as follows:
This is a superposition state where the qubit has an equal probability of becoming a 0 or a 1 when measured. The probability is , so the probability of |0⟩ is 0.5 and the probability of |1⟩ is 0.5.
The vector, as represented on the Bloch sphere, is at the equator of the X-axis. This location is called the |+⟩ state:
Figure 6.5 – Bloch sphere representing the qubit at the |+⟩ state
Figure 6.6 – Effect of using the Hadamard gate
In the preceding diagram, we applied the Hadamard gate twice:
arr_plus=arr_h @ arr_0 print(arr_plus)
Output:
[[0.70710678+0.j] [0.70710678+0.j]]
The same process can be represented with the following code:
arr_minus=draw_bloch(arr_z @ arr_plus)
Output:
Matrix: [[ 0.70710678+0.j] [-0.70710678+0.j]]
The Bloch sphere showing the qubit in the |-⟩ state is as follows:
Figure 6.7 – Bloch sphere showing the qubit state of |-⟩
Note the following:
The same process can be done with the following code:
arr_r=draw_bloch(arr_t @ arr_minus)
Output:
Matrix: [[ 0.70710678+0.j ] [-0.5 -0.5j]]
The final position on the qubit vector is on the equator of the Bloch sphere rotated π/4 from the |-⟩ state or rotated at an angle of 5/8 π from the |+⟩ state, as shown here:
Figure 6.8 – Bloch sphere showing the final state with a z-rotation angle of φ = (5/8) π
At this point, we have studied the behavior of one qubit with some simple rotations of the vector representing the qubit. We used the Bloch sphere to visualize the state vector of the qubit and also looked at the equivalent mathematical representation of the rotations using their equivalent unitary matrices.
Now, let’s build the same single-qubit rotations using the Amazon Braket Circuit() function and their equivalent quantum gates.
In the previous section, we worked with a single qubit, performed various operations on that single-qubit using matrix multiplication, and showed the results on a Bloch sphere. Now, let’s look at the same process using quantum gates using the Amazon Braket Circuit() function. We will use the custom draw_circuit() function, which will allow you to submit a single-qubit quantum circuit that will display the quantum circuit diagram, show the results after executing it using the LocalSimulator(), and display a Bloch sphere.
Let’s get started:
def draw_circuit(circ): circ=circ.state_vector() print(circ) device = LocalSimulator() result = device.run(circ).result() arr_r=np.array( [[result.values[0][0]],[result.values[0][1]]]) draw_bloch(arr_r)
circ=Circuit().i(0) draw_circuit(circ)
Output:
T : |0| q0 : -I- T : |0| Matrix: [[1.+0.j] [0.+0.j]]
As you can see, the circuit is represented in the first three lines and that after q0, we have only one gate, I. T represents the depth of the circuit. The resulting matrix shows that the state is still |0⟩. The following diagram shows the resulting Bloch sphere showing the same:
Figure 6.9 – Bloch sphere showing the |0⟩ state
circ=Circuit().h(0).z(0) draw_circuit(circ)
Output:
T : |0|1| q0 : -H-Z- T : |0|1| Matrix: [[ 0.70710678+0.j] [-0.70710678+0.j]]
The qubit is now at the |-⟩ state. The Bloch sphere should also confirm this:
Figure 6.10 – Bloch sphere showing the |-⟩ state
circ=Circuit().h(0).t(0) draw_circuit(circ)
Output:
T : |0|1| q0 : -H-T- T : |0|1| Matrix: [[0.70710678+0.j ] [0.5 +0.5j]]
The Bloch sphere shows the qubit state as follows:
Figure 6.11 – Bloch sphere after starting at |0⟩ and applying an H and a T gate
The equivalent circuit can be set up with the following code:
circ=Circuit().x(0).h(0).s(0) draw_circuit(circ)
Output:
T : |0|1|2| q0 : -X-H-S- T : |0|1|2| Matrix: [[0.70710678+0.j 0.] [0. -0.70710678j]]
The Bloch sphere representation is as follows:
Figure 6.12 – Bloch sphere in the |-i⟩ state
Most of the gates we have used so far had predefined rotations around the X or Z-axis. We can also apply specific angle rotations to the qubit around any of the three axes. We will look at the rotation functions using an example in the next section.
We can apply specific rotations around each axis using the rx(q,θ), ry(q,θ), or rz(q,φ) gates. Here, q is the qubit number, and θ and φ are the rotation angles in radians. Thus, any value from 0 to 2π or 0 to -2π will fully rotate the vector around that axis back to the starting point. Any multiples will cause multiple rotations.
To try out two new gates RY and RZ, represented by the following functions, ry() and rz(), we will encode the time of the day in the Bloch sphere. Looking back at Figure 6.1, we will use the rotation around the Y-axis or the θ angle to represent the hour of the day and the φ angle to represent the minutes.
Thus, through the day, the qubit vector will spiral down from the |0⟩ state, representing midnight, to the equator, which will be at noon, and then continue to spiral down to the |1⟩ state toward midnight again. The top of the hour (:00) will be represented with a phase of π degrees. Thus, we will use the ry(θ) gate to rotate the qubit vector in the -y direction to represent the top of the hour. Thus, θ represents a fraction of the 24 hours through the day.
To represent the clockwise rotation of the minutes and seconds around the Z-axis, we will rotate the φ angle in the reverse direction using the rz() gate, starting at π degrees. The bloch_clock() code is as follows:
def bloch_clock(): from time import gmtime import datetime import numpy as np time=gmtime() #set to EDT HR=time.tm_hour-4 if HR<0: HR=HR+24 MIN=time.tm_min SEC=time.tm_sec print''HR'',HR,''MIN'',MIN,''SEC'', SEC,''ED'') circ=Circuit().ry(0,-(HR+MIN/60 +SEC/3600)*np.pi/24).rz(0,-(MIN+SEC/60)*2*np.pi/60) draw_circuit(circ)
Output:
HR: 11 MIN: 35 SEC: 30 EDT T : | 0 | 1 | q0 : -Ry(-1.52)-Rz(-3.72)- T : | 0 | 1 | Additional result types: StateVector Matrix: [[-0.20612405+0.69586314j] [ 0.19539087+0.65962852j]]
For 11:35:30, the Bloch Clock shows the following output:
Figure 6.13–- Bloch Clock showing 11:35:30
You can get your own time on the Bloch sphere by running bloch_clock(). Please adjust the hour to your time zone by adjusting the constant in the HR=time.tm_hour-4 line.
We are now going to start dealing with multiple qubits. It is not practical to show a multiple qubit state space using a Bloch sphere, even though there are different ways to split the information into phase diagrams or show partial information on a single sphere. In any case, next, we will focus on using larger matrices that represent more qubits and also show how matrix multiplication can easily be represented by using gates on a quantum circuit.
In this section, we will build the matrix math for a full circuit that contains multiple qubits and then show how the results match the output from a quantum gate circuit. So far, we have used matrix multiplication and the @ symbol to multiply the original state with a series of matrices that represent unitary operators or quantum gates on one qubit. When we are dealing with multiple qubits, the vector space of each will have to be multiplied together using the tensor product, which is typically represented by the ⊗ symbol. We will use the NumPy kron() function to calculate this:
Figure 6.14 – Matrix multiplication of a quantum gate circuit
The preceding diagram shows a sample quantum circuit with various gates (squares) each representing a 2x2 matrix. The initial state on each qubit is going to be the |0⟩ state. To represent this quantum circuit using matrix multiplication, we can use the tensor product between each of the gates to create a unitary matrix that represents a column of gates. Thus, to create U1, we need the tensor product between 1a, 1b, and 1c. To get the state, we need to apply the unitary matrix, U1, to the prior state, , using matrix multiplication. Each subsequent column of gates is applied the same way to the previous state, or they can be multiplied together as a single unitary and then applied once to the initial state to get the final state of the circuit. Let’s build the matrix math of this circuit step by step:
For n qubits, we could write the following general expression:
We also get the following:
We also get the following equation:
This can also be expanded for clarity:
Having looked at the general process of matrix multiplication to represent a quantum circuit, in the next section, we will use this process to get the final state of a specific circuit with single-qubit and two-qubit gates. You should keep in mind that to ensure each unitary has the correct size, any missing gates can be filled with the I identity matrix. Technically, in an actual quantum circuit, gates will be stacked to the left to eliminate any gaps, unless this is prevented due to a multiple qubit gate.
We will now use what we have learned to create a slightly more complicated quantum circuit on three qubits. The diagram below shows this sample circuit with four unitary gate operations on the three qubits. The first and last operation has all Hadamard gates, while U2 has single qubit gates, and U3 has a CNOT gate with an Identity gate. We have looked at all these steps already, but now we will put it together to get the final state of this quantum circuit through both matrix multiplication and then finally using quantum gates.
Figure 6.15 – Sample circuit to create
Now, let’s use this information to learn how to calculate the end state matrix for a set of gates in a three-qubit quantum circuit, as shown in the preceding diagram:
arr_psi_i=np.kron(arr_0,np.kron(arr_0, arr_0)) print(arr_psi_i)
Output:
[[1.+0.j] [0.+0.j] [0.+0.j] [0.+0.j] [0.+0.j] [0.+0.j] [0.+0.j] [0.+0.j]]
The equivalent matrix representation involves the tensor product of the three matrices:
Thus, the first unitary matrix will be the tensor product of three Hadamard gates:
arr_U1=np.kron(arr_h,np.kron(arr_h, arr_h)) print(arr_U1)
Output:
[[ 0.35355339 0.35355339 0.35355339 0.35355339 0.35355339 0.35355339 0.35355339 0.35355339] [ 0.35355339 -0.35355339 0.35355339 -0.35355339 0.35355339 -0.35355339 0.35355339 -0.35355339] [ 0.35355339 0.35355339 -0.35355339 -0.35355339 0.35355339 0.35355339 -0.35355339 -0.35355339] [ 0.35355339 -0.35355339 -0.35355339 0.35355339 0.35355339 -0.35355339 -0.35355339 0.35355339] [ 0.35355339 0.35355339 0.35355339 0.35355339 -0.35355339 -0.35355339 -0.35355339 -0.35355339] [ 0.35355339 -0.35355339 0.35355339 -0.35355339 -0.35355339 0.35355339 -0.35355339 0.35355339] [ 0.35355339 0.35355339 -0.35355339 -0.35355339 -0.35355339 -0.35355339 0.35355339 0.35355339] [ 0.35355339 -0.35355339 -0.35355339 0.35355339 -0.35355339 0.35355339 0.35355339 -0.35355339]]
def run_circuit_local(circ): # This function prints the quantum circuit and # calculates the final state vector # This can be used for multiple qubits and only # uses the local simulator # The Bloch sphere is not printed circ=circ.state_vector() print(circ) device = LocalSimulator() result = device.run(circ).result() arr_r=np.array(result.values).T print(arr_r) return(arr_r)
circ_psi_1=Circuit().h([0,1,2]) result=run_circuit_local(circ_psi_1)
Output:
T : |0| q0 : -H- q1 : -H- q2 : -H- T : |0| Additional result types: StateVector [[0.35355339+0.j] [0.35355339+0.j] [0.35355339+0.j] [0.35355339+0.j] [0.35355339+0.j] [0.35355339+0.j] [0.35355339+0.j] [0.35355339+0.j]]
The matrix code to build U2 is as follows:
arr_U2=np.kron(arr_x,np.kron(arr_y, arr_z)) print(arr_U2)
Output:
[[ 0.+0.j 0.+0.j 0.+0.j 0.+0.j 0.+0.j 0.+0.j 0.-1.j 0.+0.j] [ 0.+0.j -0.+0.j 0.+0.j 0.+0.j 0.+0.j -0.+0.j 0.+0.j 0.+1.j] [ 0.+0.j 0.+0.j 0.+0.j 0.+0.j 0.+1.j 0.+0.j 0.+0.j 0.+0.j] [ 0.+0.j 0.-0.j 0.+0.j -0.+0.j 0.+0.j 0.-1.j 0.+0.j -0.+0.j] [ 0.+0.j 0.+0.j 0.-1.j 0.+0.j 0.+0.j 0.+0.j 0.+0.j 0.+0.j] [ 0.+0.j -0.+0.j 0.+0.j 0.+1.j 0.+0.j -0.+0.j 0.+0.j 0.+0.j] [ 0.+1.j 0.+0.j 0.+0.j 0.+0.j 0.+0.j 0.+0.j 0.+0.j 0.+0.j] [ 0.+0.j 0.-1.j 0.+0.j -0.+0.j 0.+0.j 0.-0.j 0.+0.j -0.+0.j]]
arr_psi_2=arr_U2 @ arr_psi_1 print(arr_psi_2)
Output:
[[0.-0.35355339j] [0.+0.35355339j] [0.+0.35355339j] [0.-0.35355339j] [0.-0.35355339j] [0.+0.35355339j] [0.+0.35355339j] [0.-0.35355339j]]
circ_psi_2=circ_psi_1.x(0).y(1).z(2) result=run_circuit_local(circ_psi_2)
Output:
T : |0|1| q0 : -H-X- q1 : -H-Y- q2 : -H-Z- T : |0|1| Additional result types: StateVector [[0.-0.35355339j] [0.+0.35355339j] [0.+0.35355339j] [0.-0.35355339j] [0.-0.35355339j] [0.+0.35355339j] [0.+0.35355339j] [0.-0.35355339j]]
As you can see, the resulting state vector from the circuit is the same as that when using matrix multiplication.
We will simply show how a CNOT gate works in the following steps before using this gate in the circuit:
arr_cx=np.array([[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 0, 1], [0, 0, 1, 0]])
arr_10= np.kron(arr_1, arr_0) print(arr_10)
Output:
[[0.+0.j] [0.+0.j] [1.+0.j] [0.+0.j]]
arr_11= arr_cx @ arr_10 print(arr_11)
Output:
[[0.+0.j] [0.+0.j] [0.+0.j] [1.+0.j]]
Since the first qubit is in a state of |1⟩, the second qubit will change states from |0⟩ to |1⟩. Thus, we will see a |11⟩ resulting state.
The code is as follows:
arr_U3=np.kron(arr_i,arr_cx) print(arr_U3)
Output:
[[1 0 0 0 0 0 0 0] [0 1 0 0 0 0 0 0] [0 0 0 1 0 0 0 0] [0 0 1 0 0 0 0 0] [0 0 0 0 1 0 0 0] [0 0 0 0 0 1 0 0] [0 0 0 0 0 0 0 1] [0 0 0 0 0 0 1 0]]
arr_psi_3=arr_U3 @ arr_psi_2 print(arr_psi_3)
Output:
[[0.-0.35355339j] [0.+0.35355339j] [0.-0.35355339j] [0.+0.35355339j] [0.-0.35355339j] [0.+0.35355339j] [0.-0.35355339j] [0.+0.35355339j]]
circ_psi_3=circ_psi_2.i(0).cnot(1,2) result=run_circuit_local(circ_psi_3)
Output:
T : |0|1|2| q0 : -H-X-I- q1 : -H-Y-C- | q2 : -H-Z-X- T : |0|1|2| Additional result types: StateVector [[0.-0.35355339j] [0.+0.35355339j] [0.-0.35355339j] [0.+0.35355339j] [0.-0.35355339j] [0.+0.35355339j] [0.-0.35355339j] [0.+0.35355339j]]
circ_psi_3=Circuit().h([0,1,2]).x(0).y(1).z(2).i(0).cnot(1,2).h([0,1,2]) arr_psi_3=run_circuit_local(circ_psi_3)
Output:
T : |0|1|2|3| q0 : -H-X-I-H- q1 : -H-Y-C-H- | q2 : -H-Z-X-H- T : |0|1|2|3| Additional result types: StateVector [[0.-2.36158002e-17j] [0.-1.00000000e+00j] [0.-1.26316153e-34j] [0.-9.52420783e-18j] [0.+0.00000000e+00j] [0.+0.00000000e+00j] [0.+0.00000000e+00j] [0.+0.00000000e+00j]]
circ_psi_3=Circuit().h([0,1,2]).x(0).y(1).z(2).i(0).cnot(1,2).h([0,1,2]) device = LocalSimulator() result = device.run(circ_psi_3, shots=1000).result() counts = result.measurement_counts print(counts) # plot using Counter plt.bar(counts.keys(), counts.values()); plt.xlabel('v'lue')' plt.ylabel('c'unts')'
Output:
Counter({'0'1':'1000})
The resulting bar chart also shows the same result of getting 001 only:
Figure 6.16 – Resulting probabilities after measuring the circuit using LocalSimulator()
With that, we have completed several steps showing how quantum gates are equivalent to specific matrices and that the structure of a quantum circuit can be simulated using a step-by-step process, which involves creating unitary matrix operators that act on the previous state vector. Initially, we looked at the process of matrix multiplication for a single-qubit example. We also saw that we can extend this process to multiple qubits, so long as we take the tensor product of each column of gates to ensure they are converted into a unitary matrix. Then, we multiplied the matrix of the initial state with the first unitary, which applies Hadamards to the three qubits to bring them into superposition. We did the same to the second state, which applies the single-qubit x, y, and z gates, and then the third, which applies an identity gate to the first qubit, followed by a two-qubit control gate or the CNOT gate on the next two qubits, which entangle the two qubits together. The final step involved adding Hadamards to all the qubits to remove the superposition, just before the circuit is measured. We started with a state of all zeros and returned with 001, which is measured 100% of the time through this circuit.
This simple circuit lays the foundation for the next section, where we will look at a modified example inspired by the Google Supremacy experiment.
We will apply what we have learned about gate operations to an experiment inspired by the Google Supremacy experiment. The paper titled Quantum supremacy using a programmable superconducting processor was published in Nature in October 2019. The links to this paper and the counter-claim by IBM can be found in the Further reading section. This is a good example to start with, as it does not require a specific quantum circuit, and can also explain, to some extent, the process used in the widely talked about experiment.
The Google experiment used an alternating set of randomly selected single-qubit gates (). It entangled one of a different set of interconnected qubits using a combination of iSWAP and a CrZ (π /6) gate that had been calibrated for minimum error between that specific pair of qubits. This includes five other rotation angles that I have not shown.
The following equation shows the matrices for the aforementioned and gates:
The gate is defined as .
The two-qubit gate uses a combination of an iSWAP and CrZ(), where y = π /6. Other rotations for optimizing the two-qubit gate for the highest fidelity of the qubit pair interaction have not been shown here:
The single and multiple qubit gates were repeated r times, where r=20. Since this experiment was done on their 54-qubit Sycamore quantum processor with one qubit not functioning properly, this led to a 53-qubit system and thus 253 different possible states. As we have seen, based on the matrix calculations, to simulate one step of 53 gates on 53 qubits, we must find the tensor products of 53 gates, which creates a matrix of size 253x253. The team stated that the RAM availability in the Jülich supercomputer (with 100,000 cores and 250 terabytes) limited calculations to up to 48 qubits. Beyond that, other hybrid methods were used. The claim that was made was that with the quantum processor, these calculations, with r=20 and 1 million measurement samples, were performed in 200 seconds, while classical processors would require 50 trillion core hours and consume 1 petawatt hour of energy (estimated to take 10,000 years with a million-core supercomputer). Counterclaims were made by IBM and other teams.
For our purposes, we will create a circuit where we can randomly select single-qubit gates for each qubit, and then use the two-qubit CNOT gate to entangle qubits. We will use identity gates so that we can move the CNOT gates up and down, thus randomly entangling different sets of qubits. The following diagram shows two sample repetitions of this circuit. We will calculate the full circuit’s unitary using tensor products and matrix multiplication, along with its equivalent gate circuit, and run it on the local simulator and the Amazon Braket SV1 and TN1 simulators. Finally, we will look at the performance of both the Rigetti and IonQ quantum devices:
Figure 6.17 – Google Quantum supremacy-inspired circuit with alternating random single-qubit gates (X, Y, Z, I, T) and randomly placed CNOT gates repeated r times
The function that creates the circuit and equivalent unitary for the full circuit is qc_rand() and is provided in the code for this chapter. All you need to do is provide the number of qubits and the number of repeats, r, in the circuit. The last parameter is a Boolean to create the unitary. This is True by default, but if your classical processor cannot generate the unitary (around 11 or 12 qubits), set Matrix=False.
We will start with seven qubits, q=7, and set the repeats to two, r=2. This allows us to see all the different options with good results.
Note – Code Preparation
As you follow along, do not accidentally execute a run on the quantum devices (the IonQ Device and Aspen) unless you are willing to budget $32 and $1.42 for this configuration.
Execute the available_devices() function to ensure you have the latest device names, adjust the code accordingly, and run experiments on the devices available at the time you execute the code.
Please execute the estimate_cost() or estimate_cost_measured_qubits() function before running these devices with the appropriate number of qubits and desired number of shots. In the following code, I have executed these functions to give you price estimates.
At the end of this section, I will go over some insights as to which configurations do not work or give poor results. Then, you can try out different configurations at the end of this section, though note that the IonQ execution can get very expensive. It can cost up to $100 if you’re executing the circuit on 11 qubits with the maximum number of suggested shots at 10,000. Executing a 32-qubit circuit on Rigetti with 100,000 shots can cost $35.00.
We will start with a small circuit with 7 qubits and repeat the alternating single and two-qubit gates twice. Since we will also apply Hadamard gates, this is a gate depth of 6. Due to the number of qubits, the circuit should work on all local devices, simulators, and quantum devices and provide good results.
Note – Each Run Will Generate a Different Circuit
Since the circuit is randomly generated, when you run the code, you will end up generating a different circuit with different results. Have fun!
Later, we will compare the results when using larger circuits.
In this section, we will review the code as we implement the circuit using matrix math and use simulators on local devices. Let’s get started:
q=7 r=2 Ufinal, g_circ=qc_rand(q,r)
The equation returns both the randomly generated unitary, Ufinal, and its equivalent quantum circuit, g_circ.
print(g_circ)
Output:
T : |0|1|2|3|4|5| q0 : -H-X-I-X-C-H- | q1 : -H-I-C-X-X-H- | q2 : -H-I-X-X-C-H- | q3 : -H-T-C-T-X-H- | q4 : -H-X-X-X-C-H- | q5 : -H-X-C-X-X-H- | q6 : -H-Z-X-Z-I-H- T : |0|1|2|3|4|5|
Notice that the gate position, T:0, has the Hadamard gates, the T:1 position has the random single-qubit gates (X,I,T, and Z), and the T:2 position has the two-qubit CNOT gates. Positions T:3 and T:4 repeat with a new set of single-qubit gates and a different positioning of the CNOT gates to entangle a different set of qubits. Finally, T:5 has Hadamard gates on all its qubits. We can use the values of T to determine the gate depth of the circuit or use the g_circ.depth function.
print(Ufinal)
Output:
[[-7.13653841e-20+2.87907611e-19j 7.13653841e-20+5.79454127e-19j -5.00000000e-01-5.00000000e-01j ... 7.13653841e-20-5.79454127e-19j 1.42730768e-19-2.89363173e-18j 0.00000000e+00+0.00000000e+00j] [ 7.13653841e-20-2.87907611e-19j 5.00000000e-01+5.00000000e-01j -7.13653841e-20-5.79454127e-19j ... -1.80608886e-18+1.44681586e-18j 0.00000000e+00+0.00000000e+00j 1.42730768e-19+5.75815222e-19j] [ 1.66335809e-18-2.02263109e-18j 7.13653841e-20-5.79454127e-19j 7.13653841e-20-1.44681586e-18j ... 7.13653841e-20+5.79454127e-19j 1.42730768e-19+1.15890825e-18j 0.00000000e+00+0.00000000e+00j] ... [-7.13653841e-20+2.87907611e-19j 1.80608886e-18+1.44681586e-18j 7.13653841e-20+5.79454127e-19j ... 5.27553581e-18-1.44681586e-18j 0.00000000e+00+0.00000000e+00j -1.42730768e-19-5.75815222e-19j] [-8.60225200e-18+5.49207804e-18j -7.13653841e-20+5.79454127e-19j -7.13653841e-20+1.44681586e-18j ... -7.13653841e-20-5.79454127e-19j -1.42730768e-19-1.15890825e-18j 0.00000000e+00+0.00000000e+00j] [-7.13653841e-20-2.87907611e-19j 7.13653841e-20-1.44681586e-18j 7.13653841e-20-5.79454127e-19j ... 7.13653841e-20+1.44681586e-18j 0.00000000e+00+0.00000000e+00j -1.42730768e-19+5.75815222e-19j]]
Keep in mind that this is a 27 x 27 (128x128) matrix.
n=q x_val=[] y_val=[] init_arr=np.zeros((2**n,1)) init_arr[0]=1 shots=1000 result=shots*np.square(np.abs(Ufinal @ init_arr)) y_val_t=(result.T.ravel()) for i in range (len(y_val_t)): y_val.append(int(100*y_val_t[i]/shots)) x_val=range(2**n) x_index=np.argsort(x_val) for i in (x_index): if y_val[i]>=1: print(x_val[i],':', y_val[i]) plt.bar(x_val, y_val) plt.xlabel('states'); plt.ylabel('percent'); plt.title('q='+str(n)+', r='+str(r)+ ' Unitary Matrix Multiplication') plt.rcParams["figure.figsize"] = (30,30) plt.rcParams['figure.dpi'] = 150 plt.show()
Output:
6 : 49 30 : 49
There are a total of 128 possible states or values based on the seven qubits that are used. This circuit produces two results: one is 6 with a probability of 49% and the other is 30 with a probability of 49%. These values have been rounded, and there might be other probabilities smaller than 1% that have been ignored for our purposes:
Figure 6.18 – Resulting state probabilities from matrix multiplication
targets=[] circ_Ufinal=Circuit() for I in range (n): targets.append(i) circ_Ufinal=circ_Ufinal.unitary(matrix=Ufinal, targets=targets) print(circ_Ufinal)
Output:
T : |0| q0 : -U- | q1 : -U- | q2 : -U- | q3 : -U- | q4 : -U- | q5 : -U- | q6 : -U- T : |0|
The preceding output shows a quantum circuit built out of only one unitary matrix.
device = LocalSimulator() shots=1000 result = device.run(circ_Ufinal, shots=shots).result() counts = result.measurement_counts #print(counts) x_val=[] y_val=[] for i in (counts.keys()): x_val.append(int(i,2)) for i in (counts.values()): y_val.append(int(100*i/shots)) x_index=np.argsort(x_val) for i in (x_index): if y_val[i]>=1: print(x_val[i],':', y_val[i]) plt.bar(x_val, y_val, color='b') plt.title('q='+str(n)+', r='+str(r)+ ' Unitary on LocalSimulator') plt.xlabel('states'); plt.ylabel('percent'); plt.rcParams["figure.figsize"] = (20,10) plt.rcParams['figure.dpi'] = 300 plt.show()
Output:
6 : 52 30 : 47
Notice that the results in the output are the same two values as in Step 4; that is, 6 and 30. However, the percentages are a bit different due to all the other smaller probabilities being rounded:
Figure 6.19 – Resulting state probabilities after using a gate circuit based on the single unitary
result = device.run(g_circ, shots=shots).result()
Output:
6 : 48 30 : 52
Again, the two values that are in the output have approximately the same percentages they had previously:
Figure 6.20 – The resulting state probabilities after using the gate quantum circuit on LocalSimulator
With that, we have evaluated the quantum circuit using matrix math and done the same on local simulators. In the next subsection, we will use the Amazon Braket simulators.
In this section, we will continue running the 7 x 2 circuit on both the SV1 and TN1 Amazon Braket simulators:
device_name='SV1' device=set_device(device_name) estimate_cost(device)
Output:
Device('name': SV1, 'arn': arn:aws:braket:::device/quantum-simulator/amazon/sv1) simulator cost per minute : $ 0.075 total cost cannot be estimated
shots=estimate_cost_measured_qubits(device, n)
Output:
max shots: 100000 for 7 measured qubits the number of shots recommended: 3,200 simulator cost per minute : $ 0.075 total cost cannot be estimated
title='q='+str(n)+', r='+str(r)+' Unitary on '+device_name result=run_circuit(device, circ_Ufinal, shots, s3_folder, title, False)
Output:
6 : 50 30 : 49
This results in the following chart:
Figure 6.21 – Resulting state probabilities after using the unitary circuit on SV1
title='q='+str(n)+', r='+str(r)+' Gate circuit on '+device_name result=run_circuit(device, g_circ, shots, s3_folder, title, False)
Output:
6 : 50 30 : 49
This results in the following chart:
Figure 6.22 – Resulting state probabilities after using the gate circuit on SV1
device_name='TN1' device=set_device(device_name) estimate_cost(device)
Output:
Device('name': TN1, 'arn': arn:aws:braket:::device/quantum-simulator/amazon/tn1) simulator cost per minute : $ 0.275 total cost cannot be estimated
shots=estimate_cost_measured_qubits(device, n)
Output:
max shots: 1000 for 7 measured qubits the maximum allowed shots: 1,000 simulator cost per minute : $ 0.275 total cost cannot be estimated
title='q='+str(n)+', r='+str(r)+' Gate circuit on '+device_name result=run_circuit(device, g_circ, shots, s3_folder, title, False)
Output:
6 : 49 30 : 50
The probabilities of the two states are also shown in the following bar chart:
Figure 6.23 – Resulting state probabilities after using the gate circuit on TN1
So far, we have seen that all these devices practically give the same results – at least, at the current number of qubits. This was intended to drive the point that all simulators should give the same values and that those values are consistent with matrix multiplication, which we learned about in this chapter.
In the next subsection, we will execute the same circuit, g_circ, on the IonQ device, as well as on the Rigetti Aspen M-1 device.
So far, we have executed the random circuit on simulators. Now, we will run it on quantum devices. We have access to IonQ, which has 11 qubits, and Rigetti’s Aspen M-1 quantum computer, which has 80 qubits. Of course, we will only use 7 qubits for this circuit. This process will give you a hands-on perspective regarding the performance of current quantum computers. Follow these steps:
Warning
Please only execute this section of the code if you are willing to be charged the cost of the quantum devices based on the number of shots submitted.
device_name='IonQ Device' device=set_device(device_name) estimate_cost(device)
Output:
Device('name': IonQ Device, 'arn': arn:aws:braket:::device/qpu/ionq/ionQdevice) device cost per shot : $ 0.01 total cost for 1000 shots is $10.30
shots=estimate_cost_measured_qubits(device, n)
Output:
max shots: 10000 for 7 measured qubits the number of shots recommended: 3,200 device cost per shot : $ 0.01 total cost for 3200 shots is $32.30
title='q='+str(n)+', r='+str(r)+ ' Gate circuit on '+device_name result=run_circuit(device, g_circ, shots, s3_folder, title, False)
Output:
0 : 1 2 : 2 6 : 44 14 : 1 22 : 2 26 : 2 30 : 36 38 : 1
The following bar chart shows the results:
Figure 6.24 – Resulting state probabilities after running the gate circuit on the IonQ device
This is the first time we have seen other resulting values with small probabilities. It should be noted that the IonQ device has fully connected qubits and a reasonably low error rate. However, the probabilities of our two primary numbers have reduced to 44% and 36%, respectively. With more repetitions, these probabilities will reduce further due to errors arising in the execution of the gate operations.
Note – Using Native Gates to Improve Results
We have not used native gate operations for IonQ. Native gate operations are ideal for quantum computers. Single-qubit gate operations and especially two-qubit gate operations that use native gates have the least error. More information on this can be found in the Further reading section.
Note – Aspen M-1 Connectivity
The device.properties.dict()['paradigm']['connectivity'] function can be used to determine the connectivity between qubits.
The following code shows how to determine the Rigetti-M-1 quantum processor's connectivity and embed the quantum circuit on qubits that are connected:
device_name='Aspen-M-1' device=set_device(device_name) estimate_cost(device)
Output:
Device('name': Aspen-M-1, 'arn': arn:aws:braket:us-west-1::device/qpu/rigetti/Aspen-M-1) device cost per shot : $ 0.00035 total cost for 1000 shots is $0.65
shots=estimate_cost_measured_qubits(device, n)
Output:
max shots: 100000 for 7 measured qubits the number of shots recommended: 3,200 device cost per shot : $ 0.00035 total cost for 3200 shots is $1.42
device.properties.dict()['paradigm']['connectivity']
Output:
{'fullyConnected': False, 'connectivityGraph': {'0': ['1', '103', '7'], '1': ['0', '16', '2'], '10': ['11', '113', '17'], '100': ['101', '107'], '101': ['100', '102', '116'], '102': ['101', '103', '115'], '103': ['0', '102', '104'], … '2': ['1', '15'], … '5': ['4', '6'], '6': ['5', '7'], '7': ['0', '6']}}
Based on this output, it appears that qubits 2 and 3 are not connected. The qubit connectivity appears to be as follows:
Figure 6.25 – Rigetti M-1 qubit connectivity
Since qubits 2 and 3 are not connected, the default embedding will use different qubit combinations and can result in a mapping we do not prefer. To hardcode the embedding, we will need to recreate the quantum circuit with the required qubit numbers.
m=[10,11,12,13,14,15,16] q=7 r=2 n=q g_circ=Circuit().h([m[0],m[1],m[2],m[3],m[4],m[5],m[6]]) g_circ=g_circ.x(m[0]).i(m[1]).i(m[2]).t(m[3]).x(m[4]).x(m[5]).z(m[6]) g_circ=g_circ.i(m[0]).cnot(m[1],m[2]).cnot(m[3],m[4]).cnot(m[5],m[6]) g_circ=g_circ.x(m[0]).x(m[1]).x(m[2]).t(m[3]).x(m[4]).x(m[5]).z(m[6]) g_circ=g_circ.cnot(m[0],m[1]).cnot(m[2],m[3]).cnot(m[4],m[5]).i(m[6]) g_circ=g_circ.h([m[0],m[1],m[2],m[3],m[4],m[5],m[6]]) print(g_circ)
Output:
T : |0|1|2|3|4|5| q10 : -H-X-I-X-C-H- | q11 : -H-I-C-X-X-H- | q12 : -H-I-X-X-C-H- | q13 : -H-T-C-T-X-H- | q14 : -H-X-X-X-C-H- | q15 : -H-X-C-X-X-H- | q16 : -H-Z-X-Z-I-H- T : |0|1|2|3|4|5|
The preceding code creates the same random circuit we used on the simulators and IonQ. However, in this case, we are defining specific qubit numbers we want to use on the Rigetti Aspen M-1 device. This circuit is specifically for the Rigetti QPU as other devices do not allow us to skip qubit numbers. To enforce this embedding on the Rigetti device, we need to use the disable_qubit_rewiring=True parameter, as shown here:
result = device.run(circuit, shots=shots, s3_destination_folder=s3_folder, disable_qubit_rewiring=True).result()
Please review the run_rigetti() code to see how this is used. The rest of the code is the same as it is for run_circuit(). Let's proceed.
title='q='+str(n)+', r='+str(r)+ ' Gate circuit on '+device_name result=run_rigetti(device, g_circ, shots, s3_folder, title, False)
Output:
Device('name': Aspen-M-1, 'arn': arn:aws:braket:us-west-1::device/qpu/rigetti/Aspen-M-1) 0 : 1 2 : 2 4 : 1 6 : 11 10 : 1 14 : 4 22 : 1 24 : 1 26 : 2 30 : 11 38 : 3 46 : 2 54 : 1 58 : 1 62 : 5 70 : 1 94 : 1 98 : 1 102 : 5 110 : 2 118 : 2 120 : 1 122 : 1 126 : 5
The output values are in percent. As shown in the following chart, we have many more results showing up in the output with smaller probabilities:
Figure 6. 26 – Resulting state probabilities after running the gate circuit on the Rigetti Aspen-M-1 device
The probability of detecting the two primary numbers, 6 and 30, is now at 11%. Any more repetitions in this circuit and we would not be able to detect the expected output. Also, if we "carelessly" placed a circuit on the Aspen architecture and didn't try to carefully optimize the circuit to qubit connections, we would see more errors.
This exercise, along with the code samples and functions, should help you create sample quantum circuits or even build an equivalent unitary matrix using tensor products and matrix multiplication. The value will come from finding real use cases that require tensor products and then determining the equivalent gate model. The simulators can provide cost-effective ways of testing considerably large circuits. The Google Supremacy experiment is a great learning opportunity. You can try using different gates with random rotations as well, such as the RX, RY, and RZ gates. Many quantum gates are in the literature, and it is also important to review the gates that are available in the quantum devices. Gates such as xx, yy, and zz are available on IonQ and provide unique and flexible unitary matrices and qubit rotations to work with.
In this chapter, we looked at the basics of quantum gates and quantum circuits. Quantum circuits provide you with a unique opportunity to practice tensor products and work with complex numbers. We worked with simple matrices and showed how unitary matrices represent quantum gates. First, we experimented with a single-qubit system and looked at an example of encoding the time on a single qubit. Then, we learned how a quantum circuit with multiple qubits can be represented by tensor products and matrix multiplication, which leads to simulated results. Amazon Braket provides its own set of simulators that can take gate circuit instructions and provide the resulting probabilities. We saw that by using Amazon Braket simulators, it is possible to simulate considerably large circuits. Even though these classical resources take time, it will be advantageous to build up your skills to solve problems with these simulators while better quantum computers are developed that have been scaled up with more qubits and are less error-prone. We took our knowledge of developing a quantum circuit to build a random circuit inspired by the Google Supremacy experiment. We compared the results between our unitary matrix and those from the Amazon local simulator, the SV1 and TN1 simulators, and the IonQ and Rigetti AspenM-1 quantum computers.
Having gone over the basics of quantum circuits, in the next chapter, we will take the next step toward quantum algorithms.
To learn more about the topics that were covered in this chapter, take a look at the following resources:
device.properties.dict()['action']['braket.ir.jaqcd.program']['supportedOperations']
device.properties.paradigm.nativeGateSet
device.properties.dict()['paradigm']['connectivity']
Code deep dive:
Please review the code for the following functions, which were provided as part of the code for this chapter and can be found in this book’s GitHub repository:
3.145.77.114