# Needed to set up the quantum circuit
from qiskit import QuantumCircuit, ClassicalRegister, QuantumRegister
# Needed to simulate running a quantum computer
from qiskit_aer import AerSimulator
# Neded to visualize the results of running a quantum computer
from qiskit.visualization import plot_histogram
# Needed for vectors and stuff
import numpy as np
Bernstein-Varirani Algorithm and Quantum Fourier Transforms
Bernstein-Vazirani algorithm
The (Absolute Best) Classical Implementation
# This is the "secret string" we want to determine
# Use a vector here to make the dot product easier
= [1,1,0,1,0,1]
s
# Get the length of the string and create an array to hold
# the components of s as we recreate them
= len(s)
n = np.zeros(n)
reconstructed_s
# Iterate through all indices in the s list
# This iteration runs as many times as there are elements in s
# s is queried n times
for i in range(n):
# Create a vector of length n that is all zeros and make only
# the current index 1
= np.zeros(n)
vec = 1
vec[i] # Apply the f(x) function with the vector and the secret string
= np.dot(vec,s)%2
f # f is the ith component of s
= f
reconstructed_s[i]
print(s)
print(reconstructed_s)
[1, 1, 0, 1, 0, 1]
[1. 1. 0. 1. 0. 1.]
Quantum Implementation
The below code is modified from this example provided by IBM through their Qiskit textbook.
Nice but fast video on the topic
# Secret string
# Using a string here to replicate the IBM implementation
= "110101"
s # Get the length of the string
= len(s)
n
# Need n+1 qubits. n are mapped to the n classical bits,
# the last one is used as the oracle/auxillary
= QuantumRegister(n+1)
q # Need n classical bits
= ClassicalRegister(n)
c # creates a quantum circuit that maps the result of a qubit
# to a classical bit
= QuantumCircuit(q, c)
circuit
# Add a not gate the the oracle qubit
# the n+1 qubits are indexed 0...n, so the last qubit is index n
circuit.x(n) # just a visual aid for now
circuit.barrier()
# Apply a Hadamard gate to all qubits, this let's you do it in one line
# range(n+1) returns [0,1,2,...,n] in Python. This covers all the qubits
range(n+1))
circuit.h(# The first n qubits are in the |+> state
# and the last qubit is in the |-> state
# just a visual aid for now
circuit.barrier()
# enumerate returns the index and value for each element in the string
# Reversed because of the way qiskit displays the results
# Note though that s is only referenced one time
# The number of queries to s is just 1
for i, bit in enumerate(reversed(s)):
# If the current bit is a one, then add a controlled not
# from that bit to the oracle
if bit == '1':
# i is the control qubit, n is the target qubit
circuit.cx(i, n) # HOWEVER, the oracle qubit is in the |-> state and all of the
# other qubits are in the |+> state
#So phase kickback changes the value of the qubits whose position
# corresponds to a 1 in the secret string
# just a visual aid for now
circuit.barrier()
# Apply Hadamard gates to all the qubits
# So any remaining in the |+> state become 0 and any that were
# converted to the |-> state become 1
range(n+1))
circuit.h(# just a visual aid for now
circuit.barrier()
# measure the qubits indexed from 0 to n-1 and store them into the
# classical bits indexed 0 to n-1
# Do not convert the state of the oracle to a classical bit
range(n), range(n)) circuit.measure(
<qiskit.circuit.instructionset.InstructionSet at 0x11bd5c670>
# The argument makes the circuit look nicer
# There are a few more options as well
# Check the documentation!!
='mpl') circuit.draw(output
# Simulate the circuit, all results should give us the "secret string"
= AerSimulator()
simulator = simulator.run(circuit).result().get_counts()
results # Print the secret string and then show the histogram of results
print(s)
plot_histogram(results)
110101
Quantum Fourier Transforms
Classical Implementation
##############################
## CALCULATE OMEGA ##
##############################
def calculate_omega (N):
"""
Calculate the omega constant for the DFT matrix
Arguments:
N (int): the size of the DFT matrix
Returns:
omega (complex): the omega constant
"""
= np.exp(-2*np.pi*1.0j/N)
omega return omega
##############################
## DFT MATRIX ##
##############################
def DFT_matrix (N):
"""
Constructs the DFT matrix at a given size
Arguments:
N (int): the size of the DFT matrix
Returns:
dft_matrix (a square matrix of complex values): the DFT matrix
"""
# Placeholder for the DFT matrix. SET THE TYPE TO COMPLEX!!
= np.ones((N,N), dtype=complex)
dft_matrix # First column and first row are ones,
# Only need to change indices 1 through N-1
# for rows and columns
= calculate_omega(N)
omega
# Iterate over the rows
for r in range(1,N):
# Iterate over the columns
for c in range(1,N):
# Calculate the matrix element for each opening in the matrix
= (omega)**(r*c)
dft_matrix[r,c]
# Tack on the scaling factor and then return the matrix
/= np.sqrt(N)
dft_matrix return dft_matrix
# Compute the DFT matrix for two data points
# Look familar?
2) DFT_matrix(
array([[ 0.70710678+0.00000000e+00j, 0.70710678+0.00000000e+00j],
[ 0.70710678+0.00000000e+00j, -0.70710678-8.65956056e-17j]])
# Compute the DFT matrix for 4 data points
# Round to make the output a bit neater
round(DFT_matrix(4),2) np.
array([[ 0.5+0.j , 0.5+0.j , 0.5+0.j , 0.5+0.j ],
[ 0.5+0.j , 0. -0.5j, -0.5-0.j , -0. +0.5j],
[ 0.5+0.j , -0.5-0.j , 0.5+0.j , -0.5-0.j ],
[ 0.5+0.j , -0. +0.5j, -0.5-0.j , 0. -0.5j]])
##############################
## DFT ##
##############################
def dft (x):
"""
Performs a discrete fourier transform on a given data set and
returns the transformed data set
Arguments:
x (a Numpy array): the data set to be transformed (should be complex)
Returns:
X (a Numpy array): the transformed data set (should be complex and the
same length as x)
"""
# Get the length of the data set and then compute the DFT matrix
= len(x)
N = DFT_matrix(N)
dft_matrix
# Perform DFT by multiplying the data set by the DFT matrix
= np.dot(dft_matrix, x)
X
return X
# Create an example data set, make sure it is complex
= np.array([1, 2, 3, 4], dtype=complex)
x # Apply DFT to the data set
= dft(x)
X
print("Input Data:", x)
print("DFT of the Data:", X)
Input Data: [1.+0.j 2.+0.j 3.+0.j 4.+0.j]
DFT of the Data: [ 5.+0.0000000e+00j -1.+1.0000000e+00j -1.-4.8985872e-16j
-1.-1.0000000e+00j]
# Compare to the results of using Numpy's discrete fourier transform library
# Note here that fft stands for Fast Fourier Transform, which is an optimized
# version of the discrete fourier transform
= np.fft.fft(x)
X_np print(X_np)
[10.+0.j -2.+2.j -2.+0.j -2.-2.j]
# Note that the above results are not the same as our function because Numpy
# does not apply the normalization constant to the
= np.fft.fft(x, norm="ortho")
X_np print(X_np)
[ 5.+0.j -1.+1.j -1.+0.j -1.-1.j]
Quantum Implementation
The below code is once again modified from the Qiskit Textbook GitHub.
Quick Detour Through Some New Gates and Qiskit Functionality
# This import let's us visualize the Qiskit qubits as state vectors (bra-ket notation)
from qiskit.quantum_info import Statevector
# This import let's us visualize
from qiskit.visualization import plot_bloch_multivector
# Create a one qubit circuit with a Hadamard gate and visualize the output
= QuantumRegister(1)
q = ClassicalRegister(1)
c = QuantumCircuit(q,c)
qc
0)
qc.h(
qc.measure(q,c)="mpl") qc.draw(output
# Remove the measurement (want to keep the superposition to write the output)
# no measurements allowed
qc.remove_final_measurements() # Get the quantum state that is represented by the circuit
= Statevector(qc)
statevector # Display the state in formatted text
= 'latex'))
display(statevector.draw(output
# Convert the qubit to a bloch sphere representation
plot_bloch_multivector(statevector)
\[\frac{\sqrt{2}}{2} |0\rangle+\frac{\sqrt{2}}{2} |1\rangle\]
# Create a three qubit circuit, apply a NOT gate to the first qubit
= QuantumRegister(3)
q = ClassicalRegister(3)
c = QuantumCircuit(q,c)
qc 0)
qc.x(
# When the state vector is created, the number closest to the angled bracket is
# the first qubit, the number closest to the pipe is the last qubit
# no measurements allowed
qc.remove_final_measurements() = Statevector(qc)
statevector = 'latex'))
display(statevector.draw(output
# The block where prints the diagrams in order, though they are also labelled
# to prevent confusion
plot_bloch_multivector(statevector)
\[ |001\rangle\]
# Note that the state vectors will always be printed with rationalized fractions
# Meaning that here the normalization constants that result from the Hadamard gate are shown
# as sqrt(2)/2 instead of 1/sqrt(2) (these are equivalent)
= QuantumRegister(2)
q = ClassicalRegister(2)
c = QuantumCircuit(q,c)
qc 0)
qc.h(1)
qc.x(
qc.measure(q,c)
# no measurements allowed
qc.remove_final_measurements() = Statevector(qc)
statevector = 'latex'))
display(statevector.draw(output
plot_bloch_multivector(statevector)
\[\frac{\sqrt{2}}{2} |10\rangle+\frac{\sqrt{2}}{2} |11\rangle\]
## NEW GATE #1
## The S gate
## The S gate does nothing when applied to the up state
= QuantumRegister(1)
q = ClassicalRegister(1)
c = QuantumCircuit(q,c)
qc 0)
qc.s(
print(qc.draw())
# no measurements allowed
qc.remove_final_measurements() = Statevector(qc)
statevector = 'latex'))
display(statevector.draw(output
plot_bloch_multivector(statevector)
┌───┐
q5: ┤ S ├
└───┘
c4: 1/═════
\[ |0\rangle\]
## NEW GATE #1
## The S gate
## The S gate adds a phase factor (constant) of i to the down state
= QuantumRegister(1)
q = ClassicalRegister(1)
c = QuantumCircuit(q,c)
qc 0)
qc.x(0)
qc.s(
print(qc.draw())
# no measurements allowed
qc.remove_final_measurements() = Statevector(qc)
statevector = 'latex'))
display(statevector.draw(output
plot_bloch_multivector(statevector)
┌───┐┌───┐
q6: ┤ X ├┤ S ├
└───┘└───┘
c5: 1/══════════
\[i |1\rangle\]
## NEW GATE #1
## The Phase gate
## Adds a phase factor if the qubit is down
# Phase angle
= np.pi/4
theta
= QuantumRegister(1)
q = ClassicalRegister(1)
c = QuantumCircuit(q,c)
qc 0)
qc.x(0)
qc.p(theta,
print(qc.draw())
# no measurements allowed
qc.remove_final_measurements() = Statevector(qc)
statevector = 'latex'))
display(statevector.draw(output
plot_bloch_multivector(statevector)
┌───┐┌────────┐
q7: ┤ X ├┤ P(π/4) ├
└───┘└────────┘
c6: 1/═══════════════
\[(\frac{\sqrt{2}}{2} + \frac{\sqrt{2} i}{2}) |1\rangle\]
## NEW GATE #3
## The Rz gate
## Rotates the qubit around the z axis for a given number of radians
## Easier to see if we apply a Hadamard gate first to move the qubit
## off the z axis
# Angle of rotation
= np.pi/2
theta
= QuantumRegister(1)
q = ClassicalRegister(1)
c = QuantumCircuit(q,c)
qc 0)
qc.h(0)
qc.rz(theta,
print(qc.draw())
# no measurements allowed
qc.remove_final_measurements() = Statevector(qc)
statevector = 'latex'))
display(statevector.draw(output
plot_bloch_multivector(statevector)
┌───┐┌─────────┐
q8: ┤ H ├┤ Rz(π/2) ├
└───┘└─────────┘
c7: 1/════════════════
\[(\frac{1}{2} - \frac{i}{2}) |0\rangle+(\frac{1}{2} + \frac{i}{2}) |1\rangle\]
## NEW GATE #4
## The Ry gate
## Rotates the qubit around the y axis for a given number of radians
## What is this equivalent to?
# Angle of rotation
= np.pi/2
theta
= QuantumRegister(1)
q = ClassicalRegister(1)
c = QuantumCircuit(q,c)
qc 0)
qc.ry(theta,
print(qc.draw())
# no measurements allowed
qc.remove_final_measurements() = Statevector(qc)
statevector = 'latex'))
display(statevector.draw(output
plot_bloch_multivector(statevector)
┌─────────┐
q9: ┤ Ry(π/2) ├
└─────────┘
c8: 1/═══════════
\[\frac{\sqrt{2}}{2} |0\rangle+\frac{\sqrt{2}}{2} |1\rangle\]
## NEW GATE #5
## The Rx gate
## Rotates the qubit around the x axis for a given number of radians
# Angle of rotation
= np.pi/2
theta
= QuantumRegister(1)
q = ClassicalRegister(1)
c = QuantumCircuit(q,c)
qc 0)
qc.rx(theta,
print(qc.draw())
# no measurements allowed
qc.remove_final_measurements() = Statevector(qc)
statevector = 'latex'))
display(statevector.draw(output
plot_bloch_multivector(statevector)
┌─────────┐
q10: ┤ Rx(π/2) ├
└─────────┘
c9: 1/═══════════
\[\frac{\sqrt{2}}{2} |0\rangle- \frac{\sqrt{2} i}{2} |1\rangle\]
## NEW GATE #6
## The Controlled Phase gate
## Possibly applies a given phase to the target qubit
## This is the main gate for the quantum Fourier algorithm
# Phase angle
= np.pi/3
theta
= QuantumRegister(2)
q = ClassicalRegister(2)
c = QuantumCircuit(q,c)
qc # NOT gate for the control qubit to ensure the gate triggers
0)
qc.x(# NOT gate for the target qubit, qubit will only pick up a phase if down
1)
qc.x(# Could also apply a Hadamard gate and then the target qubit will sometimes
# get a phase
#qc.h(1)
# Controlled phase gate
# Arguments are phase angle, control qubit, target qubit
0,1)
qc.cp(theta,
print(qc.draw())
# no measurements allowed
qc.remove_final_measurements() = Statevector(qc)
statevector
= 'latex'))
display(statevector.draw(output plot_bloch_multivector(statevector)
┌───┐
q11_0: ┤ X ├─■───────
├───┤ │P(π/3)
q11_1: ┤ X ├─■───────
└───┘
c10: 2/══════════════
\[(\frac{1}{2} + \frac{\sqrt{3} i}{2}) |11\rangle\]
## One Qubit Example
= np.pi
pi
# Set up a one-qubit circuit
= QuantumRegister(1)
q = ClassicalRegister(1)
c = QuantumCircuit(q,c)
qc
# Test different starting conditions
#qc.x(0)
#qc.barrier()
# Apply a Hadamard gate to the qubit and done!
0)
qc.h(
# Draw the circuit
print(qc.draw())
# Print the Bloch sphere representation
= Statevector(qc)
statevector plot_bloch_multivector(statevector)
┌───┐
q12: ┤ H ├
└───┘
c11: 1/═════
## Two Qubit Example
# Set up a two-qubit circuit
= QuantumRegister(2)
q = ClassicalRegister(2)
c = QuantumCircuit(q,c)
qc
# Test different starting conditions
#qc.x(0)
#qc.x(1)
# Apply a Hadamard gate to the highest indexed qubit, so 1
1)
qc.h(
# Apply a controlled phase gate from 1 (target) to 0 (control)
# Calculate the angle of rotation
= pi/2**(1-0)
phi 0, 1)
qc.cp(phi,
## Apply a Hadamard gate to the next highest indexed qubit, so 0
0)
qc.h(
## Done with Hadamard and CPhase gate, now just swap the qubits
0,1)
qc.swap(
# Draw the circuit
print(qc.draw())
# Print the Bloch sphere representation
= Statevector(qc)
statevector
plot_bloch_multivector(statevector)
# Print the Bloch sphere representation
= Statevector(qc)
statevector plot_bloch_multivector(statevector)
┌───┐
q13_0: ──────■───────┤ H ├─X─
┌───┐ │P(π/2) └───┘ │
q13_1: ┤ H ├─■─────────────X─
└───┘
c12: 2/══════════════════════
## Three Qubit Example
# Set up a two-qubit circuit
= QuantumRegister(3)
q = ClassicalRegister(3)
c = QuantumCircuit(q,c)
qc
# Test different starting conditions
#qc.x(0)
#qc.x(1)
#qc.x(2)
#qc.barrier()
# Apply a Hadamard gate to the highest indexed qubit, so 2
2)
qc.h(
# Apply a controlled phase gate from 2 (target) to 1 (control)
# Calculate the angle of rotation
= pi/2**(2-1)
phi 1, 2)
qc.cp(phi,
# Apply a controlled phase gate from 2 (target) to 0 (control)
# Calculate the angle of rotation
= pi/2**(2-0)
phi 0, 2)
qc.cp(phi,
## Apply a Hadamard gate to the next highest indexed qubit, so 1
1)
qc.h(
# Apply a controlled phase gate from 1 (target) to 0 (control)
# Calculate the angle of rotation
= pi/2**(1-0)
phi 0, 1)
qc.cp(phi,
## Apply a Hadamard gate to the next highest indexed qubit, so 0
0)
qc.h(
## Done with Hadamard and CPhase gate, now just swap the qubits
## For three qubits it is sufficient to just swap the order of 0 and 2
0,2)
qc.swap(
# Draw the circuit
print(qc.draw())
# Print the Bloch sphere representation
= Statevector(qc)
statevector plot_bloch_multivector(statevector)
┌───┐
q16_0: ───────────────■─────────────■───────┤ H ├─X─
│ ┌───┐ │P(π/2) └───┘ │
q16_1: ──────■────────┼───────┤ H ├─■─────────────┼─
┌───┐ │P(π/2) │P(π/4) └───┘ │
q16_2: ┤ H ├─■────────■───────────────────────────X─
└───┘
c15: 3/═════════════════════════════════════════════
Generalized Code
def qft_rotations(circuit, n):
"""
Performs qft on the first n qubits in circuit (without swaps)
Inputs:
circuit is the qiskit circuit
n is the number of qubits in the circuit
Returns
circuit after adding Hadamard gates and controlled phase gates to perform
the qft
"""
# Break condition
if n == 0:
return circuit
# Subtract 1 because the highest indexed qubit is one less than the total number of qubits
-= 1
n # Hadamard to the highest indexed qubit
circuit.h(n)# Controlled phase gate with the qubit at n (the highest ordered) being the target and every
# lower indexed qubit being the control in order
for qubit in range(n):
= pi/2**(n-qubit)
phi
circuit.cp(phi, qubit, n)
# Recursion time!
# Recall the function with the same circuit, but n is one less than it was originally
# The next run-through will ignore the highest indexed qubit in this run-through
qft_rotations(circuit, n)
def swap_qubits(circuit, n):
"""
Performs swaps the locations of all qubits
Inputs:
circuit is the qiskit circuit
n is the number of qubits in the circuit
Returns
circuit after swapping all qubits
"""
# Int in range function because range requires an integer
for qubit in range(int(n/2)):
# swap the position of a low-indexed qubit and the qubit
# that is the same distance from the highest ordered qubit
-qubit-1)
circuit.swap(qubit, nreturn circuit
def qft(circuit, n):
"""
Performs QFT on a qiskit circuit by adding the correct gates to the circuit and then
swapping the order of the qubits
Inputs:
circuit is the qiskit circuit
n is the number of qubits in the circuit
Returns:
circuit is the qiskit circuit after the qft gates are applied
"""
qft_rotations(circuit, n)
swap_qubits(circuit, n)return circuit
## Example!
= QuantumCircuit(3)
qc
# Encode the state 5
0)
qc.x(2)
qc.x(print(qc.draw())
# no measurements allowed
qc.remove_final_measurements() = Statevector(qc)
statevector plot_bloch_multivector(statevector)
┌───┐
q_0: ┤ X ├
└───┘
q_1: ─────
┌───┐
q_2: ┤ X ├
└───┘
3)
qft(qc, qc.draw()
┌───┐ ┌───┐ q_0: ┤ X ├──────■──────────────────────■───────┤ H ├─X─ └───┘ │ ┌───┐ │P(π/2) └───┘ │ q_1: ───────────┼────────■───────┤ H ├─■─────────────┼─ ┌───┐┌───┐ │P(π/4) │P(π/2) └───┘ │ q_2: ┤ X ├┤ H ├─■────────■───────────────────────────X─ └───┘└───┘
# Bloch sphere diagram
# no measurements allowed
qc.remove_final_measurements() = Statevector(qc)
statevector plot_bloch_multivector(statevector)
## Important Note: the QFT creates a superposition of many possible states
## The phase factors added by the Hadamard gates and controlled phase gates will be important
## in applications
## Note that all possible three qubit states are represented here
= 'latex')) display(statevector.draw(output
\[\frac{\sqrt{2}}{4} |000\rangle+(- \frac{1}{4} - \frac{i}{4}) |001\rangle+\frac{\sqrt{2} i}{4} |010\rangle+(\frac{1}{4} - \frac{i}{4}) |011\rangle- \frac{\sqrt{2}}{4} |100\rangle+(\frac{1}{4} + \frac{i}{4}) |101\rangle- \frac{\sqrt{2} i}{4} |110\rangle+(- \frac{1}{4} + \frac{i}{4}) |111\rangle\]
= 5
n = QuantumCircuit(n)
qc 0)
qc.x(#qc.x(1)
2)
qc.x(#qc.x(3)
4)
qc.x(
qft(qc,n)print(qc.draw())
# no measurements allowed
qc.remove_final_measurements() = Statevector(qc)
statevector plot_bloch_multivector(statevector)
┌───┐ »
q_0: ┤ X ├──────■─────────────────────────────────────────■────────────────»
└───┘ │ │ »
q_1: ───────────┼─────────■───────────────────────────────┼────────■───────»
┌───┐ │ │ │ │ »
q_2: ┤ X ├──────┼─────────┼────────■──────────────────────┼────────┼───────»
└───┘ │ │ │ ┌───┐ │P(π/8) │P(π/4) »
q_3: ───────────┼─────────┼────────┼────────■───────┤ H ├─■────────■───────»
┌───┐┌───┐ │P(π/16) │P(π/8) │P(π/4) │P(π/2) └───┘ »
q_4: ┤ X ├┤ H ├─■─────────■────────■────────■──────────────────────────────»
└───┘└───┘ »
« ┌───┐
«q_0: ───────────────■──────────────────────■───────┤ H ├─X─
« │ ┌───┐ │P(π/2) └───┘ │
«q_1: ───────────────┼────────■───────┤ H ├─■─────────X───┼─
« ┌───┐ │P(π/4) │P(π/2) └───┘ │ │
«q_2: ─■───────┤ H ├─■────────■───────────────────────┼───┼─
« │P(π/2) └───┘ │ │
«q_3: ─■──────────────────────────────────────────────X───┼─
« │
«q_4: ────────────────────────────────────────────────────X─
«