from qiskit import QuantumRegister, QuantumCircuit, ClassicalRegister
from qiskit_aer import AerSimulator
import numpy as np
Variational Quantum Eigensolver (VQE)
def pairing_2p2h (g,d):
return np.array([[-g/2, 0], [0, 2*d-g/2]])
= pairing_2p2h(0.5, 1)
H
print("Hamiltonian")
print(H)
print("Adjoint of Hamiltonian")
print(H.conj().T)
print()
print("Is the Hamiltonian Hermitian?")
print(H == H.conj().T)
print()
print("Hamiltonian Multiplied by Adjoint")
print(H.dot(H.conj().T))
print()
print("Is the Hamiltonian Unitary?")
print(H.dot(H.conj().T) == np.eye(2))
Hamiltonian
[[-0.25 0. ]
[ 0. 1.75]]
Adjoint of Hamiltonian
[[-0.25 0. ]
[ 0. 1.75]]
Is the Hamiltonian Hermitian?
[[ True True]
[ True True]]
Hamiltonian Multiplied by Adjoint
[[0.0625 0. ]
[0. 3.0625]]
Is the Hamiltonian Unitary?
[[False True]
[ True False]]
def pairing_4p4h(g,d):
return np.array([
2*d-g, -g/2, -g/2, -g/2, -g/2, 0],
[-g/2, 4*d-g, -g/2, -g/2, 0, -g/2],
[-g/2, -g/2, 6*d-g, 0, -g/2, -g/2],
[-g/2, -g/2, 0, 6*d-g, -g/2, -g/2],
[-g/2, 0, -g/2, -g/2, 8*d-g, -g/2],
[0, -g/2, -g/2, -g/2, -g/2, 10*d-g]
[
])
= 0.5
g = 1.0
d
= np.linalg.eig(pairing_4p4h(g,d))
energies, states = states.T
states
for i in range(len(energies)):
print("Energy:", energies[i])
print("State:", states[i])
print("Normalized?", np.linalg.norm(states[i]))
print()
Energy: 1.416774284351102
State: [-0.98419512 -0.13663267 -0.07210914 -0.07210914 -0.04679042 -0.01013337]
Normalized? 1.0
Energy: 3.4706732152562574
State: [-0.1521038 0.97511354 0.10838838 0.10838838 0.0070972 0.04971482]
Normalized? 0.9999999999999999
Energy: 5.499999999999999
State: [-0.08512565 -0.17025131 0.68100522 0.68100522 0.17025131 0.08512565]
Normalized? 0.9999999999999999
Energy: 7.549436759324765
State: [ 0.03029686 -0.01194329 0.12855978 0.12855978 -0.97829206 -0.09396206]
Normalized? 1.0
Energy: 9.563115741067884
State: [-0.00775438 0.03683671 0.05250236 0.05250236 0.10825629 -0.99063137]
Normalized? 0.9999999999999999
Energy: 5.500000000000002
State: [ 0.01490915 0.02981829 0.57690389 -0.81545023 -0.02981829 -0.01490915]
Normalized? 1.0
= QuantumRegister(4)
q = ClassicalRegister(4)
c = QuantumCircuit(q,c)
qc
0,1])
qc.x([
="mpl") qc.draw(output
= QuantumRegister(4)
q = ClassicalRegister(4)
c = QuantumCircuit(q,c)
qc
0,2])
qc.x([
="mpl") qc.draw(output
= QuantumRegister(4)
q = ClassicalRegister(4)
c = QuantumCircuit(q,c)
qc
2,3])
qc.x([
="mpl") qc.draw(output
VQE With a Test System
## Definitions
# Pi
= np.pi
pi
# The up and down qubits
= np.matrix([[1],[0]],dtype=complex)
up = np.matrix([[0],[1]],dtype=complex)
down
# Defining some relevant quantum gate matrices
# Spefically the Pauli matrices
= np.matrix([[1,0],[0,1]],dtype=complex)
I = np.matrix([[0,1],[1,0]],dtype=complex)
X = np.matrix([[0,-1j],[1j,0]],dtype=complex)
Y = np.matrix([[1,0],[0,-1]],dtype=complex) Z
# 1. **Hamiltonian Preparation:** Define the Hamiltonian of the system then write it in terms of the Pauli operators.
# Define the Hamiltonian of the test system in terms of the Pauli matrices
def hamiltonian(A):
return (A/2)*(np.kron(X,X) + np.kron(Y,Y))
# Print a test version of the Hamiltonian
= 1
A = hamiltonian(A)
H print(H)
[[0.+0.j 0.+0.j 0.+0.j 0.+0.j]
[0.+0.j 0.+0.j 1.+0.j 0.+0.j]
[0.+0.j 1.+0.j 0.+0.j 0.+0.j]
[0.+0.j 0.+0.j 0.+0.j 0.+0.j]]
# Define and print the four two-qubit states
= np.kron(up, up)
state_00 = np.kron(up, down)
state_01 = np.kron(down, up)
state_10 = np.kron(down, down)
state_11
print("00:\n", state_00, "\n")
print("01:\n", state_01, "\n")
print("10:\n", state_10, "\n")
print("11:\n", state_11, "\n")
00:
[[1.+0.j]
[0.+0.j]
[0.+0.j]
[0.+0.j]]
01:
[[0.+0.j]
[1.+0.j]
[0.+0.j]
[0.+0.j]]
10:
[[0.+0.j]
[0.+0.j]
[1.+0.j]
[0.+0.j]]
11:
[[0.+0.j]
[0.+0.j]
[0.+0.j]
[1.+0.j]]
# Apply the Hamiltonian to the four states
# Note that only two of them give a non-zero results
# So a good attempt to create the trial wavefunction likely will
# involve the |01> and |10> states.
print("H|00>:\n", H@state_00, "\n")
print("H|01>:\n", H@state_01, "\n")
print("H|10>:\n", H@state_10, "\n")
print("H|11>:\n", H@state_11, "\n")
H|00>:
[[0.+0.j]
[0.+0.j]
[0.+0.j]
[0.+0.j]]
H|01>:
[[0.+0.j]
[0.+0.j]
[1.+0.j]
[0.+0.j]]
H|10>:
[[0.+0.j]
[1.+0.j]
[0.+0.j]
[0.+0.j]]
H|11>:
[[0.+0.j]
[0.+0.j]
[0.+0.j]
[0.+0.j]]
# These gates need to be applied to the quantum circuit to measure the YY portion
# of the Hamiltonian
# Note that Y = SXS_dag = SHZHS_dag, so we are taking the final two matrices of this
# expansion and applying them in reverse order (the two matrices after the Z gate).
def r_yy ():
= QuantumRegister(2)
q = ClassicalRegister(2)
c = QuantumCircuit(q,c)
qc
range(2))
qc.sdg(range(2))
qc.h(
return qc
# These gates are needed to measure the XX portion of the Hamiltonian.
# X = HZH, we we are taking the final gate, the one after the Z
def r_xx ():
= QuantumRegister(2)
q = ClassicalRegister(2)
c = QuantumCircuit(q,c)
qc
range(2))
qc.h(
return qc
# 2. **Create an Ansatz**: Create an iniital guess for the ground state of the quantum system,
# called an ansatz. It must be parameterized by a series of parameters called $\theta$.
# It can be hardware inspired or physically inspired.
# The below function creates the trial wavefunction proposed in the slides.
def ansatz(theta):
# create two-qubit quantum register
= QuantumRegister(2,'q')
q = ClassicalRegister(2,'c')
c
# initialize quantum circuit
= QuantumCircuit(q,c)
qc
# Apply phase shifts based on the desired phases for each state
0)
qc.h(1)
qc.x(0,1)
qc.cx(1)
qc.p(theta,# return circuit
return qc
# Let's check to see that we are creating the correct function
from qiskit.quantum_info import Statevector
= ansatz(pi/3)
wavefunction
= Statevector(wavefunction)
statevector = 'latex')) display(statevector.draw(output
\[\frac{\sqrt{2}}{2} |01\rangle+(\frac{\sqrt{2}}{4} + \frac{\sqrt{6} i}{4}) |10\rangle\]
# The below function computes the expectation value given the simultions of a
# quantum circuit using the Hamming weight formula presented in the slides.
## expecation value
def counts_to_exp(counts):
# populate empty counts with 0
for bs in ['00','01','10','11']:
if bs not in counts:
= 0
counts[bs]
# numerator of expecation value
= counts['00'] - counts['01'] - counts['10'] + counts['11']
num
# denominator of expectation value
= counts['00'] + counts['01'] + counts['10'] + counts['11']
den
# expectation value
= num/den
exp
return exp
# expecation value of the Hamiltonian
def exp_of_H(theta,A):
# 3. **Prepare the Quantum Circuit:** Initialize the quantum system into a simple r
# eference state and then apply the ansatz with specific values for $\theta$.
# The Hamiltonian is then "added" to the circuit using its Pauli operator form
# generated in step 1.
# prepare ansatz, one per expectation value
= ansatz(theta)
circ_xx = ansatz(theta)
circ_yy
# Add the appropriate gates for each expectation value
range(2), inplace=True)
circ_xx.compose(r_xx(), range(2), inplace=True)
circ_yy.compose(r_yy(),
# 4. **Measure the Expectation Value:** Run the circuit many times and get a
# measurement for each one. This is an estimation for the energy with the given
# set of parameters $\theta$.
# measure each circuit
circ_xx.measure(circ_xx.qubits,circ_xx.clbits)
circ_yy.measure(circ_yy.qubits,circ_yy.clbits)
# get the counts of each circuit
= AerSimulator()
simulator = simulator.run(circ_xx).result().get_counts()
counts_xx = simulator.run(circ_yy).result().get_counts()
counts_yy
# get the expectation value from each set of counts
= counts_to_exp(counts_xx)
exp_xx = counts_to_exp(counts_yy)
exp_yy
# get the expectation value of the Hamiltonian
= (A/2)*(exp_xx + exp_yy)
exp
# return the expectation value of the Hamiltonian
return exp
# Test the circuit with one particular value of theta
= pi/2
theta = exp_of_H(theta, 1)
exp_pi_2 print("The expected ground state energy is", exp_pi_2)
= hamiltonian(1)
H = np.linalg.eig(H)
eig_vals, eig_vecs = np.min(eig_vals).real
ground_state_E print("The true ground state energy is", np.round(ground_state_E, 3))
The expected ground state energy is 0.005859375
The true ground state energy is -1.0
# 5. **Classical Optimization**: Using a classical optimization algorithm, minimize the
# energy with repect to $\theta$.
# Here we will just use a brute force method to search through many possible values, but you
# also set up an optimization algorithm using scipy
= []
expected_E = None
minimim_theta = 1000
minimum_E for theta in np.linspace(0, 2*pi, 100):
= exp_of_H(theta,1)
E
expected_E.append(E)if E < minimum_E:
= E
minimum_E = theta
minimim_theta
= hamiltonian(1)
H = np.linalg.eig(H)
eig_vals, eig_vecs = np.min(eig_vals).real
ground_state_E print("The true ground state energy is", np.round(ground_state_E, 3))
print("The lowest energy found with VQE is", np.round(minimum_E, 3))
print("The difference in the energies is", ground_state_E - minimum_E)
print("This occured at an angle of theta equal to", np.round(minimim_theta, 3))
The true ground state energy is -1.0
The lowest energy found with VQE is -0.999
The difference in the energies is -0.0006510416666666297
This occured at an angle of theta equal to 3.11
# Plot the results of the search
import matplotlib.pyplot as plt
0, 2*pi, 100), expected_E, linewidth=2, color="b", label="VQE Energy")
plt.plot(np.linspace(-1, 0, 2*pi, color="r", linewidth=2, linestyle="--", label="True Ground State")
plt.hlines(r"$\theta$")
plt.xlabel(r"$\langle E \rangle$")
plt.ylabel(="upper right") plt.legend(loc
# Imports
%matplotlib inline
import matplotlib.pyplot as plt
import numpy as np
import scipy
from qiskit import QuantumCircuit, QuantumRegister, ClassicalRegister
from qiskit.circuit.library import UnitaryGate
## Definitions
= np.pi
pi
= np.matrix([[1,0],[0,1]],dtype=complex)
I = np.matrix([[0,1],[1,0]],dtype=complex)
X = np.matrix([[0,-1j],[1j,0]],dtype=complex)
Y = np.matrix([[1,0],[0,-1]],dtype=complex) Z
##############################
## SA ##
##############################
def SA(t):
"""
Inputs:
t (a float): the coefficeint to be used in the exponent
Returns:
SA (a Qiskit Gate): a Qiskit gate consisting of the A_a^i gate and a SWAp
gate.
Creates one of the gates in the SWAP network for below Fermi level qubit i and
above Fermi level qubit a. The gate is based on the equation for the ansatz with
a SWAP after the ansatz term is applied.
"""
# define SWAP matrix
= np.array([[1,0,0,0],[0,0,1,0],[0,1,0,0],[0,0,0,1]])
swap
# define matrix for SA (see the lecture notes for the equation form)
# Note that kron is the tensor product and expm is the scipy function for
# an exponent to a matrix (need to do a Taylor expansion for the result see
# the documentation)
# The result of this line is a matrix.
= swap @ scipy.linalg.expm(1.0j*(t/2)*(np.kron(X,Y)-np.kron(Y,X)))
SA
# Convert the Numpy array to a qiskit gate
= UnitaryGate(SA)
SA
# return gate
return SA
##############################
## ANSATZ ##
##############################
def ansatz(t_list):
"""
Inputs:
t_list (a Numpy array): a list of four numbers to be used in the exponent
of the operator. In order, they are the coefficeints for the i = 0, a = 2
state, the i = 0, a = 3 state, the i = 1, a = 2 state, and the i = 1, a = 3
Returns:
circ (a qiskit circuit): a qiskit circuit representing the ansatz of teh VQE a
algorithm
Prepares a quantum circuit in the ansatz for the pairing model.
"""
# create registers
= QuantumRegister(4,'q')
q = ClassicalRegister(4,'c')
c
# initialize quantum circuit
= QuantumCircuit(q,c)
circ
# prepare |Phi>, which is the Fock state or the
# ground state in second quantization
0])
circ.x(q[1])
circ.x(q[
# Apply the swap network, pulling the correct coefficient from the t_list
# for each pair of a and i values
2]),[q[1],q[2]]) # i = 1, a = 2
circ.append(SA(t_list[# The qubits are now in the order 0, 2, 1, 3
# So now 0 and 2 are beside each other, 1 and 3 are beside each other
# 0 and 2 now have indices 0 and 1 and 1 and 3 now have indices 2 and 3
0]),[q[0],q[1]]) # i = 0, a = 2
circ.append(SA(t_list[# The qubits are now in the order 2, 0, 1, 3
3]),[q[2],q[3]]) # i = 1, a = 3
circ.append(SA(t_list[# The qubits are now in the order 2, 0, 3, 1
# So qubits 0, 3 are beside each other with indices 1 and 2
1]),[q[1],q[2]]) # i = 0, a = 3
circ.append(SA(t_list[
# The final ordering of the qubits is 2, 0, 1, 3
# return circuit
return circ
##############################
## ROTATE TO XXXX ##
##############################
def rotate_to_xxxx(circ):
"""
Inputs:
circ (a qiskit circuit): a qiskit circuit set up as the ansatz
Returns:
circ (a qiskit circuit): the same circuit but with the gates applied
to rotate it to measuring in the x basis.
Applies the gates needed to measure in the x basis to all four qubits of the
circuit.
"""
range(4))
circ.h(return circ
##############################
## ROTATE TO YYYY ##
##############################
def rotate_to_yyyy(circ):
"""
Inputs:
circ (a qiskit circuit): a qiskit circuit set up as the ansatz
Returns:
circ (a qiskit circuit): the same circuit but with the gates applied
to rotate it to measuring in the y basis.
Applies the gates needed to measure in the y basis to all four qubits of the
circuit.
"""
range(4))
circ.sdg(range(4))
circ.h(return circ
##############################
## EXPECT 1 ##
##############################
def expect_1(counts,shots,p):
"""
Inputs:
counts (a qiskit dictionary): the results dictionary from running the Aer
simulation
shots (an int): the number of shots used in the simulation
p (an int): the current energy level being evaluated
Returns:
exp_val (a float): the one-body expectation value from the simulation
for a given energy level
Computes the one-body expectatoin value for a given energy level. Need
to add up contributions from all energy levels for total result.
"""
# calculate expectation value
= 0
exp_val = dict(counts)
counts for key in list(counts.keys()):
# check if pth qubit was measured to be 1
# meaning the pth level was occupied in this measurement
# thus contributing a one-body term
# If occupied then add the fraction of measurements to the
# expectation value
if key[p] == "1":
+= counts[key]/shots
exp_val
# return expectation value
return exp_val
##############################
## SIGN ##
##############################
def sign(measured_result, state):
"""
Inputs:
measured_result (a string): a measured result of simulated quantum circuit
state (a list): the current state being used to determine the two-body
expectation value
Returns:
Unnamed (an int): the sign to be added to the term when calculating the
expectation value
Computes the restricted Hamming weight by comparing a measured state to a given
state.
"""
# Determines where occupied energy levels are in state
# and adds the second quanitization value of the measured
# result to the Hamming weight. Result: if the state and the
# measured result share the same occupied energy level then
# add one to the Hamming weight
= [int(x) for x in measured_result]
bit_str
# calculate restricted Hamming weight
= 0
hw for i in range(len(bit_str)):
if state[i] == 1:
+= bit_str[i]
hw
return (-1)**hw
##############################
## EXPECT 2 ##
##############################
def expect_2(counts,shots,state):
"""
Inputs:
counts (a qiskit dictionary): the results dictionary from running the Aer
simulation
shots (an int): the number of shots used in the simulation
state (a list): a state of the pairing model in second quantization
Returns:
exp_val (a float): the two-body expectation value from the simulation for a
given state
Computes the two-body expectation value for a given state. Need to iterate
through all possible states for the total expectation value.
"""
# calculate expectation value
= 0
exp_val = dict(counts)
counts for measured_result in list(counts.keys()):
+= sign(measured_result,state)*counts[measured_result]/shots
exp_val
# Return expectation value
return exp_val
##############################
## SECOND QUANT STATE ##
##############################
def second_quant_state (i,j):
"""
Inputs:
i, j (ints): the filled energy levels of the state in second quantization
Returns:
term (a Numpy array): the state of the pairing model in second quantization
Creates the state for the pairing model in second quantization where i and j
are the filled energy levels.
"""
= np.zeros(4)
term # Create the filled energy levels
= 1
term[i] = 1
term[j] return term
##############################
## VQE COUNTS ##
##############################
def vqe_counts(t_list,pauli, shots):
"""
Inputs:
t_list (a Numpy array): a list of four numbers to be used in the exponent
of the operator. In order, they are the coefficeints for the i = 0, a = 2
state, the i = 0, a = 3 state, the i = 1, a = 2 state, and the i = 1, a = 3
pauli (a character): the character representing the Pauli matrix to measure
with respect to
shots (an int): the number of times to run the quantum simulation
Returns:
counts (a qiskit dictionary): the results of a simulating the circuit using the
defined ansatz and measuring with respect to the given Pauli matrix
Defines a quantum circuit in the state of the pairing model ansatz and then rotates the circuit
to be measured in the basis of the given Pauli operator.
"""
# create circuit
= ansatz(t_list)
circ
# rotate
# If identiy matrix then do nothing.
if pauli == 'I':
pass
# If X then add Hadamard gates to all qubits
if pauli == 'X':
= rotate_to_xxxx(circ)
circ
# If Y then add S Dagger the Hadamard gates to all qubits
if pauli == 'Y':
= rotate_to_yyyy(circ)
circ
# measure with new qubit order in mind and with Qiskit's reverse qubits
0],circ.clbits[1])
circ.measure(circ.qubits[1],circ.clbits[0])
circ.measure(circ.qubits[2],circ.clbits[3])
circ.measure(circ.qubits[3],circ.clbits[2])
circ.measure(circ.qubits[
# get counts
= AerSimulator()
simulator = simulator.run(circ, shots=shots).result().get_counts()
counts
return counts
##############################
## VQE EXPECT ##
##############################
def vqe_expect(t_list,constants):
"""
Inputs:
t_list (a Numpy array): a list of four numbers to be used in the exponent
of the operator. In order, they are the coefficeints for the i = 0, a = 2
state, the i = 0, a = 3 state, the i = 1, a = 2 state, and the i = 1, a = 3
constants (a list): a list of length two where the first element is g, the interaction
strength of the particles in the pairing model and the second element is the number of
times to simulate the quantum circuit.
Returns:
E (a float): the expectation value of the energy calculated by the VQE algorithm.
Computes the expectation value of the energy for the pairing model system using the variational
quantum eigensolver algorithm
"""
# extract contants
= constants[0]
g = constants[1]
shots
# get counts from each basis
= vqe_counts(t_list,'I', shots)
counts_i = vqe_counts(t_list,'X', shots)
counts_x = vqe_counts(t_list,'Y', shots)
counts_y
# initialize energy
= 0
E
# one-body term, iterate through all energy levels
for p in range(4):
+= (2*p-(g/2))*expect_1(counts_i,shots,p)
E
# two-body term, iterate through all possible states
for p in range(4):
for q in range(p+1,4):
= second_quant_state (p,q)
state += (-g/4)*(expect_2(counts_x,shots,state)+expect_2(counts_y,shots,state))
E
# Return energy
return E
##############################
## VQE ##
##############################
def vqe(t_list,g,shots):
"""
Inputs:
t_list (a Numpy array): a list of four numbers to be used in the exponent
of the operator. In order, they are the coefficeints for the i = 0, a = 2
state, the i = 0, a = 3 state, the i = 1, a = 2 state, and the i = 1, a = 3
g (a float): the interaction strength of the particles in the
pairing model
shots (an int): the number of times to simulate running the
quantum circuit
Returns:
e_est (a float): the estimated ground state energy of the pairing model
(estimated using the VQE method)
e_ext (a float): the true ground state energy of the pairing model
(estimated using the VQE method.)
t_min (a Numpy array): the values for t which minimize the energy
Computes the minimum energy from the variational quantum eigensolver using an optimization
algorithm.
"""
# Define constants
= [g,shots]
constants
# get true (exact) ground state correlation energy
= pairing_4p4h(g, 1.0)
H = np.linalg.eig(H)
e_vals, e_vecs = np.real(min(e_vals))
e_ext = H[0][0]
e_ref
# minimize
= scipy.optimize.minimize(vqe_expect, x0=t_list, args=constants)
opt
# get t_min
= opt.x
t_min
# get estimated ground state correlation energy
= vqe_expect(t_min,constants)
e_est
# Return true and calculated
return e_est, e_ext, t_min
##############################
## AMP INIT ##
##############################
def amp_init(g):
"""
Inputs:
g (a float): the interaction strength of the particles
in the pairing model
Returns:
t (a Numpy array): the initial guesses for the t-matrix
in the pairing model ansatz
Initialize the values of t to be used for the first iteration
"""
# t (i,a) = (-g/4)/(i-a-(g/2))
= -(g/4)*np.array([1/(0-2-(g/2)), 1/(0-3-(g/2)), 1/(1-2-(g/2)), 1/(1-3-(g/2))])
t
# return t
return t
##############################
## TEST THE PROGRAM ##
##############################
# parameters
= 0
g_min = 1
g_max = 2**10
shots = 1.0
d
# initialize correlation energy lists
= []
e_ext_list = []
e_vqe_list
## g values to compute the exact energy at
= np.linspace(g_min,g_max,100)
g_ext_list
# loop through g_list
for g in g_ext_list:
# get ground state energy
= pairing_4p4h(g, d)
H = la.eig(H)
e_vec, e_val = np.real(min(e_vec))
e_min
e_ext_list.append(e_min)
## vqe g values, fewer because it takes longer
= np.linspace(g_min,g_max,5)
g_vqe_list
# loop through g_vqe_list
for g in g_vqe_list:
print('g =',g)
# get ground state correlation energy
= amp_init(g)
t_list
= vqe(t_list,g,shots)
e_vqe, e_ext, t_min print('e_vqe =',e_vqe)
print('e_exact =',e_ext)
e_vqe_list.append(e_vqe)
# plot
='e_exact', color="black")
plt.plot(g_ext_list,e_ext_list,label='e_vqe', color="red")
plt.scatter(g_vqe_list,e_vqe_list,label'Particle Interaction Strength (g)')
plt.xlabel('Ground State Energy')
plt.ylabel(
plt.legend() plt.show()
g = 0.0
e_vqe = 2.0
e_exact = 2.0
g = 0.25
e_vqe = 1.7518310546875
e_exact = 1.7304598458929061
g = 0.5
e_vqe = 1.4091796875
e_exact = 1.416774284351102
g = 0.75
e_vqe = 1.0888671875
e_exact = 1.0527262530578816
g = 1.0
e_vqe = 0.59375
e_exact = 0.6355484735755974