Adaptive methods¶
Despite VQE being cheaper than QPE, circuit depth is still a big problem for today’s quantum hardware.
from qibochem.driver import Molecule
from qibochem.ansatz import ucc_ansatz
mol = Molecule([("Li", (0.0, 0.0, 0.0)), ("H", (0.0, 0.0, 1.4))])
mol.run_pyscf()
mol.hf_embedding(active=[0, 1, 2, 5])
circuit = ucc_ansatz(mol)
print(circuit.summary())
Output:
Circuit depth = 1874
Total number of gates = 3300
Number of qubits = 8
Most common gates:
cx: 1312
h: 1216
sdg: 304
s: 304
rz: 160
x: 4
As shown in the above code block, the full UCCSD circuit for a simplified LiH/STO-3G system has a circuit depth of 1874 (!), with more than 1000 CNOT gates required! Hence, there is still a need to further reduce and simplify the circuit ansatzes used for running a VQE simulation.
Other than designing shorter and more efficient circuit ansatzes, one alternative approach is through the use of energy gradients - for instance, through the Parameter Shift Rule on hardware - to filter and reduce the number of fermionic excitations in a circuit ansatz. [1] [2] This is known as an adaptive method, in the sense that the quantum gates used to construct the circuit ansatz, as well as its actual structure and arrangement is not fixed, and varies depending on the molecular system under study.
For example, in a H2/STO-3G system mapped with the Jordan-Wigner transformation, there are three possible spin-allowed fermionic excitations:
two single excitations ([0, 2]
, [1, 3]
) and one double excitation ([0, 1, 2, 3]
).
The full UCCSD circuit for this system has been shown in an earlier example, and it requires 64 CNOT gates for this simple molecular system.
Let’s look at the gradients of each of the individual fermionic excitations:
from qibo.derivative import parameter_shift
from qibochem.driver import Molecule
from qibochem.ansatz import hf_circuit, ucc_circuit
mol = Molecule([("H", (0.0, 0.0, 0.0)), ("H", (0.0, 0.0, 0.7))])
mol.run_pyscf()
hamiltonian = mol.hamiltonian()
excitations = [[0, 1, 2, 3], [0, 2], [1, 3]]
circuit = hf_circuit(4, 2)
for excitation in excitations:
_circuit = circuit.copy()
_circuit += ucc_circuit(4, excitation)
n_parameters = len(_circuit.get_parameters())
gradients = [round(parameter_shift(_circuit, hamiltonian, index), 5) for index in range(n_parameters)]
print(f"Energy gradients for {excitation}: {gradients}")
Output:
Energy gradients for [0, 1, 2, 3]: [0.179, -0.179, -0.179, -0.179, 0.179, 0.179, 0.179, -0.179]
Energy gradients for [0, 2]: [0.0, 0.0]
Energy gradients for [1, 3]: [0.0, 0.0]
Only the doubles excitation has a non-zero energy gradient, which follows Brillouin’s theorem. Running the VQE with only the doubles excitation then gives:
import numpy as np
from qibo.models import VQE
from qibochem.driver import Molecule
from qibochem.ansatz import hf_circuit, ucc_circuit
mol = Molecule([("H", (0.0, 0.0, 0.0)), ("H", (0.0, 0.0, 0.7))])
mol.run_pyscf()
hamiltonian = mol.hamiltonian()
circuit = hf_circuit(4, 2)
circuit += ucc_circuit(4, [0, 1, 2, 3])
vqe = VQE(circuit, hamiltonian)
# Optimize starting from a random guess for the variational parameters
initial_parameters = np.random.uniform(0, 2*np.pi, len(circuit.get_parameters()))
best, params, extra = vqe.minimize(initial_parameters, method='BFGS', compile=False)
# Exact result
print(f"Exact result: {mol.eigenvalues(hamiltonian)[0]:.7f}")
print(f" VQE result: {best:.7f}")
Output:
Exact result: -1.1361895
VQE result: -1.1361895
We managed to find the exact result by applying only the doubles excitation!
Next, let’s look at the potential savings for the simplified LiH/STO-3G system. To reduce the circuit depth further, we will use the more modern ansatz, the Givens excitation circuit from Arrazola et al., [1] instead of the UCC ansatz.
As was done in the above example, we will start with a HF circuit, then find the gradients for each circuit ansatz corresponding to a fermionic excitation. After that, the excitation with the largest absolute value of the gradient will be added to the initial circuit, followed by a VQE simulation.
from qibo.derivative import parameter_shift
from qibo.models import VQE
from qibochem.driver import Molecule
from qibochem.ansatz import hf_circuit, givens_excitation_circuit, generate_excitations, sort_excitations
mol = Molecule([("Li", (0.0, 0.0, 0.0)), ("H", (0.0, 0.0, 1.4))])
mol.run_pyscf()
mol.hf_embedding(active=[0, 1, 2, 5])
hamiltonian = mol.hamiltonian()
n_qubits = mol.n_active_orbs
n_elec = mol.n_active_e
circuit = hf_circuit(n_qubits, n_elec)
excitations = sort_excitations(generate_excitations(2, list(range(n_elec)), list(range(n_elec, n_qubits))))
excitations += sort_excitations(generate_excitations(1, list(range(n_elec)), list(range(n_elec, n_qubits))))
excitation_gradients = {}
for excitation in excitations:
_circuit = circuit.copy()
_circuit += givens_excitation_circuit(n_qubits, excitation)
n_parameters = len(_circuit.get_parameters())
gradient = [round(parameter_shift(_circuit, hamiltonian, index), 5) for index in range(n_parameters)]
print(f"Energy gradients for {excitation}: {gradient}")
excitation_gradients[tuple(excitation)] = gradient[0] # Gradient magnitude is equal throughout
# Find the excitation corresponding to the largest gradient, and add it to the circuit
max_grad = max(excitation_gradients, key=lambda x: abs(excitation_gradientis.get(x)))
print(f"\nExcitation with the largest gradient: {max_grad}; Gradient = {excitation_gradients[max_grad]}")
circuit += givens_excitation_circuit(n_qubits, max_grad)
# Run VQE with the updated circuit
vqe = VQE(circuit, hamiltonian)
circuit_parameters = [param for _tuple in circuit.get_parameters() for param in _tuple]
best, params, extra = vqe.minimize(circuit_parameters, method='BFGS', compile=False)
print(f" HF energy: {mol.e_hf:.7f}")
print(f"VQE result: {best:.7f}")
Output:
Energy gradients for [0, 1, 4, 5]: [0.02132, -0.02132, 0.02132, -0.02132, -0.02132, 0.02132, -0.02132, 0.02132]
Energy gradients for [0, 1, 6, 7]: [0.00569, -0.00569, 0.00569, -0.00569, -0.00569, 0.00569, -0.00569, 0.00569]
Energy gradients for [2, 3, 4, 5]: [0.01136, -0.01136, 0.01136, -0.01136, -0.01136, 0.01136, -0.01136, 0.01136]
Energy gradients for [2, 3, 6, 7]: [0.12225, -0.12225, 0.12225, -0.12225, -0.12225, 0.12225, -0.12225, 0.12225]
Energy gradients for [0, 1, 4, 7]: [0.00016, -0.00016, 0.00016, -0.00016, -0.00016, 0.00016, -0.00016, 0.00016]
Energy gradients for [0, 1, 5, 6]: [-0.00016, 0.00016, -0.00016, 0.00016, 0.00016, -0.00016, 0.00016, -0.00016]
Energy gradients for [2, 3, 4, 7]: [-0.03254, 0.03254, -0.03254, 0.03254, 0.03254, -0.03254, 0.03254, -0.03254]
Energy gradients for [2, 3, 5, 6]: [0.03254, -0.03254, 0.03254, -0.03254, -0.03254, 0.03254, -0.03254, 0.03254]
Energy gradients for [0, 3, 4, 5]: [0.00029, -0.00029, 0.00029, -0.00029, -0.00029, 0.00029, -0.00029, 0.00029]
Energy gradients for [1, 2, 4, 5]: [-0.00029, 0.00029, -0.00029, 0.00029, 0.00029, -0.00029, 0.00029, -0.00029]
Energy gradients for [0, 3, 6, 7]: [0.00108, -0.00108, 0.00108, -0.00108, -0.00108, 0.00108, -0.00108, 0.00108]
Energy gradients for [1, 2, 6, 7]: [-0.00108, 0.00108, -0.00108, 0.00108, 0.00108, -0.00108, 0.00108, -0.00108]
Energy gradients for [0, 2, 4, 6]: [0.00299, -0.00299, 0.00299, -0.00299, -0.00299, 0.00299, -0.00299, 0.00299]
Energy gradients for [1, 3, 5, 7]: [0.00299, -0.00299, 0.00299, -0.00299, -0.00299, 0.00299, -0.00299, 0.00299]
Energy gradients for [0, 3, 4, 7]: [-0.00236, 0.00236, -0.00236, 0.00236, 0.00236, -0.00236, 0.00236, -0.00236]
Energy gradients for [0, 3, 5, 6]: [-0.00063, 0.00063, -0.00063, 0.00063, 0.00063, -0.00063, 0.00063, -0.00063]
Energy gradients for [1, 2, 4, 7]: [-0.00063, 0.00063, -0.00063, 0.00063, 0.00063, -0.00063, 0.00063, -0.00063]
Energy gradients for [1, 2, 5, 6]: [-0.00236, 0.00236, -0.00236, 0.00236, 0.00236, -0.00236, 0.00236, -0.00236]
Energy gradients for [0, 4]: [0.0, -0.0]
Energy gradients for [1, 5]: [-0.0, 0.0]
Energy gradients for [0, 6]: [0.0, -0.0]
Energy gradients for [1, 7]: [-0.0, 0.0]
Energy gradients for [2, 4]: [-0.0, 0.0]
Energy gradients for [3, 5]: [0.0, -0.0]
Energy gradients for [2, 6]: [-0.0, 0.0]
Energy gradients for [3, 7]: [0.0, -0.0]
Excitation with the largest gradient: (2, 3, 6, 7); Gradient = 0.12225
HF energy: -7.8605387
VQE result: -7.8732886
After adding the circuit ansatz corresponding to one double excitation and running VQE, the resultant energy was found to be about 0.01 Hartrees lower compared to the bare HF ansatz. Clearly, there is still room for further improvement in the obtained energies.
To do so, we further extend the circuit ansatz by adding in more excitations iteratively, with the excitation with the largest (in magnitute) energy gradients added on at each step. This can be carried out until the difference between each iteration is small (<0.001 Hartrees), or until there are no more remaining excitations to be added.
Warning
The code block below might take a few minutes to run!
import numpy as np
from qibo.models import VQE
from qibo.derivative import parameter_shift
from qibochem.driver import Molecule
from qibochem.ansatz import hf_circuit, givens_excitation_circuit, generate_excitations, sort_excitations
mol = Molecule([("Li", (0.0, 0.0, 0.0)), ("H", (0.0, 0.0, 1.4))])
mol.run_pyscf()
mol.hf_embedding(active=[0, 1, 2, 5])
hamiltonian = mol.hamiltonian()
exact_result = mol.eigenvalues(hamiltonian)[0]
n_qubits = mol.n_active_orbs
n_elec = mol.n_active_e
circuit = hf_circuit(n_qubits, n_elec)
excitations = sort_excitations(generate_excitations(2, list(range(n_elec)), list(range(n_elec, n_qubits))))
excitations += sort_excitations(generate_excitations(1, list(range(n_elec)), list(range(n_elec, n_qubits))))
excitation_gradient = {tuple(excitation):0.0 for excitation in excitations}
count = 0
current_energy = mol.e_hf
n_fixed_params = 0
# Iterating through all excitations; loop breaks if difference in VQE result between excitations is v small
while excitations:
print(f"Iteration {count+1}:")
for excitation in excitations:
_circuit = circuit.copy()
_circuit += givens_excitation_circuit(n_qubits, excitation)
n_parameters = len(_circuit.get_parameters())
gradient = [round(parameter_shift(_circuit, hamiltonian, index), 5) for index in range(n_parameters)][n_fixed_params:]
# print(f"Energy gradient for {excitation}: {gradient}")
excitation_gradient[tuple(excitation)] = gradient[0] # Gradient magnitude is equal throughout
# Find the excitation corresponding to the largest gradient, and add it to the circuit
max_grad = max(excitation_gradient, key=lambda x: abs(excitation_gradient.get(x)))
print(f"Excitation with the largest gradient: {max_grad}; Gradient = {excitation_gradient[max_grad]}")
circuit += givens_excitation_circuit(n_qubits, max_grad)
# Remove max_grad from excitations and excitation_data
excitations.pop(excitations.index(list(max_grad)))
del excitation_gradient[max_grad]
# Run VQE with the updated circuit
vqe = VQE(circuit, hamiltonian)
circuit_parameters = [param for _tuple in circuit.get_parameters() for param in _tuple]
best, params, extra = vqe.minimize(circuit_parameters, method='BFGS', compile=False)
n_fixed_params = len(params)
print(f"\nExact result: {exact_result:.7f}")
print(f" VQE result: {best:.7f}")
energy_diff = best - current_energy
print(f"Difference to previous result: {energy_diff:.7f}")
if abs(energy_diff) < 1e-3:
print("\nEnergy has converged; exiting while loop")
break
print()
# Update circuit parameters and current energy
circuit.set_parameters(params)
current_energy = best
count += 1
print("\nFinal circuit:")
print(circuit.draw())
print("\nCircuit statistics:")
print(circuit.summary())
Output:
Iteration 1:
Excitation with the largest gradient: (2, 3, 6, 7); Gradient = 0.12225
Exact result: -7.8770974
VQE result: -7.8732886
Difference to previous result: -0.0127499
Iteration 2:
Excitation with the largest gradient: (2, 3, 4, 7); Gradient = -0.03485
Exact result: -7.8770974
VQE result: -7.8748417
Difference to previous result: -0.0015531
Iteration 3:
Excitation with the largest gradient: (2, 3, 5, 6); Gradient = 0.03364
Exact result: -7.8770974
VQE result: -7.8762910
Difference to previous result: -0.0014493
Iteration 4:
Excitation with the largest gradient: (0, 1, 4, 5); Gradient = 0.02124
Exact result: -7.8770974
VQE result: -7.8763762
Difference to previous result: -0.0000853
Energy has converged; exiting while loop
Final circuit:
q0: ─X──────────────────────────────────────────────────────────────────── ...
q1: ─X──────────────────────────────────────────────────────────────────── ...
q2: ─X───o─H─o───RY─o─────RY───X─RY─────o─RY─o─X─H─o─────o─H─o───RY─o───── ...
q3: ─X───|───X───RY─|───X─RY─X─|─RY─X───|─RY─X─|───|─────|───X───RY─|───X─ ...
q4: ─────|──────────|───|────|─|────|───|──────|───|───o─X─────o────|───|─ ...
q5: ─────|──────────|───|────|─|────|───|──────|───|───|───────|────|───|─ ...
q6: ───o─X─────o────|───|────o─o────|───|──────o───X─o─|───────|────|───|─ ...
q7: ───X───H───X────X─H─o───────────o─H─X────────H───X─X───H───X────X─H─o─ ...
q0: ... ────────────────────────────────────────────────────────────────────── ...
q1: ... ────────────────────────────────────────────────────────────────────── ...
q2: ... RY───X─RY─────o─RY─o─X─H─o─────o─H─o───RY─o─────RY───X─RY─────o─RY─o─X ...
q3: ... RY─X─|─RY─X───|─RY─X─|───|─────|───X───RY─|───X─RY─X─|─RY─X───|─RY─X─| ...
q4: ... ───o─o────|───|──────o───X─o───|──────────|───|────|─|────|───|──────| ...
q5: ... ──────────|───|────────────|─o─X─────o────|───|────o─o────|───|──────o ...
q6: ... ──────────|───|────────────|─X───H───X────X─H─o───────────o─H─X─────── ...
q7: ... ──────────o─H─X────────H───X────────────────────────────────────────── ...
q0: ... ─────────o─H─o───RY─o─────RY───X─RY─────o─RY─o─X─H─o───
q1: ... ─────────|───X───RY─|───X─RY─X─|─RY─X───|─RY─X─|───|───
q2: ... ─H─o─────|──────────|───|────|─|────|───|──────|───|───
q3: ... ───|─────|──────────|───|────|─|────|───|──────|───|───
q4: ... ───|───o─X─────o────|───|────o─o────|───|──────o───X─o─
q5: ... ───X─o─X───H───X────X─H─o───────────o─H─X────────H───X─
q6: ... ─H───X─────────────────────────────────────────────────
q7: ... ───────────────────────────────────────────────────────
Circuit statistics:
Circuit depth = 78
Total number of gates = 116
Number of qubits = 8
Most common gates:
cx: 56
ry: 32
h: 24
x: 4
Recall that the full UCCSD circuit for our system had a circuit depth of 1874, with more than 1000 CNOT gates required. In contrast, the use of a simpler circuit ansatz in conjunction with an adaptive approach allowed us to find a VQE energy that is within chemical accuracy, while using only 56 CNOT gates and with a final gate depth of only 78. This is a >20-fold reduction in the gate depth and number of CNOT gates used!
References