Extended VQE
In Tutorial 1, we considered a canonical VQE calculation at a single geometry with no resource optimization. However, in general, this will only be the first step in an analysis of the quantum algorithm. We may wish to expand on this analysis by considering more molecular geometries or systems – for example, looking at the energetics of bond dissociation. We also may wish to compare optimization methods in order to assess their effectiveness at reducing the overall cost with regards to quantum computational resources.
In this tutorial, we will look at how to achieve these goals using InQuanto. We start by examining bond dissociation in molecular hydrogen using a canonical VQE approach. Then, we will look at a slightly larger system – the bending and stretching of water. As this is a larger system, we will have to introduce optimizations to enable the simulations to run on a standard laptop. Specifically, we introduce how to reduce the active space (and thus the number of qubits in the quantum computation) by freezing orbitals using the inquanto-pyscf driver. Finally, we look at one optimization strategy in InQuanto - Ansatz parameter reduction by point group symmetry.
[1]:
from pytket.extensions.qiskit import AerStateBackend
from inquanto.express import run_vqe
from inquanto.minimizers import MinimizerScipy
from inquanto.ansatzes import FermionSpaceAnsatzUCCSD
from inquanto.mappings import QubitMappingJordanWigner
from inquanto.extensions.pyscf import ChemistryDriverPySCFMolecularRHF
import datetime
import matplotlib.pyplot as plt
import numpy as np
H2 Bond Stretching
After imports, we start by examining bond dissociation in molecular hydrogen in order to present a general workflow.
[2]:
def hydrogen_vqe_energy(bond_length):
basis = 'STO-3G'
geometry = [["H", [0, 0, 0]], ["H", [0, 0, bond_length]]]
charge = 0
driver = ChemistryDriverPySCFMolecularRHF(basis=basis, geometry=geometry, charge=charge)
fermionic_hamiltonian, fock_space, fock_state = driver.get_system()
jw = QubitMappingJordanWigner
qubit_hamiltonian = jw.operator_map(fermionic_hamiltonian)
ansatz = FermionSpaceAnsatzUCCSD(fock_space, fock_state, jw)
backend = AerStateBackend()
minimizer = MinimizerScipy(method="L-BFGS-B", disp=False)
vqe = run_vqe(ansatz, qubit_hamiltonian, backend=backend, with_gradient=True, minimizer=minimizer)
ground_state_energy = vqe.generate_report()["final_value"]
hartree_fock_energy = driver.mf_energy
return ground_state_energy, hartree_fock_energy
print(hydrogen_vqe_energy(0.741))
# TIMER BLOCK-0 BEGINS AT 2024-10-30 16:43:41.136701
# TIMER BLOCK-0 ENDS - DURATION (s): 3.9710166 [0:00:03.971017]
(-1.1372744055294364, -1.116706137236105)
The code here does all that is necessary to generate a ground state energy using canonical VQE for the hydrogen molecule, as in Tutorial 1. Here, we have wrapped it in a function to allow us to easily view the change with bond length:
[3]:
h2_bond_lengths = np.linspace(0.4,2.0,20)
h2_results = [hydrogen_vqe_energy(x) for x in h2_bond_lengths]
print(h2_results)
# TIMER BLOCK-1 BEGINS AT 2024-10-30 16:43:46.171431
# TIMER BLOCK-1 ENDS - DURATION (s): 4.0358547 [0:00:04.035855]
# TIMER BLOCK-2 BEGINS AT 2024-10-30 16:43:50.989389
# TIMER BLOCK-2 ENDS - DURATION (s): 3.7274606 [0:00:03.727461]
# TIMER BLOCK-3 BEGINS AT 2024-10-30 16:43:55.334293
# TIMER BLOCK-3 ENDS - DURATION (s): 3.4503037 [0:00:03.450304]
# TIMER BLOCK-4 BEGINS AT 2024-10-30 16:43:59.633368
# TIMER BLOCK-4 ENDS - DURATION (s): 3.2898507 [0:00:03.289851]
# TIMER BLOCK-5 BEGINS AT 2024-10-30 16:44:03.817291
# TIMER BLOCK-5 ENDS - DURATION (s): 3.1686891 [0:00:03.168689]
# TIMER BLOCK-6 BEGINS AT 2024-10-30 16:44:07.503222
# TIMER BLOCK-6 ENDS - DURATION (s): 3.0724748 [0:00:03.072475]
# TIMER BLOCK-7 BEGINS AT 2024-10-30 16:44:11.347740
# TIMER BLOCK-7 ENDS - DURATION (s): 3.1164460 [0:00:03.116446]
# TIMER BLOCK-8 BEGINS AT 2024-10-30 16:44:15.175979
# TIMER BLOCK-8 ENDS - DURATION (s): 2.7394776 [0:00:02.739478]
# TIMER BLOCK-9 BEGINS AT 2024-10-30 16:44:18.672157
# TIMER BLOCK-9 ENDS - DURATION (s): 2.2093293 [0:00:02.209329]
# TIMER BLOCK-10 BEGINS AT 2024-10-30 16:44:21.420632
# TIMER BLOCK-10 ENDS - DURATION (s): 2.1199450 [0:00:02.119945]
# TIMER BLOCK-11 BEGINS AT 2024-10-30 16:44:24.168668
# TIMER BLOCK-11 ENDS - DURATION (s): 1.8255762 [0:00:01.825576]
# TIMER BLOCK-12 BEGINS AT 2024-10-30 16:44:26.935028
# TIMER BLOCK-12 ENDS - DURATION (s): 2.0119183 [0:00:02.011918]
# TIMER BLOCK-13 BEGINS AT 2024-10-30 16:44:29.992998
# TIMER BLOCK-13 ENDS - DURATION (s): 1.9670910 [0:00:01.967091]
# TIMER BLOCK-14 BEGINS AT 2024-10-30 16:44:32.259165
# TIMER BLOCK-14 ENDS - DURATION (s): 2.1355196 [0:00:02.135520]
# TIMER BLOCK-15 BEGINS AT 2024-10-30 16:44:34.760024
# TIMER BLOCK-15 ENDS - DURATION (s): 2.1056226 [0:00:02.105623]
# TIMER BLOCK-16 BEGINS AT 2024-10-30 16:44:37.843971
# TIMER BLOCK-16 ENDS - DURATION (s): 2.1566833 [0:00:02.156683]
# TIMER BLOCK-17 BEGINS AT 2024-10-30 16:44:40.437622
# TIMER BLOCK-17 ENDS - DURATION (s): 1.8876533 [0:00:01.887653]
# TIMER BLOCK-18 BEGINS AT 2024-10-30 16:44:43.253917
# TIMER BLOCK-18 ENDS - DURATION (s): 1.8078745 [0:00:01.807875]
# TIMER BLOCK-19 BEGINS AT 2024-10-30 16:44:45.620761
# TIMER BLOCK-19 ENDS - DURATION (s): 1.8578411 [0:00:01.857841]
# TIMER BLOCK-20 BEGINS AT 2024-10-30 16:44:47.767584
# TIMER BLOCK-20 ENDS - DURATION (s): 1.6365678 [0:00:01.636568]
[(-0.9141497046270836, -0.9043613941635398), (-1.039644193368418, -1.0278952240485908), (-1.1027234512173354, -1.0885821110334752), (-1.1303984654811357, -1.1133931546411708), (-1.1373027360323475, -1.1169158055488677), (-1.1320031440770355, -1.1076586023375483), (-1.119647652656638, -1.0906923776851998), (-1.103357320131612, -1.0690432214496197), (-1.0850885605499416, -1.04456492118589), (-1.0661536707290789, -1.0184750668009017), (-1.047492324194345, -0.9916426659510071), (-1.0297900705318137, -0.9647203151980028), (-1.0135255910407297, -0.9382014307919535), (-0.998995920249617, -0.9124510069368059), (-0.9863403143934909, -0.8877296363816177), (-0.9755678355211701, -0.8642149983652552), (-0.9665878877681977, -0.8420201240601859), (-0.9592413973101761, -0.8212077601664908), (-0.9533300339756416, -0.8018012103709642), (-0.9486411121756362, -0.7837926542773532)]
We have successfully generated a potential energy curve for the dissociation of the H2 molecule using VQE and, as a reference, Hartree-Fock. We can now separate the VQE and HF results and plot them:
[4]:
h2_vqe_results,h2_hf_results = zip(*h2_results)
plt.plot(h2_bond_lengths,h2_vqe_results,label='VQE')
plt.plot(h2_bond_lengths,h2_hf_results,label='HF')
plt.xlabel('H-H Bond Length/Å')
plt.ylabel('Ground state energy/Ha')
plt.legend()
[4]:
<matplotlib.legend.Legend at 0x7f8c0ce1a150>
Here we can see the improvement obtained by using UCCSD VQE – indeed, this system is sufficiently low in number of spin-orbitals that UCCSD is exact (i.e. FCI-level). On the other hand, we can also observe the increasing inaccuracy of (restricted) Hartree-Fock at higher bond lengths.
H2O Bending - active space reduction
H2O in an STO-3G basis is a 14 spin-orbital (and thus 14 qubit for conventional qubit mappings) system. This is within the capacity of a classical computer to simulate, but such a simulation may perhaps require more resources than is practical for this tutorial. We can reduce the active spin-orbital space by freezing orbitals. While our purpose here is to demonstrate, in a real experiment it may be necessary to freeze orbitals in order to reduce the (exponentially growing) resources to a level
that is actually implementable. In InQuanto, orbital freezing is performed by passing the frozen
parameter to the driver:
[5]:
def water_bending_vqe_energy(bond_angle):
x_h2 = np.sin(bond_angle / 360 * np.pi)
x_h1 = -x_h2
y_h1 = np.cos(bond_angle / 360 * np.pi)
y_h2 = y_h1
geometry = [['H', [x_h1, y_h1, 0.]], ['O', [0., 0., 0.]], ['H', [x_h2, y_h2, 0.]]]
basis = 'STO-3G'
charge = 0
frozen = [0]
driver = ChemistryDriverPySCFMolecularRHF(basis=basis, geometry=geometry, charge=charge, frozen=frozen)
fermionic_hamiltonian, fock_space, fock_state = driver.get_system()
jw = QubitMappingJordanWigner
qubit_hamiltonian = jw.operator_map(fermionic_hamiltonian)
ansatz = FermionSpaceAnsatzUCCSD(fock_space, fock_state, jw)
backend = AerStateBackend()
minimizer = MinimizerScipy(method="L-BFGS-B", disp=False)
vqe = run_vqe(ansatz, qubit_hamiltonian, backend=backend, with_gradient=True, minimizer=minimizer)
ground_state_energy = vqe.generate_report()["final_value"]
hartree_fock_energy = driver.mf_energy
return ground_state_energy, hartree_fock_energy
print(water_bending_vqe_energy(104.5))
# TIMER BLOCK-21 BEGINS AT 2024-10-30 16:44:56.034528
# TIMER BLOCK-21 ENDS - DURATION (s): 153.2555107 [0:02:33.255511]
(-75.01966834467403, -74.96466253913084)
This block may take up to a minute to run, as the system is a bit bigger than molecular hydrogen. Here, we have asked the driver to freeze the lowest energy spatial orbital (i.e. the core electrons). Note that frozen orbitals are specified as a list of indices of spatial orbitals, not spin-orbitals - so every orbital frozen in this way will save two qubits. Note that for consistency, we have here specified the geometry in Cartesian co-ordinates by explicitly calculating the position of each atom. It is also possible in InQuanto to specify geometries in z-matrix format.
As before, we have successfully calculated the VQE and HF energy at (roughly) the equilibrium geometry. We can again calculate the effect of changing the bond angle and plot the results (this may take a few minutes to run - reducing the amount of data points generated will speed it up if needed):
[6]:
h2o_bond_angles = np.linspace(45.,180.,10)
h2o_bending_results = [water_bending_vqe_energy(x) for x in h2o_bond_angles]
# TIMER BLOCK-22 BEGINS AT 2024-10-30 16:47:32.489322
# TIMER BLOCK-22 ENDS - DURATION (s): 147.5711262 [0:02:27.571126]
# TIMER BLOCK-23 BEGINS AT 2024-10-30 16:50:03.101402
# TIMER BLOCK-23 ENDS - DURATION (s): 131.6115983 [0:02:11.611598]
# TIMER BLOCK-24 BEGINS AT 2024-10-30 16:52:16.768056
# TIMER BLOCK-24 ENDS - DURATION (s): 93.3536312 [0:01:33.353631]
# TIMER BLOCK-25 BEGINS AT 2024-10-30 16:53:51.939762
# TIMER BLOCK-25 ENDS - DURATION (s): 94.7769456 [0:01:34.776946]
# TIMER BLOCK-26 BEGINS AT 2024-10-30 16:55:28.628001
# TIMER BLOCK-26 ENDS - DURATION (s): 96.1523666 [0:01:36.152367]
# TIMER BLOCK-27 BEGINS AT 2024-10-30 16:57:06.673848
# TIMER BLOCK-27 ENDS - DURATION (s): 104.1345748 [0:01:44.134575]
# TIMER BLOCK-28 BEGINS AT 2024-10-30 16:58:52.760146
# TIMER BLOCK-28 ENDS - DURATION (s): 102.5373510 [0:01:42.537351]
# TIMER BLOCK-29 BEGINS AT 2024-10-30 17:00:37.357127
# TIMER BLOCK-29 ENDS - DURATION (s): 111.2587417 [0:01:51.258742]
# TIMER BLOCK-30 BEGINS AT 2024-10-30 17:02:30.529603
# TIMER BLOCK-30 ENDS - DURATION (s): 120.3243071 [0:02:00.324307]
# TIMER BLOCK-31 BEGINS AT 2024-10-30 17:04:32.235839
# TIMER BLOCK-31 ENDS - DURATION (s): 91.3656000 [0:01:31.365600]
[7]:
h2o_angle_vqe_results,h2o_angle_hf_results = zip(*h2o_bending_results)
plt.plot(h2o_bond_angles,h2o_angle_vqe_results,label='VQE')
plt.plot(h2o_bond_angles,h2o_angle_hf_results,label='HF')
plt.xlabel('HOH bond angle /°')
plt.ylabel('Ground state energy/Ha')
plt.legend()
[7]:
<matplotlib.legend.Legend at 0x7f8c0dc51390>
H2O Stretching - symmetry-allowed excitations
In larger systems, we may be interested in benchmarking the cost of VQE with various optimization schemes. Choosing optimization schemes is a question of balancing the need for accuracy and resource constraints. Several resource constraints occur when performing quantum algorithms – for instance, the number of qubits and the circuit length. Similar to space and time in classical computing, certain optimization schemes may reduce cost in one metric while having a detrimental effect on others. Such tradeoff in resources applies to VQE itself; VQE as an algorithm is designed to replace the (extremely) long quantum circuits of the phase estimation algorithm with significantly more but much shorter circuits.
Many such optimization schemes are available in InQuanto and examples of their use can be found in the examples directory. In this tutorial, we will look at the use of point group symmetry to exclude unphysical symmetry-violating excitations. This is a technique commonly used in quantum chemistry codes on classical computers, and can substantially reduce the number of Ansatz parameters. In turn, the quantum circuit length can be reduced, as is the difficulty of classical optimization (and consequentially the number of individual VQE shots required, and thus the overall runtime). As this technique is simply removing excitations that are unphysical, it is essentially “free” with regards to other computational resources.
First, we modify our VQE routine to incorporate point group symmetry – this time, in the context of symmetric bond stretching:
[8]:
def water_stretching_vqe_energy(bond_length):
x_h2 = bond_length * np.sin(104.45 / 360 * np.pi)
x_h1 = -x_h2
y_h1 = bond_length * np.cos(104.45 / 360 * np.pi)
y_h2 = y_h1
geometry = [['H', [x_h1, y_h1, 0.]], ['O', [0., 0., 0.]], ['H', [x_h2, y_h2, 0.]]]
basis = 'STO-3G'
charge = 0
frozen = [0]
driver = ChemistryDriverPySCFMolecularRHF(basis=basis, geometry=geometry, charge=charge, frozen=frozen,point_group_symmetry=True)
fermionic_hamiltonian, fock_space, fock_state = driver.get_system()
jw = QubitMappingJordanWigner
qubit_hamiltonian = jw.operator_map(fermionic_hamiltonian)
ansatz = FermionSpaceAnsatzUCCSD(fock_space, fock_state, jw)
backend = AerStateBackend()
minimizer = MinimizerScipy(method="L-BFGS-B", disp=False)
vqe = run_vqe(ansatz, qubit_hamiltonian, backend=backend, with_gradient=True, minimizer=minimizer)
ground_state_energy = vqe.generate_report()["final_value"]
hartree_fock_energy = driver.mf_energy
return ground_state_energy, hartree_fock_energy
print(water_stretching_vqe_energy(1.))
# TIMER BLOCK-32 BEGINS AT 2024-10-30 17:06:05.207723
# TIMER BLOCK-32 ENDS - DURATION (s): 32.2701180 [0:00:32.270118]
(-75.01969733754348, -74.9646831402391)
Here, incorporating point group symmetry is as simple as passing point_group_symmetry=True
to the driver. Note that the ability to use point group symmetry is reliant on the capacity of the underlying classical quantum chemistry package (in this case, PySCF). We then generate a plot of the change in the ground state energy as the bonds stretch:
[9]:
h2o_bond_lengths = np.linspace(0.6,2.,10)
h2o_stretching_results = [water_stretching_vqe_energy(x) for x in h2o_bond_lengths]
# TIMER BLOCK-33 BEGINS AT 2024-10-30 17:06:39.056564
# TIMER BLOCK-33 ENDS - DURATION (s): 27.8122375 [0:00:27.812237]
# TIMER BLOCK-34 BEGINS AT 2024-10-30 17:07:08.346952
# TIMER BLOCK-34 ENDS - DURATION (s): 27.4166968 [0:00:27.416697]
# TIMER BLOCK-35 BEGINS AT 2024-10-30 17:07:37.237101
# TIMER BLOCK-35 ENDS - DURATION (s): 30.5406426 [0:00:30.540643]
# TIMER BLOCK-36 BEGINS AT 2024-10-30 17:08:09.219422
# TIMER BLOCK-36 ENDS - DURATION (s): 30.3685859 [0:00:30.368586]
# TIMER BLOCK-37 BEGINS AT 2024-10-30 17:08:41.110292
# TIMER BLOCK-37 ENDS - DURATION (s): 33.5521658 [0:00:33.552166]
# TIMER BLOCK-38 BEGINS AT 2024-10-30 17:09:16.101997
# TIMER BLOCK-38 ENDS - DURATION (s): 41.9080718 [0:00:41.908072]
# TIMER BLOCK-39 BEGINS AT 2024-10-30 17:09:59.508629
# TIMER BLOCK-39 ENDS - DURATION (s): 38.6801983 [0:00:38.680198]
# TIMER BLOCK-40 BEGINS AT 2024-10-30 17:10:39.754052
# TIMER BLOCK-40 ENDS - DURATION (s): 47.9701067 [0:00:47.970107]
# TIMER BLOCK-41 BEGINS AT 2024-10-30 17:11:29.169592
# TIMER BLOCK-41 ENDS - DURATION (s): 60.7648270 [0:01:00.764827]
# TIMER BLOCK-42 BEGINS AT 2024-10-30 17:12:31.746106
# TIMER BLOCK-42 ENDS - DURATION (s): 67.5135041 [0:01:07.513504]
[10]:
h2o_lengths_vqe_results,h2o_lengths_hf_results = zip(*h2o_stretching_results)
plt.plot(h2o_bond_lengths,h2o_lengths_vqe_results,label='VQE')
plt.plot(h2o_bond_lengths,h2o_lengths_hf_results,label='HF')
plt.xlabel('OH bond length /Å')
plt.ylabel('Ground state energy/Ha')
plt.legend()
[10]:
<matplotlib.legend.Legend at 0x7f8c0dd9b910>
We have successfully demonstrated that the point group symmetry reductions yield the correct ground state energy. However, we have not yet looked at the resource reductions obtained. We can adapt our VQE wrapper functions to return this, but for simplicity here we just look at one configuration:
[11]:
x_h2 = np.sin(104.45 / 360 * np.pi)
x_h1 = -x_h2
y_h1 = np.cos(104.45 / 360 * np.pi)
y_h2 = y_h1
geometry = [['H', [x_h1, y_h1, 0.]], ['O', [0., 0., 0.]], ['H', [x_h2, y_h2, 0.]]]
basis = 'STO-3G'
charge = 0
frozen = [0]
driver_with_symmetry = ChemistryDriverPySCFMolecularRHF(basis=basis, geometry=geometry, charge=charge, frozen=frozen,point_group_symmetry=True)
driver_without_symmetry = ChemistryDriverPySCFMolecularRHF(basis=basis, geometry=geometry, charge=charge, frozen=frozen,point_group_symmetry=False)
fermionic_hamiltonian_with_symmetry, fock_space_with_symmetry, fock_state_with_symmetry = driver_with_symmetry.get_system()
fermionic_hamiltonian_without_symmetry, fock_space_without_symmetry, fock_state_without_symmetry = driver_without_symmetry.get_system()
jw = QubitMappingJordanWigner()
ansatz_with_symmetry = FermionSpaceAnsatzUCCSD(fock_space_with_symmetry, fock_state_with_symmetry, jw)
ansatz_without_symmetry = FermionSpaceAnsatzUCCSD(fock_space_without_symmetry, fock_state_without_symmetry, jw)
For simplicity, we restrict ourselves to looking at the impact on the resources required for generating the Ansatz state. Note that many quantum resources can be estimated without actually running VQE in this manner; this dramatically decreases the resources necessary to perform an experiment.
If we generate a report for each Ansatz:
[12]:
print('### ANSATZ RESOURCES WITH SYMMETRY REDUCTION ###')
print(ansatz_with_symmetry.generate_report())
print('\n### ANSATZ RESOURCES WITHOUT SYMMETRY REDUCTION ###')
print(ansatz_without_symmetry.generate_report())
### ANSATZ RESOURCES WITH SYMMETRY REDUCTION ###
{'ansatz_circuit_depth': 841, 'ansatz_circuit_gates': 2260, 'n_parameters': 30, 'n_qubits': 12}
### ANSATZ RESOURCES WITHOUT SYMMETRY REDUCTION ###
{'ansatz_circuit_depth': 2798, 'ansatz_circuit_gates': 7032, 'n_parameters': 92, 'n_qubits': 12}