Statistical Phase Estimation
In this tutorial, we will go over how to execute statistical phase estimation (SPE) using QURI Algo and QURI VM.
Overview
SPE is an algorithm that is used to estimate the eigenenergies of a problem Hamiltonian. It does so by performing a Hadamard test on a trial wave-function, resulting in a measurable signal that can be used to infer the wave-functions spectral density. If this wave-function has a significant overlap with the true ground state of the system, the ground state energy should make a significant contribution to the spectral density. The following propoerties makes SPE an attractive algorithm in EFTQC
- Good circuit depth scaling compared to FTQC algorithms
- The run-time scales with the energy accuracy as
- Noise resilience
In this notebook we will introduce all of the pieces needed to run SPE. In order, this notebook will
- Explain the LT22 and Gaussian filter variants of SPE and showcase their implementations in QURI Algo
- Instruct in the use of QURI Parts' noise models and showcase the robustness of SPE to noise based on the STAR architecture
- Estimate the Fidelity of the Hadamard test circuits used in SPE after transpilation to the STAR architecture and introduce QURI VM.
Prerequisites
We do not go into great detail about the following topics, but they will be part of this notebook.
Set up the system
Before running SPE we briefly introduce some of the building blocks we need. We will here show how to
- Define a molecule
- Set up programming abstractions needed for SPE using QURI Algo
- Verify that they behave as expected by conducting the Hadamard test
We first set up the problem using PySCF and quri-parts. PySCF is used to define the molecular geometry and then give us the spin-restricted Hartree-Fock ground state solution to the molecular Hamiltonian. Then the second quantized Hamiltonian is generated by calculating the electron integrals, which is then converted to a qubit operator. For this we use the quri_parts.openfermion and quri_parts.pyscf modules.
from pyscf import gto, scf
from quri_parts.pyscf.mol import get_spin_mo_integrals_from_mole
from quri_parts.openfermion.mol import get_qubit_mapped_hamiltonian
from quri_algo.problem import QubitHamiltonian
# Prepare Hamiltonian
mole = gto.M(atom="H 0 0 0; H 0 0 1")
mf = scf.RHF(mole).run(verbose=0)
hamiltonian, mapping = get_qubit_mapped_hamiltonian(
*get_spin_mo_integrals_from_mole(mole, mf.mo_coeff)
)
Before moving on, we compute some exact system characteristics, e.g. the exact ground state energy and the exact energy gap between the ground state and the first excited state. To this end, we first convert the Hamiltonian to a sparse matrix that we can diagonalize using scipy's sparse linear algebra library.
import numpy as np
from quri_parts.core.operator import get_sparse_matrix
vals, vecs = np.linalg.eigh(get_sparse_matrix(hamiltonian).toarray())
EXACT_GS_ENERGY = np.min(vals)
EXACT_GAP = np.abs(vals[np.argsort(vals)][:2] @ [1, -1])
print("E_0:", EXACT_GS_ENERGY)
print("Delta:", EXACT_GAP)
E_0: -1.1011503302326187
Delta: 0.3552785371970515
To perform further computations, we need to encode our problem into some circuit that represents some operator function of our problem. For example in SPE, to estimate the ground state of a Hamiltonian, we need to encode our Hamiltonian into a controlled time evolution circuit. Here, we choose to do the time evolution with Trotterization. To do this, we wrap the hamiltonian into QubitHamiltonian for encoding into a circuit later.
QubitHamiltonian can be seen as a generalization of the Operator class from QURI Parts, containing both the terms of a qubit operator and the number of qubits. This makes it convenient when contructing a class hierarchy with quantum circuit factories intended to represent time-evolution and other problem defined quantum circuit constructs.
hamiltonian_input = QubitHamiltonian(mapping.n_qubits, hamiltonian)
Circuit factories
As a direct example of the aforementioned circuit factories we consider the TrotterControlledTimeEvolutionCircuitFactory. As the name suggests this is a circuit factory that instance time-evolution circuits based on the Trotter-Suzuki decomposition. It needs a QubitHamiltonian which serves as the generator of time evolution, as well as the number of Trotter steps. Details of this factory are explained in circuit factories.
from quri_parts.circuit.utils.circuit_drawer import draw_circuit
from quri_algo.circuit.time_evolution.trotter_time_evo import (
TrotterControlledTimeEvolutionCircuitFactory,
)
trotter_concotrolled_time_evo_circuit_factory = (
TrotterControlledTimeEvolutionCircuitFactory(hamiltonian_input, n_trotter=30)
)
# Time-evolution with t=1
c_time_evo = trotter_concotrolled_time_evo_circuit_factory(evolution_time=1)
draw_circuit(c_time_evo, line_length=10000)