Variational Quantum Eigensolver using Estimator primitive and sessions
Background
Variational quantum algorithms are promising candidate hybrid-algorithms for observing the utility of quantum computation on noisy near-term devices. Variational algorithms are characterized by the use of a classical optimization algorithm to iteratively update a parameterized trial solution, or "ansatz". Chief among these methods is the Variational Quantum Eigensolver (VQE) that aims to solve for the ground state of a given Hamiltonian represented as a linear combination of Pauli terms, with an ansatz circuit where the number of parameters to optimize over is polynomial in the number of qubits. Given that size of the full solution vector is exponential in the number of qubits, successful minimization using VQE requires, in general, additional problem specific information to define the structure of the ansatz circuit.
Executing a VQE algorithm requires the following 3 components:
- Hamiltonian and ansatz (problem specification)
- Qiskit Runtime estimator
- Classical optimizer
Although the Hamiltonian and ansatz require domain specific knowledge to construct, these details are immaterial to the Runtime, and we can execute a wide class of VQE problems in the same manner.
Setup
Here we import the tools needed for a VQE experiment. The primary imports can be grouped logically into three components that correspond to the three required elements.
Output:
Initialize Runtime service and select backend
Here we instantiate the Runtime service that gives access to the simulator that we use in this tutorial.
Output:
Output:
'ibmq_qasm_simulator'
Problem specification
Here we define the problem instance for our VQE algorithm. Although the problem in question can come from a variety of domains, the form for execution via Runtime is the same. Qiskit provides a convenience class for expressing Hamiltonians in Pauli form, and a collection of widely used ansatz circuits in the
Here, our example Hamiltonian is derived from a quantum chemistry problem
Output:
and our choice of ansatz is the
Output:
From the previous figure we see that our ansatz circuit is defined by a vector of parameters, , with the total number given by:
Output:
16
VQE cost function and minimization
Like many classical optimization problems, the solution to a VQE problem can be formulated as minimization of a scalar cost function. By definition, VQE looks to find the ground state solution to a Hamiltonian by optimizing the ansatz circuit parameters to minimize the expectation value (energy) of the Hamiltonian. With the Runtime
Output:
Note that, in addition to the array of optimization parameters that must be the first argument, we use additional arguments to pass the terms needed in the cost function.
We are now free to use a classical optimizer of our choice to minimize our cost function. Here we use the COBYLA routine from SciPy via the
To begin the routine, we start by specifying a random initial set of parameters,
Output:
and because we are sending a large number of jobs that we would like to execute together, here we use a
Output:
At the terminus of this routine we have a result in the standard SciPy
Output:
message: Optimization terminated successfully.
success: True
status: 1
fun: -0.7029303831467195
x: [ 5.698e+00 2.919e+00 ... 6.052e-01 1.883e+00]
nfev: 343
maxcv: 0.0
Adding a callback function
As it stands now, we are unable to save intermediate results from the iteration process, view the value of the cost function per iteration, nor are we able to monitor the progress of the routine. Callback functions are a standard way for users to obtain additional information about the status of an iterative algorithm. The standard SciPy callback routine allows for returning only the interim vector at each iteration. However, it is possible to do much more than this.
We show how to use a mutable object, such as a dictionary, to store the current vector at each iteration. This is useful when restarting the routine due to failure, or seeing the current iteration number and average time per iteration while running.
Output:
We can now repeat the experiment setting the
Output:
Output:
Iters. done: 163 [Avg. time per iter: 27.21]
If the procedure terminates correctly, then the
Output:
True
Output:
True
We can also now view the progress towards convergence as monitored by the cost history at each iteration:
Output:
Text(0, 0.5, 'Cost')
Output:
Output:
'0.11.3'
Output:
'0.25.0'