Quantum Computing#
Author:
Date:
Time Spent on this Assignment:
!pip install qiskit[visualization];
!pip install qutip
!pip install qiskit_aer;
import numpy as np
import pylab as plt
import qiskit
import qutip
from qiskit import QuantumCircuit, transpile
from qiskit_aer import AerSimulator
from qiskit.visualization import plot_histogram
def run_me(circuit, a=None):
"""Run a quantum circuit and return its final state and measurement counts.
Inputs:
circuit (QuantumCircuit): the circuit to execute.
a (Statevector, optional): an initial state to prepare before ``circuit``.
If ``None``, the simulation starts from |0...0>.
Returns:
statevector (Statevector): the saved statevector immediately before the
measurement step.
counts (dict[str, int]): measurement outcomes from a single shot, keyed
by bitstring.
"""
numQubits=circuit.num_qubits
if a!=None:
numQubits=max(numQubits,a.num_qubits)
initCircuit=QuantumCircuit(a.num_qubits,a.num_qubits)
initCircuit.initialize(a)
circuit=add_circuits([initCircuit,circuit])
circuit.save_statevector(label='myStateVector')
compiled_circuit = transpile(circuit, simulator)
resultA = simulator.run(compiled_circuit).result()
forward=list(range(0,numQubits))
reverse=forward[::-1]
circuit.measure(forward,reverse)
compiled_circuit = transpile(circuit, simulator)
resultB = simulator.run(compiled_circuit).result()
return resultA.data()['myStateVector'],resultB.data()['counts']
def make_state(v):
"""Build a qiskit ``Statevector`` from a numpy array of amplitudes.
Inputs:
v (array-like): complex amplitudes of length ``2**N`` for some N.
Returns:
statevector (Statevector): the corresponding qiskit state.
"""
numWires=int(round(np.log2(len(np.array(v)))))
qc1=QuantumCircuit(numWires,numWires)
qc1.initialize(v)
a,_=run_me(qc1)
return a
def print_dirac(out_state_a):
"""Print a state vector in Dirac (ket) notation.
Inputs:
out_state_a (array-like): complex amplitudes of length ``2**N``.
Components close to zero are omitted from the printout.
"""
out_state=np.asarray(out_state_a)
l=len(out_state)
num_qubits=int(round(np.log(len(out_state))/np.log(2)))
for i in range(0,l):
if not np.isclose(out_state[i],0):
print(str(out_state[i])+'|'+bin(i)[2:].zfill(num_qubits)[::-1]+'>',end=' + ')
print()
def add_bloch_vector(v, bb):
"""Add the Bloch-sphere vector for a single-qubit state to ``bb``.
Inputs:
v (array-like): length-2 complex amplitudes of a single-qubit state.
bb (qutip.Bloch): Bloch sphere object; the new vector is appended to it
in place.
"""
vv=np.asarray(v)
a=vv[0]*np.conj(vv[0])
b=vv[1]*np.conj(vv[0])
x = np.real(2.0 * b.real)
y = np.real(2.0 * b.imag)
z = np.real(2.0 * a - 1.0)
bb.add_vectors([x,y,z])
return
def plot_state(v, plot_all=True, my_title=""):
"""Plot the real and imaginary parts of a quantum state vector.
The x-axis indexes the ``2**N`` basis states; red sticks show real parts
and blue sticks show imaginary parts. A dashed line marks the average
amplitude, which is useful when visualising Grover-style "inversion about
the mean".
Inputs:
v (array-like): the quantum state vector to plot.
plot_all (bool): if ``True`` plot every component; if ``False`` plot only
the first half (useful when the last qubit is an ancilla).
my_title (str): title displayed above the plot.
"""
if plot_all:
N=len(np.asarray(v))
else:
N=len(np.asarray(v))//2
vv=np.zeros(N,dtype=complex)
vv[:]=np.asarray(v)[0:N]
vv=vv*np.exp(-1.j*np.angle(vv[0]))
plt.axhline(0)
for idx,i in enumerate(vv):
plt.plot([int(idx),int(idx)],[0,np.real(i)],color='red')
plt.plot([int(idx)],[np.real(i)],'o',color='red')
for idx,i in enumerate(vv):
plt.plot([idx+0.1,idx+0.1],[0,np.imag(i)],color='blue')
plt.plot([idx+0.1],[np.imag(i)],'o',color='blue')
avg=np.average(vv)
plt.axhline(np.real(avg),linestyle='--')
plt.ylim(-1,1)
plt.title(my_title)
plt.show()
def add_circuits(the_circuits):
"""Concatenate several circuits into a single new circuit.
Inputs:
the_circuits (list[QuantumCircuit]): circuits to compose, applied in
order. They may have different numbers of qubits; the result has as
many wires as the largest input.
Returns:
circuit (QuantumCircuit): the composed circuit.
"""
numQubits=np.array([c.num_qubits for c in the_circuits])
numQubits=np.max(numQubits)
circuit=QuantumCircuit(numQubits,numQubits)
for i in range(0,len(the_circuits)):
circuit=circuit.compose(the_circuits[i],qubits=list(range(0,the_circuits[i].num_qubits)))
return circuit
def mark(r, N):
"""Build the Grover oracle that flips the sign of the basis state |r>.
Inputs:
r (int): index of the basis state to mark.
N (int): total number of qubits in the circuit.
Returns:
circuit (QuantumCircuit): the oracle circuit on N qubits.
"""
circuit=QuantumCircuit(N,N)
circuit.barrier()
myString=np.binary_repr(r,width=N)[::-1]
for i in range(0,len(myString)):
if myString[i]=='0':
circuit.x(i)
circuit.barrier()
circuit.h(N-1)
circuit.mcx(list(range(0,N-1)), N-1,mode='noancilla')
circuit.h(N-1)
circuit.barrier()
for i in range(0,len(myString)):
if myString[i]=='0':
circuit.x(i)
circuit.barrier()
return circuit
def initialize_circuit_random(N):
"""Build a circuit that prepares a random real state on N qubits.
Inputs:
N (int): number of qubits.
Returns:
circuit (QuantumCircuit): an N-qubit circuit initialised to a random
normalised real-valued state of dimension ``2**N``.
"""
r=np.random.random(2**N)
r=r/np.linalg.norm(r)
circuit=QuantumCircuit(N,N)
circuit.initialize(list(r))
return circuit
def initialize_circuit(my_string, N):
"""Build a circuit that prepares a specific computational basis state.
Inputs:
my_string (str): bitstring of length N. ``'00000'`` is all spin-up,
``'11111'`` is all spin-down.
N (int): number of qubits.
Returns:
circuit (QuantumCircuit): an N-qubit circuit prepared in
``|my_string>``.
"""
circuit = QuantumCircuit(N,N)
circuit.initialize(my_string[::-1])
return circuit
simulator = AerSimulator()
Exercise 1: A Single Qubit#
a. A one qubit state#
A quantum state consists of a certain fraction of \(|0\rangle\) and a certain fraction of \(|1\rangle\) - i.e. \(\sqrt{0.3} |0\rangle - \sqrt{0.7} |1\rangle\).
We can plot this state in a variety of ways.
state=np.array([np.sqrt(0.3),-np.sqrt(0.7)])
state=make_state(state)
print_dirac(state)
plot_state(state)
b=qutip.Bloch()
add_bloch_vector(state,b)
b.render()
b.show()
Go ahead and try it out
### ANSWER HERE
Let’s go ahead and plot these three states
\(|0\rangle \equiv [1,0]\)
\(|1\rangle \equiv [0,1]\)
\(1/\sqrt{2} |0\rangle + 1/\sqrt{2} |1\rangle \equiv [1/\sqrt{2},1/\sqrt{2}]\)
Pay special attention to where those three states are on the Bloch Sphere
### ANSWER HERE
b. One qubit gates#
Quantum circuits are made out of quantum gates. Let us start by considering 1-qubit gates. The 1-qubit gate rotates states around the Bloch sphere. Three special gates are rx, ry, and rz which respectively rotate around the X, Y, and Z axis. These gates take an angle and a wire - i.e. rx(1/3*np.pi,0) will rotate \(1/3 \pi\) around the X axis.
To set up your circuit you can do
circuit=QuantumCircuit(1,1)
#Build your circuit here
state,measure = run_me(circuit) #<--- This runs the circuit
# now you can plot the circuit
Go ahead and figure out how to use rx gate to move from a \(|0\rangle\) to a \(|1\rangle\) state.
### ANSWER HERE
Now we want to go ahead and use ry to go from \(|0\rangle \rightarrow 1/\sqrt{2}|0\rangle + 1/\sqrt{2}|1\rangle\). Write such a circuit. Then check to see what your circuit does to the state \(|1\rangle\) (you can get this using your other circuit from above).
### ANSWER HERE
So far we’ve seen we can get half \(|0\rangle\) and half \(|1\rangle\). We can also adjust states to get more of \(|1\rangle\) than \(|0\rangle\) (or vice versa). Generically we can get a state \(\cos(\theta) |0\rangle + \sin(\theta) e^{i\phi} |1\rangle\).
If we had such a state, after measurement we get “0” with probability \(\cos^2\theta\) and “1” with probability \(\sin^2\theta\).
Notice that \(\cos^2\theta + \sin^2\theta =1\) so we either get “0” or “1”.
To produce this state with \(\phi=0\), we can use the gate qc.ry(2*theta,wire) which takes \(|0\rangle \rightarrow \cos \theta |0\rangle + \sin \theta |1\rangle\)
Produce the state \(\cos(0.5)|0\rangle + \sin(0.5)|1\rangle\) and check to make sure it gives you the right amplitudes.
### ANSWER HERE
Now, make a circuit that rotates rx and rz both by \(0.3 \pi\).
### ANSWER HERE
Notice that by smartly choosing the angles around rx and rz and then rx again you can produce an arbitrary rotation on the sphere and therefore take \(|0\rangle\) and \(|1\rangle\) to any two orthogonal states.
c. Seeing the rotation#
We would like to plot the rotation around the Bloch sphere. We can do this as follows:
b=qutip.Bloch()
for angle in np.arange(0,2*np.pi*0.8,0.1):
quantumWires=1
qc = QuantumCircuit(quantumWires,quantumWires)
# rotate around the x-axis by angle theta
vec,output=run_me(qc)
add_bloch_vector(vec,b)
b.render()
b.show()
### ANSWER HERE
d. Measuring the state#
So far we’ve been cheating by looking at the quantum state. In the real world, you can’t do that. Instead you have to measure at the end of your circuit. The run_me circuit returns two parameters. The second one is measurement outcomes - i.e.
vector,measure=run_me(circuit)
can then plot the measurement outcomes by doing
plot_histogram(measure)
Go ahead and measure the outcomes of a state rotated around the X axis by \(0.3\pi\). See that it gives you the expected fraction of 0 and 1. Remember that the probability should be equal to the amplitude squared.
### ANSWER HERE
Exercise 2: Two qubits#
In this exercise we will see how to build quantum states of two qubits.
a. Control-not gates#
The controlled-not gate: The key gate for two qubits is the control-not gate (circuit.cx(0,1)). The key gate “nots” the second wire (wire 1) if the first wire is “1” (wire 0). Let’s check it out. Apply the “control-not” to a state \(|00\rangle\) and to a state \(|10\rangle\) and print out the state (not the measurement outcome)
### ANSWER HERE
b. Build an EPR Pair#
The most interesting two qubit state is an EPR pair, \(\frac{1}{\sqrt{2}}\left(|00\rangle + |11\rangle\right)\). You can build it with a Hadamard and a CNOT. Go ahead and try different things and figure out how to build yourself an EPR pair. You can start out with
quantumWires=2
classicalWires=2
qc=QuantumCircuit(quantumWires,classicalWires)
to start out a quantum circuit with two wires. You can’t plot this on the Bloch sphere but you can still plot it with plot_state.
### ANSWER HERE
c. Measuring EPR Pairs#
Now that you’ve made an EPR pair, let’s go ahead and measure your EPR circuit above plotting the histogram of your measurements.
### ANSWER HERE
There is something very powerful here. If you get a “0” on the top wire, then you always get a “0” on the bottom wire. If you get a “1” on the top wire, then you get a “1” on the bottom wire. This is even the case if these wires are taken miles apart before you measure.
Exercise 3: Grover’s Algorithm#
In this exercise, we are going to implement Grover’s algorithm. Grover’s algorithm solves the search problem
Grover’s algorithm takes a unitary which marks an element \(i\) and uses that as a subroutine with more circuit elements so that, after measurement, you sample the marked element \(i\).
To do this we are going to need to put together a lot of different circuit elements. Let’s start with the unitary which does the marking which we will give you. In this case it’s going to mark element 3 by taking it and changing its sign. Go ahead and run this to see explicitly what it does.
## Check Mark
circuit=initialize_circuit_random(5)
a,b=run_me(circuit)
plot_state(a,True,my_title="Initial Random State")
circuit=mark(3,5)
a,b=run_me(circuit,a)
plot_state(a,True,my_title="Hopefully marked state")
### ANSWER HERE
a. Control-Control-Control Z#
Define a function control_z(N) which generates the control-control-control-control Z on \(n\) qubits. The first \(n-1\) qubits have to be all 1 for the last Z qubit to happen.
To do this, implement the following circuit for general values of N. This is an example for control_z(5).
You can get a control-control-control-control-x from qiskit by doing the following:
circuit.mcx(list(range(0,N-1)), N-1,mode='noancilla')
where \(N-1\) is the number of target wires.
Test out your circuit by using the following code:
initCircuit=initialize_circuit('11110',5)
#initCircuit=initialize_circuit('11010',5)
initCircuit.h(4)
circuit=add_circuits([initCircuit,control_z(5)])
print(circuit)
a,b=run_me(circuit)
print_dirac(a)
You should get something close to
(0.7071067811865475+8.659560562354932e-17j)|11110> + (-0.7071067811865476-8.659560562354934e-17j)|11111>
and then if you comment out the second initialization with 11010 you will get
(0.7071067811865476+0j)|11010> + (0.7071067811865475+0j)|11011>
### ANSWER HERE
b. All Hadamards#
Define a function all_hadamard(n) which builds \(n\) wires of Hadamards
Check it by the following test:
circuit=all_hadamard(5)
a,b=run_me(circuit)
plot_state(a,True)
and seeing that it produces a state which is uniform. This is what we will use for the start of Grover’s algorithm.
### ANSWER HERE
c. flip_all_but_zero#
Define flip_all_but_zero(n). which should flip all but the zero configurations on \(n\) bits. It will use \(n+1\) wires (you need an ancilla, which is an extra bit that doesn’t get read out later. We will plot our state using plot_state(a,False) to ignore this extra bit). The example below is the output for flip_all_but_zero(4)
You might want to add some circuits together. To add these circuits you can do
add_circuits([circuit1,circuit2,circuit3])
You can test it your result by doing
circuit=initialize_circuit_random(4)
a,b =run_me(circuit)
plot_state(a,True,my_title="Initial State")
circuit=flip_all_but_zero(4)
a,b =run_me(circuit,a)
plot_state(a,False,my_title="Flipped Circuit")
You should find that everything but the zero flipped. It is ok if you are finding the zero flipped and nothing else. Quantum states aren’t defined up to a global phase and so the flipping of everything is equivalent to doing nothing.
### ANSWER HERE
d. invert#
Define the invert(N) function. This will also be on \(N+1\) wires. Here you need to add together the all_hadamard and flip_all_but_zero and all_hadamard again.
You can test it in the following way:
circuit=initialize_circuit_random(4)
a,b=run_me(circuit)
plot_state(a,True)
circuit=invert(4)
a,b=run_me(circuit,a)
plot_state(a,False)
This will generate a random state and then invert it around the mean (shown in a blue dotted line). Your mean before and after should be the same and everything else should flip around that dotted line. (Also everything totally flipping is still allowed)
### ANSWER HERE
e. Running Grover’s Algorithm#
Put everything together to run Grover’s algorithm.
### ANSWER HERE
Acknowledgement:
Bryan Clark (original)
Copyright: 2025