Introduction to Variational Quantum Eigensolver
Building Variational Quantum Eigensolver from scratch
Let us assume that we are given a
Today, we will be finding eigenvalues of such matrices using VQE-like circuits for such matrices. While doing so, we will cover the following topics:
- Hamiltonian in Pauli Basis
- Calculations of Expectation Value
- Designing Ansatze
- Running Simulations
- Running Noisy Simulations
- Extension to Excited Energy States
- Interpreting the Results
Necessary Imports
xxxxxxxxxx
import numpy as np
import scipy as sp
import itertools
import functools as ft
%matplotlib notebook
import matplotlib.pyplot as plt
from mpl_toolkits import mplot3d
import re
xxxxxxxxxx
#!pip install qiskit
from qiskit import *
from qiskit.providers.aer.noise import NoiseModel
from qiskit.providers.aer.noise import QuantumError, ReadoutError
from qiskit.providers.aer.noise import phase_amplitude_damping_error
from qiskit.providers.aer.noise import depolarizing_error
from qiskit.providers.aer.noise import thermal_relaxation_error
from qiskit.ignis.mitigation.measurement import complete_meas_cal, CompleteMeasFitter
Hamiltonian in Pauli Basis
The normalized Pauli matrices
Decomposing Hamiltonian into Pauli Terms
xxxxxxxxxx
def decompose_ham_to_pauli(H):
"""Decomposes a Hermitian matrix into a linear combination of Pauli operators.
Args:
H (array[complex]): a Hermitian matrix of dimension 2**n x 2**n.
Returns:
tuple[list[float], list[string], list [ndarray]]: a list of coefficients,
a list of corresponding string representation of tensor products of Pauli
observables that decompose the Hamiltonian, and
a list of their matrix representation as numpy arrays
"""
n = int(np.log2(len(H)))
N = 2 ** n
# Sanity Checks
if H.shape != (N, N):
raise ValueError(
"The Hamiltonian should have shape (2**n, 2**n), for any qubit number n>=1"
)
if not np.allclose(H, H.conj().T):
raise ValueError("The Hamiltonian is not Hermitian")
sI = np.eye(2, 2, dtype=complex)
sX = np.array([[0, 1], [1, 0]], dtype=complex)
sZ = np.array([[1, 0], [0,-1]], dtype=complex)
sY = complex(0,-1)*np.matmul(sZ,sX)
paulis = [sI, sX, sY, sZ]
paulis_label = ['I', 'X', 'Y', 'Z']
obs = []
coeffs = []
matrix = []
for term in itertools.product(paulis, repeat=n):
matrices = [pauli for pauli in term]
coeff = np.trace(ft.reduce(np.kron, matrices) @ H) / N
coeff = np.real_if_close(coeff).item()
# Hilbert-Schmidt-Product
if not np.allclose(coeff, 0):
coeffs.append(coeff)
obs.append(''.join([paulis_label[[i for i, x in enumerate(paulis)
if np.all(x == t)][0]]+str(idx) for idx, t in enumerate(reversed(term))]))
matrix.append(ft.reduce(np.kron, matrices))
return obs, coeffs , matrix
Composing Hamiltonian into Pauli Terms
xxxxxxxxxx
def compose_ham_from_pauli(terms, coeffs):
"""Composes a Hermitian matrix from a linear combination of Pauli operators.
Args:
tuple[list[float], list[string]]: a list of coefficients,
a list of corresponding string representation of tensor products of
Pauli observables that decompose the Hamiltonian.
Returns:
H (array[complex]): a Hermitian matrix of dimension 2**n x 2**n.
"""
pauli_qbs = [re.findall(r'[A-Za-z]|-?\d+\.\d+|\d+', x) for x in terms]
qubits = max([max(list(map(int, x[1:][::2]))) for x in pauli_qbs])
N = int(2**np.ceil(np.log2(qubits+1)))
sI = np.eye(2, 2, dtype=complex)
sX = np.array([[0, 1], [1, 0]], dtype=complex)
sZ = np.array([[1, 0], [0,-1]], dtype=complex)
sY = complex(0,-1)*np.matmul(sZ,sX)
paulis = [sI, sX, sY, sZ]
paulis_label = ['I', 'X', 'Y', 'Z']
paulis_term = {'I':sI, 'X':sX, 'Y':sY, 'Z':sZ}
hamil = np.zeros((2**N, 2**N), dtype=complex)
for coeff, pauli_qb in zip(coeffs, pauli_qbs):
term_str = ['I'] * N
for term, index in zip(pauli_qb[0:][::2], pauli_qb[1:][::2]):
term_str[int(index)] = term
matrices = [paulis_term[pauli] for pauli in term_str]
term_matrix = np.asarray(ft.reduce(np.kron, matrices[::-1]))
hamil += coeff*term_matrix
# Sanity Check
if not np.allclose(hamil, hamil.conj().T):
raise ValueError("The Hamiltonian formed is not Hermitian")
return hamil
Test Hamiltonian
xxxxxxxxxx
H0 = np.array([
[ 0.7056 , 0. , -0. , 0. ],
[ 0. , -1.1246 , 0.182 , 0. ],
[-0. , 0.182 , 0.4318 , -0. ],
[ 0. , 0. , -0. , 0.888 ]
])
H0
xxxxxxxxxx
array([[ 0.7056, 0. , -0. , 0. ],
[ 0. , -1.1246, 0.182 , 0. ],
[-0. , 0.182 , 0.4318, -0. ],
[ 0. , 0. , -0. , 0.888 ]])
xxxxxxxxxx
a, b , c = decompose_ham_to_pauli(H0)
a, b
xxxxxxxxxx
(['I0I1', 'Z0I1', 'X0X1', 'Y0Y1', 'I0Z1', 'Z0Z1'],
[0.2252, 0.3435, 0.091, 0.091, -0.43470000000000003, 0.5716])
xxxxxxxxxx
compose_ham_from_pauli(a, b)
xxxxxxxxxx
array([[ 0.7056+0.j, 0. +0.j, 0. +0.j, 0. +0.j],
[ 0. +0.j, -1.1246+0.j, 0.182 +0.j, 0. +0.j],
[ 0. +0.j, 0.182 +0.j, 0.4318+0.j, 0. +0.j],
[ 0. +0.j, 0. +0.j, 0. +0.j, 0.888 +0.j]])
xxxxxxxxxx
assert(np.isclose(compose_ham_from_pauli(a, b).real, H0).all())
Given Hamiltonian
xxxxxxxxxx
H1 = np.array([[1, 0, 0, 0],
[0, 0, -1, 0],
[0, -1, 0, 0],
[0, 0, 0, 1]])
H1
array([[ 1, 0, 0, 0],
[ 0, 0, -1, 0],
[ 0, -1, 0, 0],
[ 0, 0, 0, 1]])
xxxxxxxxxx
a, b, c = decompose_ham_to_pauli(H1)
a, b
xxxxxxxxxx
(['I0I1', 'X0X1', 'Y0Y1', 'Z0Z1'], [0.5, -0.5, -0.5, 0.5])
$ H_{1} = 0.5\times (I_{0}I_{1} - X_{0}X_{1} - Y_{0}Y_{1} + Z_{0}Z_{1})$
xxxxxxxxxx
compose_ham_from_pauli(a, b)
xxxxxxxxxx
array([[ 1.+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, 1.+0.j]])
xxxxxxxxxx
assert(np.isclose(compose_ham_from_pauli(a, b).real, H1).all())
Calculating the Expectation values
For an
By convention, performing a computational basis measurement is equivalent to measuring in measuring Pauli Z
, which gives us two eigenvectors
For some qubit
Change of Basis
Therefore, to measure a qubit, we can use any
For
This feature can be exploited to create circuits for both
For example: In order to do a
Similar to the one-qubit case, all multi-qubit Pauli-measurements can be written as a generalization of above.
For example: For two-qubit
Constructing Measurement Circuits
xxxxxxxxxx
def measure_circuit(basis, num_qubits):
"""
Generate measurement circuit according to the Pauli observable
string provided.
Args:
basis (str): String representation of tensor products of Pauli
observables
num_qubits (int): Number of qubits in the circuit
Returns:
measure_qc (QuantumCircuit): Measurement Circuit for the
corresponding basis.
"""
basis_qb = re.findall(r'[A-Za-z]|-?\d+\.\d+|\d+', basis)
basis = basis_qb[0:][::2]
qubit = list(map(int, basis_qb[1:][::2]))
measure_qc = QuantumCircuit(num_qubits, num_qubits)
for base, qb in zip(basis, qubit):
if base == 'I' or base == 'Z':
pass
elif base == 'X':
measure_qc.h(qb)
elif base == 'Y':
measure_qc.sdg(qb)
measure_qc.h(qb)
else:
raise ValueError("Wrong Basis provided")
for qb in range(num_qubits):
measure_qc.measure(qb, qb)
return measure_qc
xxxxxxxxxx
def calculate_expecation_val(circuit, basis, shots=2048, backend='qasm_simulator'):
"""
Calculate expectation value for the measurement of a circuit in a
given basis.
Args:
circuit (QuantumCircuit): Circuit using which expectation value
will be calculated for a given basis.
basis (str): String representation of tensor products of Pauli
observables.
shots (int): Number of times measurements needed to be done for calculating
probability.
backend (str): Backend for running the circuit.
Returns:
exp (float): Expectation value for the measurement of a circuit
in a given basis.
"""
exp_circuit = circuit + measure_circuit(basis, circuit.num_qubits)
result = execute(exp_circuit, backend=Aer.get_backend(backend),
shots=shots).result()
exp = 0.0
for key, counts in result.get_counts().items():
exp += (-1)**(int(key[0])+int(key[1])) * counts
return exp/shots
Examples
Measurements in XX, YY, ZZ basis
xxxxxxxxxx
measure_circuit('X0X1', 2).draw()
┌───┐┌─┐ q_0: ┤ H ├┤M├─── ├───┤└╥┘┌─┐ q_1: ┤ H ├─╫─┤M├ └───┘ ║ └╥┘ c: 2/══════╩══╩═ 0 1
xxxxxxxxxx
measure_circuit('Y0Y1', 2).draw()
┌─────┐┌───┐┌─┐ q_0: ┤ SDG ├┤ H ├┤M├─── ├─────┤├───┤└╥┘┌─┐ q_1: ┤ SDG ├┤ H ├─╫─┤M├ └─────┘└───┘ ║ └╥┘ c: 2/═════════════╩══╩═ 0 1
xxxxxxxxxx
measure_circuit('Z0Z1', 2).draw()
┌─┐ q_0: ┤M├─── └╥┘┌─┐ q_1: ─╫─┤M├ ║ └╥┘ c: 2/═╩══╩═ 0 1
Measurements in XY, YZ, ZX basis (To show flexibility of the function)
xxxxxxxxxx
measure_circuit('X0Y1', 2).draw()
┌───┐ ┌─┐ q_0: ─┤ H ├──────┤M├─── ┌┴───┴┐┌───┐└╥┘┌─┐ q_1: ┤ SDG ├┤ H ├─╫─┤M├ └─────┘└───┘ ║ └╥┘ c: 2/═════════════╩══╩═ 0 1
xxxxxxxxxx
measure_circuit('Y0Z1', 2).draw()
┌─────┐┌───┐┌─┐ q_0: ┤ SDG ├┤ H ├┤M├ └─┬─┬─┘└───┘└╥┘ q_1: ──┤M├────────╫─ └╥┘ ║ c: 2/═══╩═════════╩═ 1 0
xxxxxxxxxx
measure_circuit('Z0X1', 2).draw()
┌─┐ q_0: ─────┤M├─── ┌───┐└╥┘┌─┐ q_1: ┤ H ├─╫─┤M├ └───┘ ║ └╥┘ c: 2/══════╩══╩═ 0 1
Determining the Ansatz
Designing Ansatze
Ansatze are simply a parameterized quantum circuits (PQC), which play an essential role in the performance of many variational hybrid quantum-classical (HQC) algorithms. Major challenge while designing an asatz is to choose an effective template circuit that well represents the solution space while maintaining a low circuit depth and number of parameters. Here, we make a choice of two ansatze, one randomly and another inspired from the given hint.
Ansatz 1 (Random Choice)
xxxxxxxxxx
def ansatz1(params, num_qubits):
"""
Generate an templated ansatz with given parameters
Args:
params (array[float]): Parameters to initialize the parameterized unitary.
num_qubits (int): Number of qubits in the circuit.
Returns:
ansatz (QuantumCircuit): Generated ansatz circuit
"""
ansatz = QuantumCircuit(num_qubits, num_qubits)
params = params.reshape(2,2)
for idx in range(num_qubits):
ansatz.rx(params[0][idx], idx)
ansatz.cnot(0, 1)
for idx in range(num_qubits):
ansatz.rz(params[1][idx], idx)
return ansatz
ansatz1(np.random.uniform(-np.pi, np.pi, (2,2)), 2).draw()
┌─────────────┐ ┌──────────────┐ q_0: ┤ RX(0.74906) ├──■──┤ RZ(-0.10279) ├ ├─────────────┤┌─┴─┐├─────────────┬┘ q_1: ┤ RX(0.40493) ├┤ X ├┤ RZ(-2.3803) ├─ └─────────────┘└───┘└─────────────┘ c: 2/════════════════════════════════════
Ansatz 2 (From Hint)
xxxxxxxxxx
def ansatz2(params, num_qubits):
"""
Generate an templated ansatz with given parameters
Args:
params (array[float]): Parameters to initialize the parameterized unitary.
num_qubits (int): Number of qubits in the circuit.
Returns:
ansatz (QuantumCircuit): Generated ansatz circuit
"""
params = np.array(params).reshape(1)
ansatz = QuantumCircuit(num_qubits, num_qubits)
ansatz.h(0)
ansatz.cx(0, 1)
ansatz.rx(params[0], 0)
return ansatz
ansatz2([np.pi], 2).draw()
┌───┐ ┌────────┐ q_0: ┤ H ├──■──┤ RX(pi) ├ └───┘┌─┴─┐└────────┘ q_1: ─────┤ X ├────────── └───┘ c: 2/════════════════════
Running Simulations
Variational Quantum Eigensolver (VQE) is based on Rayleigh-Ritz variational principle. To perform VQE, the first step is to enocde the problem into a Hermitian matrix
Here, we optimize these parameters scipy.optimize
library based on a cost function
xxxxxxxxxx
def vqe(params, meas_basis, coeffs, circuit, num_qubits, shots=2048):
"""
Return the calculated energy scalar for a given asnatz and
decomposed Hamiltoian.
Args:
params (matrix(np.array)): Parameters for initializing the ansatz.
meas_basis (list[str]): String representation of measurement basis, i.e.
the decomposed pauli term with their corresponding qubits.
coeffs (vector(np.array)): Coefficients for the decomposed Pauli Term
circuit (QuantumCircuit): Template for Ansatz circuit
num_qubits (int): Number of qubits in the given asatze.
shots (int): Number of shots to get the probability distribution.
Return:
energy (float): Expectation value of the Hamiltonian whose
decomposition was provided.
"""
N = num_qubits
circuit = circuit(params, num_qubits)
energy = 0
for basis, coeff in zip(meas_basis, coeffs):
if basis.count('I') != N:
energy += coeff*calculate_expecation_val(circuit, basis, shots)
energy += 0.5
return energy
Ansatz 1 (Random Choice)
xxxxxxxxxx
a, b, c = decompose_ham_to_pauli(H1)
params = np.random.uniform(-np.pi, np.pi, (2,2))
func = ft.partial(vqe, meas_basis=a, coeffs=b, circuit=ansatz1,
num_qubits=2, shots=2048)
xxxxxxxxxx
res = sp.optimize.minimize(func, params, method='Powell')
res
xxxxxxxxxx
direc: array([[1., 0., 0., 0.],
[0., 1., 0., 0.],
[0., 0., 1., 0.],
[0., 0., 0., 1.]])
fun: array(-1.)
message: 'Optimization terminated successfully.'
nfev: 190
nit: 3
status: 0
success: True
x: array([ 4.7130171 , -3.15044759, -1.59558296, -0.02529809])
xxxxxxxxxx
assert(float(res.fun) == min(np.linalg.eig(H1)[0])) #Sanity Check
Visualizing the Result
After looking at multiple results' res.x
, we realize that first and second parameters always have the values
xxxxxxxxxx
def energy_expectation(x, y):
""" Returns meshgrid values for plotting """
energy = np.zeros(x.shape)
for idx, thetas in enumerate(x):
for ind, theta1 in enumerate(thetas):
params = np.array([np.pi/2, np.pi, theta1, y[idx][ind]])
energy[idx][ind] = func(params)
return energy
theta1 = np.linspace(0.0, 2*np.pi, 200)
theta2 = np.linspace(0.0, 2*np.pi, 200)
X, Y = np.meshgrid(theta1, theta2)
Z = energy_expectation(X, Y)
fig = plt.figure()
ax = plt.axes(projection='3d')
ax.contour3D(X, Y, Z, 50, cmap='summer')
ax.set_xlabel('theta_1')
ax.set_ylabel('theta_2')
ax.set_zlabel('Expectation of H_1');
plt.show()
Ansatz 2 (From Hint)
xxxxxxxxxx
a, b, c = decompose_ham_to_pauli(H1)
params = np.random.uniform(-np.pi, np.pi, 1)
func = ft.partial(vqe, meas_basis=a, coeffs=b, circuit=ansatz2,
num_qubits=2, shots=2048)
xxxxxxxxxx
res = sp.optimize.minimize(func, params, method='Powell')
res
xxxxxxxxxx
direc: array([[1.]])
fun: array(-1.)
message: 'Optimization terminated successfully.'
nfev: 26
nit: 2
status: 0
success: True
x: array([3.13590442])
xxxxxxxxxx
assert(float(res.fun) == min(np.linalg.eig(H1)[0])) #Sanity Check
Visualizing the Result
We scan over the paramter
xxxxxxxxxx
thetas = np.linspace(0.0, 2*np.pi, 500)
energy = np.zeros(len(thetas))
for idx, theta in enumerate(thetas):
energy[idx] = func([theta])
fig = plt.figure()
plt.plot(thetas, energy)
plt.title('Expectation value w.r.t angle theta')
plt.ylabel('Expectation value of $H_1$')
plt.xlabel('Theta (radians)')
indices = [idx for idx, x in enumerate(energy) if x <= -1.0]
xmin = thetas[indices[len(indices)//2]]
ymin = energy.min()
text= "Theta={:.3f}, \n Energy={:.3f}".format(xmin, ymin)
plt.plot(xmin,ymin,'x')
plt.annotate(text, xy=(xmin+0.75, ymin))
fig.show()
Number of Measurements (or Shots)
In order to obtain results from a quantum computer, one needs to perform sampling. This allows us to obtain the probability distribution, and hence know the most probable results from the measurement. Here, we see the change of expectation value of
xxxxxxxxxx
thetas = np.linspace(0.0, 2*np.pi, 500)
shots = [128, 256, 512, 1024, 2048]
shot_energy = np.zeros((len(shots), len(thetas)))
for ind, shot in enumerate(shots):
for idx, theta in enumerate(thetas):
func = ft.partial(vqe, meas_basis=a, coeffs=b, circuit=ansatz2,
num_qubits=2, shots=shot)
shot_energy[ind][idx] = func([theta])
xxxxxxxxxx
plt.figure()
for ind, shot in enumerate(shots):
plt.plot(thetas, shot_energy[ind], label='Shots=' + str(shot))
plt.title('Expectation value w.r.t angle theta and shots')
plt.ylabel('Expectation value of $H_1$')
plt.xlabel('Theta (radians)')
plt.legend()
plt.show()
Performance of Classical Optimizers
Successful application of hybrid-quantum classical algorithms, with the classical step involving an optimizer, on current hardware, requires the classical optimizer to be noise-aware. In order to study this, we study the change of expectation value of
xxxxxxxxxx
a, b, c = decompose_ham_to_pauli(H1)
params = np.random.uniform(-np.pi, np.pi, 1)
func = ft.partial(vqe, meas_basis=a, coeffs=b, circuit=ansatz2,
num_qubits=2, shots=2048)
xxxxxxxxxx
optimizers = ['Nelder-Mead', 'L-BFGS-B', 'TNC', 'Powell', 'COBYLA', 'SLSQP']
opt_val = []
opt_param = []
opt_feval = []
opt_iter = []
init_params = params
for opt in optimizers:
params = init_params
res = sp.optimize.minimize(func, params, method=opt)
opt_val.append(float(res.fun))
opt_param.append(float(res.x))
opt_feval.append(int(res.nfev))
try:
opt_iter.append(int(res.nit))
except:
opt_iter.append(1)
xxxxxxxxxx
fig = plt.figure()
plt.bar(optimizers, opt_val, color=['black', 'red', 'green', 'blue', 'yellow', 'orange'])
plt.title('Minimum expectation value w.r.t different optimizers')
plt.ylabel('Expectation value of $H_1$')
plt.xlabel('Optimizer')
plt.xticks(rotation=5)
fig.show()
xxxxxxxxxx
fig = plt.figure()
plt.bar(optimizers[:-1], opt_param[:-1], color=['black', 'red', 'green', 'blue', 'yellow'])
plt.title('Optimal parameter w.r.t different optimizers')
plt.ylabel('Value of theta')
plt.xlabel('Optimizer')
plt.xticks(rotation=5)
fig.show()
xxxxxxxxxx
fig = plt.figure()
plt.bar(optimizers[:-1], opt_feval[:-1], color=['black', 'red', 'green', 'blue', 'yellow'])
plt.title('Function evaluations w.r.t different optimizers')
plt.ylabel('Number of function evaluations$')
plt.xlabel('Optimizer')
plt.xticks(rotation=5)
fig.show()
xxxxxxxxxx
fig = plt.figure()
plt.bar(optimizers[:-1], opt_iter[:-1], color=['black', 'red', 'green', 'blue', 'yellow'])
plt.title('Number of iterations w.r.t different optimizers')
plt.ylabel('Number of iterations')
plt.xlabel('Optimizer')
plt.xticks(rotation=5)
fig.show()
Noisy Simulation
Computational power of NISQ processors suffers a lot from the limited capabilities of their physical qubits. This is essentially due to present of decoherence, limited connectivity, and absence of error-correction. Hence, it is essential to see how our ansatz would perform on a real device. Here, we replicate the noise model from a real device, and then test the perfrormance of our VQE module.
xxxxxxxxxx
# Noise Model Preparation
provider = IBMQ.load_account()
noisy_backend = provider.get_backend('ibmq_vigo')
coupling_map = noisy_backend.configuration().coupling_map
noise_model = providers.aer.noise.NoiseModel.from_backend(noisy_backend)
basis_gates = noise_model.basis_gates
Qubit coupling map of this real device is the following:
xxxxxxxxxx
qiskit.transpiler.CouplingMap(noisy_backend.configuration().coupling_map).draw()
xxxxxxxxxx
noise_model
xxxxxxxxxx
NoiseModel:
Basis gates: ['cx', 'id', 'u2', 'u3']
Instructions with noise: ['id', 'measure', 'cx', 'u2', 'u3']
Qubits with noise: [0, 1, 2, 3, 4]
Specific qubit errors: [('id', [0]), ('id', [1]), ('id', [2]), ('id', [3]), ('id', [4]), ('u2', [0]), ('u2', [1]), ('u2', [2]), ('u2', [3]), ('u2', [4]), ('u3', [0]), ('u3', [1]), ('u3', [2]), ('u3', [3]), ('u3', [4]), ('cx', [0, 1]), ('cx', [1, 0]), ('cx', [1, 2]), ('cx', [1, 3]), ('cx', [2, 1]), ('cx', [3, 1]), ('cx', [3, 4]), ('cx', [4, 3]), ('measure', [0]), ('measure', [1]), ('measure', [2]), ('measure', [3]), ('measure', [4])]
xxxxxxxxxx
def calculate_noisy_expecation_val(circuit, basis, mitigated=False, shots=2048, backend='qasm_simulator'):
"""
Calculate expectation value for the measurement of a circuit in a
given basis in presence of noise.
Args:
circuit (QuantumCircuit): Circuit using which expectation value
will be calculated for a given basis.
basis (str): String representation of tensor products of Pauli
observables.
mitigated (bool): Whether mitigation has to be performed or not.
shots (int): Number of times measurements needed to be done for calculating
probability.
backend (str): Backend for running the circuit.
Returns:
exp (float): Expectation value for the measurement of a circuit
in a given basis.
"""
exp_circuit = circuit + measure_circuit(basis, circuit.num_qubits)
result = execute(exp_circuit, backend=Aer.get_backend(backend),
shots=shots, coupling_map=coupling_map,
basis_gates=basis_gates,
noise_model=noise_model).result()
if mitigated:
meas_calibs, state_labels = complete_meas_cal(qr=exp_circuit.qregs[0], cr=exp_circuit.cregs[0])
cal_results = qiskit.execute(meas_calibs, backend=Aer.get_backend(backend),
coupling_map=coupling_map, basis_gates=basis_gates,
shots=shots, noise_model=noise_model).result()
meas_fitter = CompleteMeasFitter(cal_results, state_labels)
meas_filter = meas_fitter.filter
result = meas_filter.apply(result)
exp = 0.0
for key, counts in result.get_counts().items():
exp += (-1)**(int(key[0])+int(key[1])) * counts
return exp/shots
def noisy_vqe(params, meas_basis, coeffs, circuit, num_qubits, mitigated=False, shots=2048):
"""
Return the calculated energy scalar for a given asnatz and
decomposed Hamiltoian in presence of noise.
Args:
params (matrix(np.array)): Parameters for initializing the ansatz.
meas_basis (list[str]): String representation of measurement basis, i.e.
the decomposed pauli term with their corresponding qubits.
coeffs (vector(np.array)): Coefficients for the decomposed Pauli Term
circuit (QuantumCircuit): Template for Ansatz circuit
num_qubits (int): Number of qubits in the given asatze.
mitigated (bool): Whether mitigation has to be performed or not.
shots (int): Number of shots to get the probability distribution.
Return:
energy (float): Expectation value of the Hamiltonian whose
decomposition was provided.
"""
N = num_qubits
circuit = circuit(params, num_qubits)
energy = 0
for basis, coeff in zip(meas_basis, coeffs):
if basis.count('I') != N:
energy += coeff*calculate_noisy_expecation_val(circuit, basis, mitigated, shots)
energy += 0.5
return energy
Ansatz 1 (Random Choice)
xxxxxxxxxx
a, b, c = decompose_ham_to_pauli(H1)
params = np.random.uniform(-np.pi, np.pi, (2,2))
func = ft.partial(noisy_vqe, meas_basis=a, coeffs=b, circuit=ansatz1,
num_qubits=2, mitigated=False, shots=8192)
xxxxxxxxxx
res = sp.optimize.minimize(func, params, method='Powell')
res
xxxxxxxxxx
direc: array([[1., 0., 0., 0.],
[0., 1., 0., 0.],
[0., 0., 1., 0.],
[0., 0., 0., 1.]])
fun: array(-0.87451172)
message: 'Optimization terminated successfully.'
nfev: 266
nit: 3
status: 0
success: True
x: array([1.64212384, 3.14620703, 2.15383843, 0.53103981])
xxxxxxxxxx
a, b, c = decompose_ham_to_pauli(H1)
params = np.random.uniform(-np.pi, np.pi, (2,2))
func = ft.partial(noisy_vqe, meas_basis=a, coeffs=b, circuit=ansatz1,
num_qubits=2, mitigated=True, shots=8192)
xxxxxxxxxx
res = sp.optimize.minimize(func, params, method='Powell')
res
xxxxxxxxxx
direc: array([[1., 0., 0., 0.],
[0., 1., 0., 0.],
[0., 0., 1., 0.],
[0., 0., 0., 1.]])
fun: -0.9619728377818637
message: 'Optimization terminated successfully.'
nfev: 257
nit: 3
status: 0
success: True
x: array([ 1.58409174, -3.11397559, -0.50929574, -1.96419365])
Ansatz 2 (From Hint)
xxxxxxxxxx
a, b, c = decompose_ham_to_pauli(H1)
params = np.random.uniform(-np.pi, np.pi, 1)
func = ft.partial(noisy_vqe, meas_basis=a, coeffs=b, circuit=ansatz2,
num_qubits=2, mitigated=False, shots=8192)
xxxxxxxxxx
res = sp.optimize.minimize(func, params, method='Powell')
res
xxxxxxxxxx
direc: array([[-0.1127984]])
fun: array(-0.88122559)
message: 'Optimization terminated successfully.'
nfev: 88
nit: 5
status: 0
success: True
x: array([3.17376448])
xxxxxxxxxx
a, b, c = decompose_ham_to_pauli(H1)
params = np.random.uniform(-np.pi, np.pi, 1)
func = ft.partial(noisy_vqe, meas_basis=a, coeffs=b, circuit=ansatz2,
num_qubits=2, mitigated=True, shots=8192)
xxxxxxxxxx
res = sp.optimize.minimize(func, params, method='Powell')
res
xxxxxxxxxx
direc: array([[3.01441921e-09]])
fun: -0.9702586386528804
message: 'Optimization terminated successfully.'
nfev: 120
nit: 5
status: 0
success: True
x: array([-3.12601157])
Visualizing the Result (Noisy v/s Mitigated v/s Ideal)
xxxxxxxxxx
thetas = np.linspace(0.0, 2*np.pi, 500)
energy1 = np.zeros(len(thetas))
energy2 = np.zeros(len(thetas))
func1 = ft.partial(noisy_vqe, meas_basis=a, coeffs=b, circuit=ansatz2,
num_qubits=2, mitigated=False, shots=8192)
func2 = ft.partial(noisy_vqe, meas_basis=a, coeffs=b, circuit=ansatz2,
num_qubits=2, mitigated=True, shots=8192)
for idx, theta in enumerate(thetas):
energy1[idx] = func1([theta])
energy2[idx] = func2([theta])
xxxxxxxxxx
plt.plot(thetas, energy, label='simulation')
plt.plot(thetas, energy1, label='noisy')
plt.plot(thetas, energy2, label='mitigated')
plt.title('Expectation value w.r.t angle theta')
plt.ylabel('Expectation value of $H_1$')
plt.xlabel('Theta (radians)')
plt.legend()
plt.show()
xxxxxxxxxx
<IPython.core.display.Javascript object>
Number of Measurements (or Shots)
Let's see the change of expectation value of
xxxxxxxxxx
thetas = np.linspace(0.0, 2*np.pi, 500)
shots = [128, 256, 512, 1024, 2048]
noisy_shot_energy = np.zeros((len(shots), len(thetas)))
for ind, shot in enumerate(shots):
for idx, theta in enumerate(thetas):
func = ft.partial(noisy_vqe, meas_basis=a, coeffs=b, circuit=ansatz2,
num_qubits=2, shots=shot)
noisy_shot_energy[ind][idx] = func([theta])
xxxxxxxxxx
plt.figure()
for ind, shot in enumerate(shots):
plt.plot(thetas, noisy_shot_energy[ind], label='Shots=' + str(shot))
plt.title('Expectation value w.r.t angle theta and shots')
plt.ylabel('Expectation value of $H_1$')
plt.xlabel('Theta (radians)')
plt.legend()
plt.show()
Peformance of Classical Optimizers
Let's see the change of expectation value of
xxxxxxxxxx
a, b, c = decompose_ham_to_pauli(H1)
params = np.random.uniform(-np.pi, np.pi, 1)
func = ft.partial(noisy_vqe, meas_basis=a, coeffs=b, circuit=ansatz2,
num_qubits=2, mitigated=True, shots=8192)
xxxxxxxxxx
optimizers = ['Nelder-Mead', 'L-BFGS-B', 'TNC', 'Powell', 'COBYLA', 'SLSQP']
opt_val = []
opt_param = []
opt_feval = []
opt_iter = []
init_params = params
for opt in optimizers:
params = init_params
res = sp.optimize.minimize(func, params, method=opt)
opt_val.append(float(res.fun))
opt_param.append(float(res.x))
opt_feval.append(int(res.nfev))
try:
opt_iter.append(int(res.nit))
except:
opt_iter.append(1)
xxxxxxxxxx
fig = plt.figure()
plt.bar(optimizers, opt_val, color=['black', 'red', 'green', 'blue', 'yellow', 'orange'])
plt.title('Minimum expectation value w.r.t different optimizers')
plt.ylabel('Expectation value of $H_1$')
plt.xlabel('Optimizer')
plt.xticks(rotation=5)
fig.show()
xxxxxxxxxx
fig = plt.figure()
plt.bar(optimizers[:-1], opt_param[:-1], color=['black', 'red', 'green', 'blue', 'yellow'])
plt.title('Optimal parameter w.r.t different optimizers')
plt.ylabel('Value of theta')
plt.xlabel('Optimizer')
plt.xticks(rotation=5)
fig.show()
xxxxxxxxxx
fig = plt.figure()
plt.bar(optimizers[:-1], opt_feval[:-1], color=['black', 'red', 'green', 'blue', 'yellow'])
plt.title('Function evaluations w.r.t different optimizers')
plt.ylabel('Number of function evaluations')
plt.xlabel('Optimizer')
plt.xticks(rotation=5)
fig.show()
xxxxxxxxxx
fig = plt.figure()
plt.bar(optimizers[:-1], opt_iter[:-1], color=['black', 'red', 'green', 'blue', 'yellow'])
plt.title('Number of iterations w.r.t different optimizers')
plt.ylabel('Number of iterations')
plt.xlabel('Optimizer')
plt.xticks(rotation=5)
fig.show()
Comparing the performance of VQE in presence of errors due to Readout, Thermal Relaxation, Amplitude-Phase damping and Depolarization
Readout
Describes classical readout errors
xxxxxxxxxx
# Measurement miss-assignement probabilities
p0given1 = 0.1
p1given0 = 0.05
noise_readout = ReadoutError([[1 - p1given0, p1given0], [p0given1, 1 - p0given1]])
noise_readout
xxxxxxxxxx
ReadoutError([[0.95 0.05]
[0.1 0.9 ]])
Thermal Relaxation
Single qubit thermal relaxation error is characterized by relaxation time constants
xxxxxxxxxx
num_qubits = 2
T1s = np.random.normal(50e3, 10e3, num_qubits) # Sampled from normal distribution
T2s = np.random.normal(80e3, 20e3, num_qubits) # Sampled from normal distribution
T2s = np.array([min(T2s[j], 2 * T1s[j]) for j in range(num_qubits)]) # Ensuring T2s < 2*T1s
# Instruction times (in nanoseconds)
time_u1 = 0 # virtual gate
time_u2 = 50 # (single X90 pulse)
time_u3 = 100 # (two X90 pulses)
time_cx = 300
time_measure = 1000 # 1 microsecond
# Add depolarizing error to all single qubit u1, u2, u3, cx, measure gates
errors_thermal_u1 = [thermal_relaxation_error(t1, t2, time_u1)
for t1, t2 in zip(T1s, T2s)]
errors_thermal_u2 = [thermal_relaxation_error(t1, t2, time_u2)
for t1, t2 in zip(T1s, T2s)]
errors_thermal_u3 = [thermal_relaxation_error(t1, t2, time_u3)
for t1, t2 in zip(T1s, T2s)]
errors_thermal_cx = [[thermal_relaxation_error(t1a, t2a, time_cx).expand(
thermal_relaxation_error(t1b, t2b, time_cx))
for t1a, t2a in zip(T1s, T2s)]
for t1b, t2b in zip(T1s, T2s)]
errors_thermal_measure = [thermal_relaxation_error(t1, t2, time_measure)
for t1, t2 in zip(T1s, T2s)]
# Add errors to noise model
noise_thermal = NoiseModel()
for j in range(num_qubits):
noise_thermal.add_quantum_error(errors_thermal_u1[j], "u1", [j])
noise_thermal.add_quantum_error(errors_thermal_u2[j], "u2", [j])
noise_thermal.add_quantum_error(errors_thermal_u3[j], "u3", [j])
for k in range(num_qubits):
noise_thermal.add_quantum_error(errors_thermal_cx[j][k], "cx", [j, k])
noise_thermal.add_quantum_error(errors_thermal_measure[j], "measure", [j])
noise_thermal
xxxxxxxxxx
NoiseModel:
Basis gates: ['cx', 'id', 'u2', 'u3']
Instructions with noise: ['u2', 'u3', 'cx', 'measure']
Qubits with noise: [0, 1]
Specific qubit errors: [('u2', [0]), ('u2', [1]), ('u3', [0]), ('u3', [1]), ('cx', [0, 0]), ('cx', [0, 1]), ('cx', [1, 0]), ('cx', [1, 1]), ('measure', [0]), ('measure', [1])]
Amplitude-Phase Damping
Single-qubit generalized combined phase and amplitude damping error is given by an amplitude damping parameter
xxxxxxxxxx
lamb = 0.05
gamma = 0.1
errors_phase_amplitude_1 = phase_amplitude_damping_error(lamb, gamma)
errors_phase_amplitude_2 = phase_amplitude_damping_error(lamb, gamma).expand(phase_amplitude_damping_error(lamb, gamma))
# Add errors to noise model
noise_phase_amplitude = NoiseModel()
noise_phase_amplitude.add_all_qubit_quantum_error(errors_phase_amplitude_1, ['u1', 'u2', 'u3', 'measure'])
noise_phase_amplitude.add_all_qubit_quantum_error(errors_phase_amplitude_2, ['cx'])
noise_phase_amplitude
xxxxxxxxxx
NoiseModel:
Basis gates: ['cx', 'id', 'u1', 'u2', 'u3']
Instructions with noise: ['u2', 'u3', 'u1', 'cx', 'measure']
All-qubits errors: ['u1', 'u2', 'u3', 'measure', 'cx']
Depolarization
N-qubit depolarizing error is given by a depolarization probability
xxxxxxxxxx
# Add depolarizing error to all single qubit u1, u2, u3, cx, measure gates
error_depol_1 = depolarizing_error(0.05, 1)
error_depol_2 = depolarizing_error(0.1, 2)
error_depol_3 = depolarizing_error(0.1, 1)
# Add errors to noise model
noise_depol = NoiseModel()
noise_depol.add_all_qubit_quantum_error(error_depol_1, ['u1', 'u2', 'u3'])
noise_depol.add_all_qubit_quantum_error(error_depol_2, ['cx'])
noise_depol.add_all_qubit_quantum_error(error_depol_3, ['measure'])
noise_depol
xxxxxxxxxx
NoiseModel:
Basis gates: ['cx', 'id', 'u1', 'u2', 'u3']
Instructions with noise: ['u2', 'u3', 'u1', 'cx', 'measure']
All-qubits errors: ['u1', 'u2', 'u3', 'cx', 'measure']
xxxxxxxxxx
def calculate_restricted_noisy_expecation_val(circuit, basis, noise_model, shots=2048, backend='qasm_simulator'):
"""
Calculate expectation value for the measurement of a circuit in a
given basis in presence of a particular kind of noise.
Args:
circuit (QuantumCircuit): Circuit using which expectation value
will be calculated for a given basis.
basis (str): String representation of tensor products of Pauli
observables.
noise_model (NoiseModel): Noise Model for execution
shots (int): Number of times measurements needed to be done for calculating
probability.
backend (str): Backend for running the circuit.
Returns:
exp (float): Expectation value for the measurement of a circuit
in a given basis.
"""
exp_circuit = circuit + measure_circuit(basis, circuit.num_qubits)
result = execute(exp_circuit, backend=Aer.get_backend(backend),
shots=shots, noise_model=noise_model).result()
exp = 0.0
for key, counts in result.get_counts().items():
exp += (-1)**(int(key[0])+int(key[1])) * counts
return exp/shots
def restricted_noisy_vqe(params, meas_basis, coeffs, circuit, num_qubits, noise_model, shots=2048):
"""
Return the calculated energy scalar for a given asnatz and
decomposed Hamiltoian in presence of a particular kind of noise.
Args:
params (matrix(np.array)): Parameters for initializing the ansatz.
meas_basis (list[str]): String representation of measurement basis, i.e.
the decomposed pauli term with their corresponding qubits.
coeffs (vector(np.array)): Coefficients for the decomposed Pauli Term
circuit (QuantumCircuit): Template for Ansatz circuit
num_qubits (int): Number of qubits in the given asatze.
noise_model (NoiseModel): Noise Model for execution
shots (int): Number of shots to get the probability distribution.
Return:
energy (float): Expectation value of the Hamiltonian whose
decomposition was provided.
"""
N = num_qubits
circuit = circuit(params, num_qubits)
energy = 0
for basis, coeff in zip(meas_basis, coeffs):
if basis.count('I') != N:
energy += coeff*calculate_restricted_noisy_expecation_val(circuit, basis, noise_model, shots)
energy += 0.5
return energy
xxxxxxxxxx
noise_models = [noise_readout, noise_thermal, noise_phase_amplitude, noise_depol]
thetas = np.linspace(0.0, 2*np.pi, 500)
noisy_energy = np.zeros((len(noise_models), len(thetas)))
for ind, model in enumerate(noise_models):
func3 = ft.partial(restricted_noisy_vqe, meas_basis=a, coeffs=b, circuit=ansatz2,
num_qubits=2, noise_model=model, shots=4096)
for idx, theta in enumerate(thetas):
noisy_energy[ind][idx] = func3([theta])
xxxxxxxxxx
noise_models_name = ['Readout Error (0.1, 0.05)', 'Thermal Relaxation (5e-5, 8e-5)',
'Amplitude-Phase Damping (0.05, 0.1)', 'Depolarization (0.05)']
plt.figure()
for ind, model in enumerate(noise_models):
plt.plot(thetas, noisy_energy[ind], label=noise_models_name[ind])
plt.title('Expectation value w.r.t angle theta and shots')
plt.ylabel('Expectation value of $H_1$')
plt.xlabel('Theta (radians)')
plt.legend()
plt.show()
We see from the above plot that the presence of noise due to depolarization has affected our VQE results the most. This can be explained via its definiton that it is the uniform contraction of the Bloch sphere, parameterized by
xxxxxxxxxx
# Add depolarizing error to all single qubit u1, u2, u3, cx, measure gates
error_depol_1 = depolarizing_error(0.05, 1)
error_depol_2 = depolarizing_error(0.1, 2)
error_depol_3 = depolarizing_error(0.1, 1)
# Add errors to noise model
noise_depol_single = NoiseModel()
noise_depol_two = NoiseModel()
noise_depol_measure = NoiseModel()
noise_depol_single.add_all_qubit_quantum_error(error_depol_1, ['u1', 'u2', 'u3'])
noise_depol_two.add_all_qubit_quantum_error(error_depol_2, ['cx'])
noise_depol_measure.add_all_qubit_quantum_error(error_depol_3, ['measure'])
noise_depol_single, noise_depol_two, noise_depol_measure
xxxxxxxxxx
(NoiseModel:
Basis gates: ['cx', 'id', 'u1', 'u2', 'u3']
Instructions with noise: ['u2', 'u3', 'u1']
All-qubits errors: ['u1', 'u2', 'u3'],
NoiseModel:
Basis gates: ['cx', 'id', 'u3']
Instructions with noise: ['cx']
All-qubits errors: ['cx'],
NoiseModel:
Basis gates: ['cx', 'id', 'u3']
Instructions with noise: ['measure']
All-qubits errors: ['measure'])
xxxxxxxxxx
depol_noise_models = [noise_depol_single, noise_depol_two, noise_depol_measue]
thetas = np.linspace(0.0, 2*np.pi, 500)
depol_noisy_energy = np.zeros((len(depol_noise_models), len(thetas)))
for ind, model in enumerate(depol_noise_models):
func3 = ft.partial(restricted_noisy_vqe, meas_basis=a, coeffs=b, circuit=ansatz2,
num_qubits=2, noise_model=model, shots=4096)
for idx, theta in enumerate(thetas):
depol_noisy_energy[ind][idx] = func3([theta])
xxxxxxxxxx
depol_noise_models_name = ['One-Qubit Gates Depolarization', ' Two-Qubit Gates Depolarization',
'Measure Gate Depolarization']
plt.figure()
for ind, model in enumerate(depol_noise_models):
plt.plot(thetas, depol_noisy_energy[ind], label=depol_noise_models_name[ind])
plt.title('Expectation value w.r.t angle theta and shots')
plt.ylabel('Expectation value of $H_1$')
plt.xlabel('Theta (radians)')
plt.legend()
plt.show()
Excited States of the Hamiltonian
One of the way to find the
This can be used to update our cost function,
We'd already calculated the first term in VQE, but to calculate the next summation term, we refer to [2]. It suggests us to rewrite the overlap term
This technique is known as Variational Quantum Deflation and requires the same number of qubits as Variational Quantum Eigensolver and around twice the circuit depth.
xxxxxxxxxx
def vqd_cost(thetak, thetai, func, circuit, num_qubits, beta, shots=2048):
""" Returns cost for VQD """
energy = func(thetak)
for idx, theta in enumerate(thetai):
ansatz = circuit(theta, num_qubits) + circuit(thetak, num_qubits).inverse()
ansatz.measure_all()
backend = Aer.get_backend('qasm_simulator')
result = execute(ansatz, backend, shots=shots).result()
energy += beta[idx]*result.get_counts().get('00 00', 0)/shots
return energy
def vqd(hamiltonian, circuit, num_qubits, params_shape, beta):
"""
Runs Variational Quantum Deflation algorithm on a given Hamiltonian
to give its excited eigenvalues.
Args:
hamiltonian (matrix(ndarray)): Hamiltonian for
circuit (Quantum Circuit): ansatz template
num_qubits (int): number of qubits to make an ansatz
params_shape (tuple): shape tuple for parameters in ansatz
beta (int): beta coefficent for VQD
Returns:
energy (vector(ndarray)): Calculated eigen energies for the Hamiltonian.
"""
a, b, c = decompose_ham_to_pauli(hamiltonian)
initial_params = np.random.uniform(-np.pi, np.pi, params_shape)
func = ft.partial(vqe, meas_basis=a, coeffs=b, circuit=circuit,
num_qubits=num_qubits, shots=2048)
res1 = sp.optimize.minimize(func, initial_params, method='Powell')
energies = [float(res1.fun)]
thetas = [np.reshape(res1.x, params_shape)]
if len(beta) != np.shape(hamiltonian)[0]-1:
raise ValueError('Length mismatch! Provide sufficient amount of beta_{i}')
for eigst in range(np.shape(hamiltonian)[0]-1):
cost = ft.partial(vqd_cost, thetai=thetas, func=func, circuit=circuit,
num_qubits=num_qubits, beta=beta, shots=2048)
res2 = sp.optimize.minimize(cost, initial_params, method='Powell')
if res2.success:
thetas.append(np.reshape(res2.x, params_shape))
energies.append(float(res2.fun))
else:
raise ValueError('Minimization was not successful.')
return energies
Ansatz 1
xxxxxxxxxx
excited_energies = vqd(H1, ansatz1, 2, (2,2), [3.5, 1e-1, 1e-3])
excited_energies
xxxxxxxxxx
[-1.0, 0.9921875, 0.994189453125, 1.0003808593749999]
xxxxxxxxxx
assert(np.allclose(np.asarray(excited_energies), np.sort(np.linalg.eig(H1)[0]), atol=1e-2))
Ansatz 2
xxxxxxxxxx
excited_energies = vqd(H1, ansatz2, 2, 1, [2.7, 1e-2, 1e-3])
excited_energies
xxxxxxxxxx
[-1.0, 0.99365234375, 1.0012158203125, 1.0074306640625]
xxxxxxxxxx
assert(np.allclose(np.asarray(excited_energies), np.sort(np.linalg.eig(H1)[0]), atol=1e-2))
Interpreting the Results
- Both of our ansatz produced accurate results despite of ansatz 2 being more expressible than ansatz 1. One reason for this can be that the the ansatz 2 expressibility was covered the interested solution subspace. Another reason could be due to its more entangling capability.
- Convergence of VQE depends upon both the type of optimizer (gradient-based or point-search) and the choice of ansatz. In general more the number of parameters, and the depth of ansatz, more difficult will be the convergence.
- Number of shots did matter for non-optimal values of theta in ansatz 2. However, at optimal values of theta, less shots also gave good results. For noisy simulations, higher number of shots were required to get agreeable results.
- COBYLA and Powell performed far better than others in both noisy and ideal conditions. Next one in line was Nelder-Mead. Gradient-based optimizers gave much poor results. In future, it would nice to see results from specialized optimizers such as rotosolve and rotoselect, and also optimizers that calculate gradients via parameter-shift rule.
- Ansatz 2 performed better than ansatz 1 under Noisy simulation. However, mitigation improved the result for both of them.
- Out of all the all the noise models tested, we saw VQE still gave the (deviated) minima at the optimal value of theta, and performed worst for errors due to damping and depolarization.
- Even with depolarization, presence of errors in measurement gates gave much poor result than two-qubit gates which in turn gave worse results than single-qubit gates.
- VQD was able to perform better as we adjusted the values of
based on the difference between the eigen-energies. In general, values of these should be larger than the largest difference between the consecutive eigen-energies.