Quantinuum H-Series - Quantum Subspace Expansion

This tutorial illustrates the computation of molecular excited states through the application of the Quantum Subspace Expansion (QSE) technique on the Quantinuum H-series remote emulator. The QSE method leverages a precise calculation of the ground state energy for a specific molecular configuration to approximate the energies of corresponding excited states. Consider a stable molecule, denoted by a Hamiltonian \(H\), and let \(\left|\Psi_{0}\right\rangle\) represent the outcome of the Variational Quantum Eigensolver (VQE) algorithm employed to estimate the ground state energy of \(H\). More information on the QSE method can be found in the publication available at this link.

The QSE technique constructs a subspace of state vectors \(\left|\Psi_j^k\right\rangle\) formed by one-electron excitations of the ground state wavefunction:

\begin{equation} \left|\Psi_{j}^{k}\right\rangle = c_k^{\dagger}c_{j}\left|\Psi_0\right\rangle. \end{equation}

where \(c_k^{\dagger}, c_{j}\) are the fermionic creation and annihilation operators over spin orbitals \(k\) and \(j\), respectively. That is, these vectors are formed by reducing the occupation of spin orbital \(j\) by one, and increasing the occupation of spin orbital \(k\) by one. The vectors are not in general orthogonal to \(\Psi_{0}\) hence we will need to calculate an overlap matrix.

Within this subspace, we solve a generalized eigenvalue problem. Consider the operator \(\hat{H} \) with matrix elements given by $\((H)_{jk}^{lm} = \langle\Psi_j^l \left| \hat{H} \right| \Psi_k^m\rangle,\)$

and define an overlap matrix \(S\) whose matrix elements are given by

\[S_{jk}^{lm} = \langle \Psi_j^l \left|\Psi_k^m\right\rangle.\]

The generalized eigenvalue equation to be solved is

\begin{equation} HC=SCE, \end{equation}

where \(C\) is the matrix of eigenvectors, and \(E\) is the vector of eigenvalues. Crucially, the energy eigenvalues \(E\) provide an estimate of the excited state energies of \(H\) as well as a refined value of the ground state energy.

Notice that the solution to the generalized eigenvalue equation can be done on a classical computer, provided \(H\) and \(S\) have been calculated. The matrix elements of both of these matrices can be constructed using a quantum computer, in the following way. First, re-write the matrix elements in terms of \(\left | \Psi_{0}\right \rangle\):

\begin{align} (H){jk}^{lm} =& \langle\Psi_j^l \left| \hat{H} \right| \Psi_k^m\rangle = \langle \Psi{0} | c_{j}^\dagger c_{l} \hat{H}c_{m}^{\dagger}c_{k}|\Psi_{0}\rangle\ S_{jk}^{lm} &= \langle \Psi_j^l \left|\Psi_k^m\right\rangle = \langle \Psi_{0} | c_{j}^\dagger c_{l} c_{m}^{\dagger}c_{k}|\Psi_{0}\rangle. \end{align}

The matrix elements can be calculated using a quantum computer or simulator. By transforming the operators \(c_{j}^\dagger c_{l} c_{k}^{\dagger}c_{m}\) and \(c_{l}^\dagger c_{j} \hat{H} c_{m}^{\dagger}c_{k}\) to a set of Pauli quantum gates according to an appropriate scheme such as Jordan-Wigner or Bravyi-Kitaev, apply this gate set to the ground state wavefunction (constructed with the coefficients obtained from the VQE calculation) and perform a measurement to obtain the expected value. By measuring these expectation values, we obtain estimates of \(S_{jk}^{lm}\) and \(H_{jk}^{lm}\), respectively.

This tutorial will guide you through the process of performing QSE and will show how to collate the resulting eigen-energies and eigen-vectors. To demonstrate these concepts, we will utilize the methane (CH\(_4\)) chemical system. Note that to follow this tutorial, you must have been granted access to Quantinuum systems by your InQuanto administrator.

The steps below are such:

  • Define the system

  • Prepare the approximate ground state obtained by the VQE algorithm

  • Configure the Quantinuum backend

  • Establish the PMSV error mitigation

  • Define protocol for measuring QSE matrix elements

  • Build InQuanto computables

  • Submit and retreive InQuanto computables

  • The black-box approach using the AlgorithmQSE

After defining the Z-matrix for Methane, the Hamiltonian object is created using a restricted Hartree-Fock (RHF) InQuanto-PySCF driver.

The get_system function is responsible for computing the fermionic Hamiltonian operator, Fock space, and Hartree Fock state.

import warnings
warnings.filterwarnings('ignore')

from inquanto.extensions.pyscf import ChemistryDriverPySCFMolecularRHF

zmatrix = """
C
H             1    1.083000
H             1    1.083000      2  109.471000
H             1    1.083000      2  109.471000      3  120.000000
H             1    1.083000      2  109.471000      4  120.000000
"""

driver = ChemistryDriverPySCFMolecularRHF(
    zmatrix=zmatrix,
    charge=0,
    frozen=[0, 1, 2, 3, 7, 8],
    basis="STO-3G",
    point_group_symmetry=True,
)

hamiltonian, space, state = driver.get_system()

The qubit_encode function carries out qubit encoding, utilizing the mapping class associated with the current integral operator. The default mapping approach is the Jordan-Wigner method. Additionally, we employ the compress method, which eliminates Hamiltonian terms with coefficients less than abs_tol, chosen as \(10^{-6}\) here. This step is a means of reducing the number of measurement circuits/quantum computational resources needed (which means less time to run which is preferred for a tutorial), and the threshold we use is fairly reasonable compared to the scale of errors induced by noisy quantum simulation.

qubit_hamiltonian = hamiltonian.qubit_encode()
qubit_hamiltonian.compress(abs_tol=1e-6)
qubit_hamiltonian.df()
Coefficient Term Coefficient Type
0 -37.822198 <class 'numpy.float64'>
1 -0.496463 Z5 <class 'numpy.float64'>
2 -0.496463 Z4 <class 'numpy.float64'>
3 0.128963 Z4 Z5 <class 'numpy.float64'>
4 -0.496463 Z3 <class 'numpy.float64'>
5 0.110247 Z3 Z5 <class 'numpy.float64'>
6 0.123222 Z3 Z4 <class 'numpy.float64'>
7 -0.012975 X2 X3 Y4 Y5 <class 'numpy.float64'>
8 0.012975 X2 Y3 Y4 X5 <class 'numpy.float64'>
9 0.012975 Y2 X3 X4 Y5 <class 'numpy.float64'>
10 -0.012975 Y2 Y3 X4 X5 <class 'numpy.float64'>
11 -0.496463 Z2 <class 'numpy.float64'>
12 0.123222 Z2 Z5 <class 'numpy.float64'>
13 0.110247 Z2 Z4 <class 'numpy.float64'>
14 0.128963 Z2 Z3 <class 'numpy.float64'>
15 -0.067741 Z1 <class 'numpy.float64'>
16 0.106870 Z1 Z5 <class 'numpy.float64'>
17 0.118715 Z1 Z4 <class 'numpy.float64'>
18 0.106870 Z1 Z3 <class 'numpy.float64'>
19 0.118715 Z1 Z2 <class 'numpy.float64'>
20 -0.011845 X0 X1 Y4 Y5 <class 'numpy.float64'>
21 -0.011845 X0 X1 Y2 Y3 <class 'numpy.float64'>
22 0.011845 X0 Y1 Y4 X5 <class 'numpy.float64'>
23 0.011845 X0 Y1 Y2 X3 <class 'numpy.float64'>
24 0.011845 Y0 X1 X4 Y5 <class 'numpy.float64'>
25 0.011845 Y0 X1 X2 Y3 <class 'numpy.float64'>
26 -0.011845 Y0 Y1 X4 X5 <class 'numpy.float64'>
27 -0.011845 Y0 Y1 X2 X3 <class 'numpy.float64'>
28 -0.067741 Z0 <class 'numpy.float64'>
29 0.118715 Z0 Z5 <class 'numpy.float64'>
30 0.106870 Z0 Z4 <class 'numpy.float64'>
31 0.118715 Z0 Z3 <class 'numpy.float64'>
32 0.106870 Z0 Z2 <class 'numpy.float64'>
33 0.123178 Z0 Z1 <class 'numpy.float64'>

To construct our ansatz for the specified fermion space and fermion state, we have employed the Chemically Aware Unitary Coupled Cluster method with singles and doubles excitations (UCCSD). The circuit is synthesized using Jordan-Wigner encoding.

from inquanto.ansatzes import FermionSpaceAnsatzChemicallyAwareUCCSD

ansatz = FermionSpaceAnsatzChemicallyAwareUCCSD(space, state)

Next, a noiseless VQE simulation is executed on the QulacsBackend. The goal is to achieve a minimized expectation value and a set of circuit parameters corresponding to the ground state. These optimized parameters are subsequently employed in the Hamiltonian averaging experiment later on.

from inquanto.express import run_vqe
from pytket.extensions.qulacs import QulacsBackend

state_backend = QulacsBackend()
vqe = run_vqe(ansatz, hamiltonian, backend=state_backend, with_gradient=False)

parameters = vqe.final_parameters
# TIMER BLOCK-0 BEGINS AT 2024-02-19 10:02:38.479944
# TIMER BLOCK-0 ENDS - DURATION (s):  0.8142243 [0:00:00.814224]

To simulate the specific noise profiles of machines, we can load and apply them to our simulations using the QuantinuumBackend, which retrieves information from your Quantinuum account. The QuantinuumBackend offers a range of available emulators, such as H1-1E and H1-2E. These emulators are designed for specific devices and they run remotely on a server.

Parameters used:

device_name – Name of device, e.g. “H1-1”

label – Job labels used if Circuits have no name, defaults to “job”

group – string identifier of a collection of jobs, can be used for usage tracking.

The pytket-quantinuum extension allows the user to access the following quantum devices, emulators and syntax checkers.

from pytket.extensions.quantinuum import QuantinuumBackend

backend = QuantinuumBackend(device_name="H1-1E", group ="Default - UK")

To reduce errors and inaccuracies caused by quantum noise and imperfections in the Quantinuum device, we can employ noise mitigation techniques. In this case, we will define the Qubit Operator symmetries within the system, enabling us to utilize PMSV (Partition Measurement Symmetry Verification). More information about QubitOperator can be found in the provided link.

from inquanto.operators import QubitOperator
from inquanto.protocols import PMSV

stabilizers = [QubitOperator("Z0 Z1 Z2 Z3 Z4 Z5", 1)]

mitms_pmsv = PMSV(stabilizers)

To expand the subspace for computing low-lying excited states, it is essential to define the basis. In this context, the function generate_subspace_singlet_singles is employed, which sequentially provides spin-adapted singlet-single excitation operators to preserve spin symmetry which are then mapped to qubits using the Jordan-Wigner method.

from inquanto.mappings import QubitMappingJordanWigner

mapping = QubitMappingJordanWigner()

fermionic_excitations_singlets = list(space.generate_subspace_singlet_singles())
qubit_excitations_singlets = mapping.operator_map(fermionic_excitations_singlets)

Now that the Hamiltonian, Ansatz and QSE basis operators are defined, we can build the computable. The computable is the chemical quantity that we are interested in. In this example, we are interested in building the QSE computable using the QSEMatricesComputable function.

Parameters used:

state – Ansatz used to represent the ground state, it is used as a reference state for the excitations to generate the subspace.

hermitian_operator – A hermitian operator, typically a hamiltonian, to be expanded in the subspace.

expansion_operators – A List of excitation operators spanning the subspace.

from inquanto.computables.composite import QSEMatricesComputable

qse = QSEMatricesComputable(
    state=ansatz,
    hermitian_operator=qubit_hamiltonian,
    expansion_operators=qubit_excitations_singlets,
)

To compute the expectation value of a Hermitian operator through operator averaging on the system register, we employ the PauliAveraging protocol. This protocol effectively implements the procedure outlined in Operator Averaging.

from inquanto.protocols import PauliAveraging
from pytket.partition import PauliPartitionStrat

protocol = PauliAveraging(
    backend=backend,
    shots_per_circuit=6000,
    pauli_partition_strategy=PauliPartitionStrat.CommutingSets,
)

protocol.build_from(parameters=parameters, computable=qse, noise_mitigation=mitms_pmsv)
<inquanto.protocols.averaging._pauli_averaging.PauliAveraging at 0x16ac06990>

Despite having many Qubit Operators to measure, this protocol ends up generating 30 measurement circuits due to the commutation relations measurement partitioning strategy pytket uses.

protocol.n_circuit
30

To launch the circuits to the backend we have used the launch function. This method processes all the circuits associated with the expectation value calculations and returns a list of ResultHandle objects representing the handles for the results. We can pickle these ResultHandle objects so we can retrieve the results once they are ready. After our experiments have finished, we can obtain the results by utilizing the retrieve function, which retrieves distributions from the backend for the specified source.

handles = protocol.launch()
import pickle

with open("handles.pickle", "wb") as handle:
    pickle.dump(handles, handle, protocol=pickle.HIGHEST_PROTOCOL)
completion_check=backend.circuit_status(handles[-1])[0].value #n-1 
print(completion_check)
Circuit is queued.
protocol.retrieve(handles)
<inquanto.protocols.averaging._pauli_averaging.PauliAveraging at 0x7fa080330990>

Once the results have been retrieved the Hamiltonian expectation value is evaluated as a classical post-processing step.

We now need to pass the output of the QSEMatricesComputable into pd_safe_eigh. This will solve the generalized eigenvalue equation and return the eigenvalues and eigenvectors.

H,S = qse.evaluate(protocol.get_evaluator())
from inquanto.core import pd_safe_eigh
  
e_vals_singlets, e_coeffs_singlets, _ = pd_safe_eigh(H,S)

print(e_vals_singlets)
print(e_coeffs_singlets)
[-39.729 -38.91  -38.871 -38.768 -38.273 -37.903]
[[-0.156+0.476j  0.004-0.008j  0.002-0.006j -0.018+0.023j  0.002-0.015j
  -0.   -0.002j]
 [-0.311-0.135j -0.24 +0.368j -0.567+0.08j   3.806+1.333j  0.888+0.622j
  -0.223-0.734j]
 [-0.388-0.069j -0.606-0.105j -0.781+0.008j  4.621+0.967j -1.663-0.86j
  -0.85 +0.603j]
 [-0.003-0.003j  0.576+0.164j -0.384+0.022j  0.06 +0.043j -0.061+0.026j
  -0.004-0.055j]
 [-0.077-0.165j -1.04 +0.671j -0.19 -0.518j  0.644+1.722j -1.634+2.515j
  -5.204+1.559j]
 [ 0.16 +0.204j  0.08 +1.131j  0.391+0.141j -2.197-2.655j  0.879+3.241j
  -2.243+2.55j ]
 [-0.006+0.003j  0.111+0.338j  0.363+0.46j   0.113+0.117j -0.12 -0.056j
  -0.007+0.04j ]
 [-0.098-0.106j -0.638+0.085j -0.395+0.104j  1.81 +1.216j -1.329+1.704j
   1.878-1.188j]
 [ 0.17 +0.018j  0.057+0.8j    0.203+0.411j -1.287-0.708j  1.636+4.126j
   3.24 -1.954j]]

We perform additional post-processing to report the operator expansion for each excited state.

from inquanto.operators import FermionOperator

e_vecs = []
for i in range(e_coeffs_singlets.shape[1]):
    operator = FermionOperator()
    for c, o in zip(e_coeffs_singlets[:, i], fermionic_excitations_singlets):
        operator += c * o
    operator.compress(abs_tol=1e-5)
    print(operator)
    e_vecs.append(operator)
(-0.15565783977198872+0.47621487975804033j, F0^ F0 ), (-0.15565783977198872+0.47621487975804033j, F1^ F1 ), (-0.3109942131939058-0.13521605338785778j, F0^ F2 ), (-0.3109942131939058-0.13521605338785778j, F1^ F3 ), (-0.38839307178519544-0.0693114600174391j, F0^ F4 ), (-0.38839307178519544-0.0693114600174391j, F1^ F5 ), (-0.003308852334800071-0.002955219215334559j, F2^ F0 ), (-0.003308852334800071-0.002955219215334559j, F3^ F1 ), (-0.07692730809458342-0.1649068223822534j, F2^ F2 ), (-0.07692730809458342-0.1649068223822534j, F3^ F3 ), (0.16028553390102782+0.2040034257419802j, F2^ F4 ), (0.16028553390102782+0.2040034257419802j, F3^ F5 ), (-0.006135282730581221+0.003137920667681536j, F4^ F0 ), (-0.006135282730581221+0.003137920667681536j, F5^ F1 ), (-0.09846811137359278-0.10620905658730515j, F4^ F2 ), (-0.09846811137359278-0.10620905658730515j, F5^ F3 ), (0.16993533787082993+0.01769895101511043j, F4^ F4 ), (0.16993533787082993+0.01769895101511043j, F5^ F5 )
(0.003529209585032674-0.00795687921061023j, F0^ F0 ), (0.003529209585032674-0.00795687921061023j, F1^ F1 ), (-0.24049475157327982+0.36783111046288186j, F0^ F2 ), (-0.24049475157327982+0.36783111046288186j, F1^ F3 ), (-0.6060191423196757-0.10482632989727117j, F0^ F4 ), (-0.6060191423196757-0.10482632989727117j, F1^ F5 ), (0.5763459636232429+0.16388842490954422j, F2^ F0 ), (0.5763459636232429+0.16388842490954422j, F3^ F1 ), (-1.0398193192980176+0.6711364842936922j, F2^ F2 ), (-1.0398193192980176+0.6711364842936922j, F3^ F3 ), (0.0799938237057527+1.1312076955513257j, F2^ F4 ), (0.0799938237057527+1.1312076955513257j, F3^ F5 ), (0.11138656902183633+0.3380893725102996j, F4^ F0 ), (0.11138656902183633+0.3380893725102996j, F5^ F1 ), (-0.6378073443599778+0.08473450806257327j, F4^ F2 ), (-0.6378073443599778+0.08473450806257327j, F5^ F3 ), (0.057037317243711416+0.7995538108062838j, F4^ F4 ), (0.057037317243711416+0.7995538108062838j, F5^ F5 )
(0.002165953897558483-0.005944635760931628j, F0^ F0 ), (0.002165953897558483-0.005944635760931628j, F1^ F1 ), (-0.5667818952627056+0.08033281708487372j, F0^ F2 ), (-0.5667818952627056+0.08033281708487372j, F1^ F3 ), (-0.7806938566797863+0.008375225559272712j, F0^ F4 ), (-0.7806938566797863+0.008375225559272712j, F1^ F5 ), (-0.38358059039903575+0.02164733189057025j, F2^ F0 ), (-0.38358059039903575+0.02164733189057025j, F3^ F1 ), (-0.18951924122622935-0.5175643220754953j, F2^ F2 ), (-0.18951924122622935-0.5175643220754953j, F3^ F3 ), (0.39067878123749633+0.14105366840747063j, F2^ F4 ), (0.39067878123749633+0.14105366840747063j, F3^ F5 ), (0.3633037576320181+0.45991596424530234j, F4^ F0 ), (0.3633037576320181+0.45991596424530234j, F5^ F1 ), (-0.3948958613818514+0.10360791672209882j, F4^ F2 ), (-0.3948958613818514+0.10360791672209882j, F5^ F3 ), (0.2025074399411661+0.41092059234737977j, F4^ F4 ), (0.2025074399411661+0.41092059234737977j, F5^ F5 )
(-0.017840047182897188+0.022744956740089296j, F0^ F0 ), (-0.017840047182897188+0.022744956740089296j, F1^ F1 ), (3.806126291783216+1.332658212611577j, F0^ F2 ), (3.806126291783216+1.332658212611577j, F1^ F3 ), (4.621131902658994+0.9671995436795001j, F0^ F4 ), (4.621131902658994+0.9671995436795001j, F1^ F5 ), (0.05993966785070938+0.04310122860320857j, F2^ F0 ), (0.05993966785070938+0.04310122860320857j, F3^ F1 ), (0.6441688080331356+1.7220627585285808j, F2^ F2 ), (0.6441688080331356+1.7220627585285808j, F3^ F3 ), (-2.1965755386805927-2.655483790470051j, F2^ F4 ), (-2.1965755386805927-2.655483790470051j, F3^ F5 ), (0.1127899167896759+0.11659167840761259j, F4^ F0 ), (0.1127899167896759+0.11659167840761259j, F5^ F1 ), (1.809508151433246+1.2160491220244634j, F4^ F2 ), (1.809508151433246+1.2160491220244634j, F5^ F3 ), (-1.2874605040396814-0.7077883650219732j, F4^ F4 ), (-1.2874605040396814-0.7077883650219732j, F5^ F5 )
(0.0021294584990190677-0.01537836611697584j, F0^ F0 ), (0.0021294584990190677-0.01537836611697584j, F1^ F1 ), (0.8881021827827966+0.6217291428756166j, F0^ F2 ), (0.8881021827827966+0.6217291428756166j, F1^ F3 ), (-1.6634091829941275-0.85957877147374j, F0^ F4 ), (-1.6634091829941275-0.85957877147374j, F1^ F5 ), (-0.06072464916897717+0.025946744324903372j, F2^ F0 ), (-0.06072464916897717+0.025946744324903372j, F3^ F1 ), (-1.6336871970371127+2.515156393240569j, F2^ F2 ), (-1.6336871970371127+2.515156393240569j, F3^ F3 ), (0.8787624912303986+3.2406507977554675j, F2^ F4 ), (0.8787624912303986+3.2406507977554675j, F3^ F5 ), (-0.12020930798769339-0.055773317529686006j, F4^ F0 ), (-0.12020930798769339-0.055773317529686006j, F5^ F1 ), (-1.3286485705307103+1.7035098710284213j, F4^ F2 ), (-1.3286485705307103+1.7035098710284213j, F5^ F3 ), (1.6355067063551598+4.126046236307394j, F4^ F4 ), (1.6355067063551598+4.126046236307394j, F5^ F5 )
(-0.00020756753326981142-0.0016959811491862975j, F0^ F0 ), (-0.00020756753326981142-0.0016959811491862975j, F1^ F1 ), (-0.22283094986115085-0.7342082125387235j, F0^ F2 ), (-0.22283094986115085-0.7342082125387235j, F1^ F3 ), (-0.8502303263381792+0.6025252787670932j, F0^ F4 ), (-0.8502303263381792+0.6025252787670932j, F1^ F5 ), (-0.004028844274401662-0.054943191515560284j, F2^ F0 ), (-0.004028844274401662-0.054943191515560284j, F3^ F1 ), (-5.203599573643404+1.5592992892443864j, F2^ F2 ), (-5.203599573643404+1.5592992892443864j, F3^ F3 ), (-2.2427076382898403+2.550269786999596j, F2^ F4 ), (-2.2427076382898403+2.550269786999596j, F3^ F5 ), (-0.006964622239363893+0.040135082617393666j, F4^ F0 ), (-0.006964622239363893+0.040135082617393666j, F5^ F1 ), (1.878027835548134-1.18788450746501j, F4^ F2 ), (1.878027835548134-1.18788450746501j, F5^ F3 ), (3.2403778890630868-1.953712459189611j, F4^ F4 ), (3.2403778890630868-1.953712459189611j, F5^ F5 )

Alternatively, the Quantum Subspace Expansion algorithm implemented in InQuanto, known as AlgorithmQSE, can be employed. This is a black-box method enabling direct retrieval of eigenvalues and eigenvectors, stored as final_value and final_states, respectively. The algorithm addresses numerical instabilities during matrix diagonalization by eliminating near-linear dependencies in the basis. However, it’s worth noting that the user has less control over the specific details of the process. Some examples that cannot be executed include launching, retrieving, and performing PMSV.

from inquanto.algorithms import AlgorithmQSE

algorithm = AlgorithmQSE(
    computable_qse_matrices=qse,
    parameters=parameters,
)

protocol_expression = PauliAveraging(
    backend=backend,
    shots_per_circuit=1000,
    pauli_partition_strategy=PauliPartitionStrat.CommutingSets,
)
    
algorithm.build(protocol=protocol_expression)
    
#algorithm.run() 

#print(algorithm.generate_report())
<inquanto.algorithms.qse._algorithm_qse.AlgorithmQSE at 0x7fa0534b7850>