Adaptive circuits for quantum chemistry

Author: PennyLane dev team. Posted: 2021. Last updated: 13 September 2021

The key component of variational quantum algorithms for quantum chemistry is the circuit used to prepare electronic ground states of a molecule. The variational quantum eigensolver (VQE) [1], [2] is the method of choice for performing such quantum chemistry simulations on quantum devices with few qubits. For a given molecule, the appropriate circuit can be generated by using a pre-selected wavefunction ansatz, for example, the unitary coupled cluster with single and double excitations (UCCSD) [3]. In this approach, we include all possible single and double excitations of electrons from the occupied spin-orbitals of a reference state to the unoccupied spin-orbitals [4]. This makes the construction of the ansatz straightforward for any given molecule. However, using a pre-constructed ansatz has the disadvantage of reducing performance in favour of generality: the approach may work well in many cases, but it will not be optimized for a specific problem.

In practical applications, including all possible excitations usually increases the cost of the simulations without improving the accuracy of the results. This motivates implementing a strategy that allows for approximation of the contribution of the excitations and selects only those excitations that are found to be important for the given molecule. This can be done by using adaptive methods to construct a circuit for each given problem [5]. Using adaptive circuits helps improve performance at the cost of reducing generality.

../_images/main.png

Examples of selecting specific gates to generate adaptive circuits.

In this tutorial, you will learn how to adaptively build customized quantum chemistry circuits. This includes a recipe to adaptively select gates that have a significant contribution to the desired state, while neglecting those that have a small contribution. You will also learn how to use the functionality in PennyLane for leveraging the sparsity of a molecular Hamiltonian to make the computation of the expectation values even more efficient. Let’s get started!

Adaptive circuits

The main idea behind building adaptive circuits is to compute the gradients with respect to all possible excitation gates and then select gates based on the magnitude of the computed gradients.

There are different ways to make use of the gradient information and here we discuss one of these strategies and apply it to compute the ground state energy of LiH. This method requires constructing the Hamiltonian and determining all possible excitations, which we can do with functionality built into PennyLane. But we first need to define the molecular parameters, including atomic symbols and coordinates. Note that the atomic coordinates are in Bohr.

import pennylane as qml
from pennylane import qchem
from pennylane import numpy as np
import time

symbols = ["Li", "H"]
geometry = np.array([0.0, 0.0, 0.0, 0.0, 0.0, 2.969280527])

We now compute the molecular Hamiltonian in the STO-3G basis and obtain the electronic excitations. We restrict ourselves to single and double excitations, but higher-level ones such as triple and quadruple excitations can be considered as well. Each of these electronic excitations is represented by a gate that excites electrons from the occupied orbitals of a reference state to the unoccupied ones. This allows us to prepare a state that is a superposition of the reference state and all of the excited states.

H, qubits = qchem.molecular_hamiltonian(
    symbols,
    geometry,
    active_electrons=2,
    active_orbitals=5
)

active_electrons = 2

singles, doubles = qchem.excitations(active_electrons, qubits)

print(f"Total number of excitations = {len(singles) + len(doubles)}")

Out:

Total number of excitations = 24

Note that we have a total of 24 excitations which can be represented by the same number of excitation gates [4]. We now implement a strategy that constructs the circuit by adding groups of gates one at a time. We follow these steps:

  1. Compute gradients for all double excitations.
  2. Select the double excitations with gradients larger than a pre-defined threshold.
  3. Perform VQE to obtain the optimized parameters for the selected double excitations.
  4. Repeat steps 1 and 2 for the single excitations.
  5. Perform the final VQE optimization with all the selected excitations.

We create a circuit that applies a selected group of gates to a reference Hartree-Fock state.

hf_state = qchem.hf_state(active_electrons, qubits)


def circuit_1(params, wires, excitations):
    qml.BasisState(hf_state, wires=wires)

    for i, excitation in enumerate(excitations):
        if len(excitation) == 4:
            qml.DoubleExcitation(params[i], wires=excitation)
        else:
            qml.SingleExcitation(params[i], wires=excitation)

We now construct our first group of gates by including all the double excitations and compute the gradient for each one. We also need to define a device and a cost function. We initialize the parameter values to zero such that the gradients are computed with respect to the Hartree-Fock state.

dev = qml.device("default.qubit", wires=qubits)
cost_fn = qml.ExpvalCost(circuit_1, H, dev, optimize=True)

circuit_gradient = qml.grad(cost_fn, argnum=0)

params = [0.0] * len(doubles)
grads = circuit_gradient(params, excitations=doubles)

for i in range(len(doubles)):
    print(f"Excitation : {doubles[i]}, Gradient: {grads[i]}")

Out:

Excitation : [0, 1, 2, 3], Gradient: -0.012782175157697306
Excitation : [0, 1, 2, 5], Gradient: 1.423015351387224e-19
Excitation : [0, 1, 2, 7], Gradient: 1.151964808265848e-19
Excitation : [0, 1, 2, 9], Gradient: 0.03426451170172341
Excitation : [0, 1, 3, 4], Gradient: 6.09863722023096e-20
Excitation : [0, 1, 3, 6], Gradient: 3.388131789017206e-20
Excitation : [0, 1, 3, 8], Gradient: -0.03426451170172315
Excitation : [0, 1, 4, 5], Gradient: -0.023581529020686144
Excitation : [0, 1, 4, 7], Gradient: 0.0
Excitation : [0, 1, 4, 9], Gradient: 0.0
Excitation : [0, 1, 5, 6], Gradient: 0.0
Excitation : [0, 1, 5, 8], Gradient: -6.7762635780339814e-21
Excitation : [0, 1, 6, 7], Gradient: -0.023581529020686158
Excitation : [0, 1, 6, 9], Gradient: 0.0
Excitation : [0, 1, 7, 8], Gradient: -1.1519648082658432e-19
Excitation : [0, 1, 8, 9], Gradient: -0.1236227348559693

The computed gradients have different values, reflecting the contribution of each gate in the final state prepared by the circuit. Many of the gradient values are zero and we select those gates that have a gradient above a pre-defined threshold, which we set to \(10^{-5}\).

doubles_select = [doubles[i] for i in range(len(doubles)) if abs(grads[i]) > 1.0e-5]
doubles_select

Out:

[[0, 1, 2, 3], [0, 1, 2, 9], [0, 1, 3, 8], [0, 1, 4, 5], [0, 1, 6, 7], [0, 1, 8, 9]]

There are only 6 double excitation gates, out of the original 16, that have gradients above the threshold. We add the selected gates to the circuit and perform one optimization step to determine the updated parameters for the selected gates. We also need to define an optimizer. Note that the optimization is not very costly as we only have six gates in our circuit.

opt = qml.GradientDescentOptimizer(stepsize=0.5)

params_doubles = [0.0] * len(doubles_select)

for n in range(20):
    params_doubles = opt.step(cost_fn, params_doubles, excitations=doubles_select)

Now, we keep the selected gates in the circuit and compute the gradients with respect to all of the single excitation gates, selecting those that have a non-negligible gradient. To do that, we need to slightly modify our circuit such that parameters of the double excitation gates are kept fixed while the gradients are computed for the single excitation gates.

def circuit_2(params, wires, excitations, gates_select, params_select):
    qml.BasisState(hf_state, wires=wires)

    for i, gate in enumerate(gates_select):
        if len(gate) == 4:
            qml.DoubleExcitation(params_select[i], wires=gate)
        elif len(gate) == 2:
            qml.SingleExcitation(params_select[i], wires=gate)

    for i, gate in enumerate(excitations):
        if len(gate) == 4:
            qml.DoubleExcitation(params[i], wires=gate)
        elif len(gate) == 2:
            qml.SingleExcitation(params[i], wires=gate)

We now compute the gradients for the single excitation gates.

cost_fn = qml.ExpvalCost(circuit_2, H, dev, optimize=True)
circuit_gradient = qml.grad(cost_fn, argnum=0)
params = [0.0] * len(singles)

grads = circuit_gradient(
    params,
    excitations=singles,
    gates_select=doubles_select,
    params_select=params_doubles
)

for i in range(len(singles)):
    print(f"Excitation : {singles[i]}, Gradient: {grads[i]}")

Out:

Excitation : [0, 2], Gradient: -0.005062536239294277
Excitation : [0, 4], Gradient: 5.2391471973802364e-18
Excitation : [0, 6], Gradient: 1.0482472043385647e-18
Excitation : [0, 8], Gradient: -0.0009448044625726855
Excitation : [1, 3], Gradient: 0.004926616876963493
Excitation : [1, 5], Gradient: 3.075871892523104e-18
Excitation : [1, 7], Gradient: 7.796321646503736e-19
Excitation : [1, 9], Gradient: 0.0014535534853958448

Similar to the double excitation gates, we select those single excitations that have a gradient larger than a predefined threshold.

singles_select = [singles[i] for i in range(len(singles)) if abs(grads[i]) > 1.0e-5]
singles_select

Out:

[[0, 2], [0, 8], [1, 3], [1, 9]]

We now have all of the gates we need to build our circuit. The selected single and double excitation gates are highlighted in the figure below.

../_images/adapted_circuit.png

We perform one final step of optimization to get the ground-state energy. The resulting energy should match the exact energy of the ground electronic state of LiH which is -7.8825378193 Ha.

cost_fn = qml.ExpvalCost(circuit_1, H, dev, optimize=True)

params = [0.0] * len(doubles_select + singles_select)

gates_select = doubles_select + singles_select

for n in range(20):
    t1 = time.time()
    params, energy = opt.step_and_cost(cost_fn, params, excitations=gates_select)
    t2 = time.time()
    print("n = {:},  E = {:.8f} H, t = {:.2f} s".format(n, energy, t2 - t1))

Out:

n = 0,  E = -7.86266587 H, t = 1.74 s
n = 1,  E = -7.87094621 H, t = 1.83 s
n = 2,  E = -7.87563100 H, t = 1.66 s
n = 3,  E = -7.87829146 H, t = 1.79 s
n = 4,  E = -7.87981705 H, t = 1.87 s
n = 5,  E = -7.88070477 H, t = 1.65 s
n = 6,  E = -7.88123143 H, t = 1.66 s
n = 7,  E = -7.88155161 H, t = 1.84 s
n = 8,  E = -7.88175217 H, t = 1.86 s
n = 9,  E = -7.88188237 H, t = 1.68 s
n = 10,  E = -7.88197041 H, t = 1.91 s
n = 11,  E = -7.88203267 H, t = 1.83 s
n = 12,  E = -7.88207879 H, t = 1.63 s
n = 13,  E = -7.88211452 H, t = 1.75 s
n = 14,  E = -7.88214335 H, t = 1.63 s
n = 15,  E = -7.88216743 H, t = 1.74 s
n = 16,  E = -7.88218814 H, t = 1.69 s
n = 17,  E = -7.88220634 H, t = 1.82 s
n = 18,  E = -7.88222261 H, t = 1.86 s
n = 19,  E = -7.88223734 H, t = 1.94 s

Success! We obtained the ground state energy of LiH, within chemical accuracy, by having only 10 gates in our circuit. This is less than half of the total number of single and double excitations of LiH (24).

Sparse Hamiltonians

Molecular Hamiltonians and quantum states are sparse. For instance, let’s look at the Hamiltonian we built for LiH. We can compute its matrix representation in the computational basis using the PennyLane function sparse_hamiltonian(). This function returns the matrix in the SciPy sparse coordinate format.

H_sparse = qml.utils.sparse_hamiltonian(H)
H_sparse

Out:

<1024x1024 sparse matrix of type '<class 'numpy.complex128'>'
    with 11264 stored elements in COOrdinate format>

The matrix has \(1024^2=1,048,576\) entries, but only \(11264\) of them are non-zero.

../_images/h_sparse.png

Matrix representation of the LiH Hamiltonian in the computational basis.

Leveraging this sparsity can significantly reduce the simulation times. We use the implemented functionality in PennyLane for computing the expectation value of the sparse Hamiltonian observable. This can reduce the cost of simulations by orders of magnitude depending on the size of the molecule. We use the selected gates obtained in the previous steps and perform the final optimization step with the sparse method. Note that the sparse method currently only works with the parameter-shift differentiation method.

opt = qml.GradientDescentOptimizer(stepsize=0.5)

excitations = doubles_select + singles_select
params = [0.0] * len(excitations)


@qml.qnode(dev, diff_method="parameter-shift")
def circuit(params):
    qml.BasisState(hf_state, wires=range(qubits))

    for i, excitation in enumerate(excitations):
        if len(excitation) == 4:
            qml.DoubleExcitation(params[i], wires=excitation)
        elif len(excitation) == 2:
            qml.SingleExcitation(params[i], wires=excitation)

    return qml.expval(qml.SparseHamiltonian(H_sparse, wires=range(qubits)))


def cost(params):
    return circuit(params)


for n in range(20):
    t1 = time.time()
    params, energy = opt.step_and_cost(cost, params)
    t2 = time.time()
    print("n = {:},  E = {:.8f} H, t = {:.2f} s".format(n, energy, t2 - t1))

Out:

n = 0,  E = -7.86266587 H, t = 0.08 s
n = 1,  E = -7.87094621 H, t = 0.11 s
n = 2,  E = -7.87563100 H, t = 0.20 s
n = 3,  E = -7.87829146 H, t = 0.10 s
n = 4,  E = -7.87981705 H, t = 0.09 s
n = 5,  E = -7.88070477 H, t = 0.11 s
n = 6,  E = -7.88123143 H, t = 0.09 s
n = 7,  E = -7.88155161 H, t = 0.09 s
n = 8,  E = -7.88175217 H, t = 0.09 s
n = 9,  E = -7.88188237 H, t = 0.09 s
n = 10,  E = -7.88197041 H, t = 0.09 s
n = 11,  E = -7.88203267 H, t = 0.09 s
n = 12,  E = -7.88207879 H, t = 0.09 s
n = 13,  E = -7.88211452 H, t = 0.12 s
n = 14,  E = -7.88214335 H, t = 0.12 s
n = 15,  E = -7.88216743 H, t = 0.11 s
n = 16,  E = -7.88218814 H, t = 0.09 s
n = 17,  E = -7.88220634 H, t = 0.12 s
n = 18,  E = -7.88222261 H, t = 0.10 s
n = 19,  E = -7.88223734 H, t = 0.10 s

Using the sparse method reproduces the ground state energy while the optimization time is much shorter. The average iteration time for the sparse method is about 18 times smaller than that of the original non-sparse approach. The performance of the sparse optimization will be even better for larger molecules.

Conclusions

We have learned that building quantum chemistry circuits adaptively and using the functionality for sparse objects makes molecular simulations significantly more efficient. In this tutorial, we followed an adaptive strategy that selects a group of gates based on information about the gradients. This method can be extended such that the gates are selected one at time, or even to other more elaborate strategies [5].

References

[1]Alberto Peruzzo, Jarrod McClean et al., “A variational eigenvalue solver on a photonic quantum processor”. Nature Communications 5, 4213 (2014).
[2]Yudong Cao, Jonathan Romero, et al., “Quantum Chemistry in the Age of Quantum Computing”. Chem. Rev. 2019, 119, 19, 10856-10915.
[3]J. Romero, R. Babbush, et al., “Strategies for quantum computing molecular energies using the unitary coupled cluster ansatz”. arXiv:1701.02691
[4](1, 2) Givens rotations for quantum chemistry
[5](1, 2) Harper R. Grimsley, Sophia E. Economou, Edwin Barnes, Nicholas J. Mayhall, “An adaptive variational algorithm for exact molecular simulations on a quantum computer”. Nat. Commun. 2019, 10, 3007.

Total running time of the script: ( 1 minutes 23.402 seconds)

Gallery generated by Sphinx-Gallery