# Installation of tangelo if not already installed.
try:
import tangelo
except ModuleNotFoundError:
!pip install git+https://github.com/goodchemistryco/Tangelo.git@develop --quiet
State preparation using quantum signal processing
We are going to apply a delta function to the Hamiltonian (\(H\)), such that all states with eigenvalues outside a specific energy band (\(\lambda \pm \Delta/2\)) are supressed. This is performed by applying the operation
\(f(H,\Delta,k)=\sum_{n=0}^{k}\frac{T_n(-1+2\frac{H^2-\Delta^2}{1-\Delta^2})}{T_n(-1+2\frac{-\Delta^2}{1-\Delta^2})}\)
using quantum signal processing, where \(T_n\) refers to the Chebyshev polynomial of the first kind, defined by \(T_n(cos(\theta)) = cos(n\theta)\).
As an example, we will use the polynomial with approximation order \(k=20\) for \(\Delta = 0.1\) which is depicted below
from scipy.special import eval_chebyt
import numpy as np
import matplotlib.pyplot as plt
= 20
k = 0.1
delta
def filter_func(x, delta, k):
= 0
val for i in range(k):
= -1 + 2*((x**2-delta**2)/(1-delta**2))
arg = eval_chebyt(k, arg)
topk = -1 + 2*(-delta**2/(1-delta**2))
arg = eval_chebyt(k, arg)
botk += topk/botk
val return val
# Plot the polynomial
= np.linspace(-1, 1, 100)
xs = np.zeros(len(xs))
ys
for i, x in enumerate(xs):
= filter_func(x, delta, k)
ys[i]
plt.plot(xs, ys)
This polynomial needs to be converted to phases to use Quantum Signal Processing. For convenience, here are the phases for \(k=20\) and \(\Delta = 0.1\). At the end of the notebook, we show two ways to calculate the phases for a given polynomial.
# phases below are calculated using QSPPACK for n=20, delta = 0.1
= [ 0.76974538, 0.00646461, -0.00773143, 0.00908568, -0.01051591, 0.01200849,
phases -0.01354772, 0.01511600, -0.01669402, 0.01826109, -0.01979540, 0.02127448,
-0.02267559, 0.02397626, -0.02515479, 0.02619086, -0.02706603, 0.02776435,
-0.02827282, 0.02858185, -0.02868552, 0.02858185, -0.02827282, 0.02776435,
-0.02706603, 0.02619086, -0.02515479, 0.02397626, -0.02267559, 0.02127448,
-0.01979540, 0.01826109, -0.01669402, 0.01511600, -0.01354772, 0.01200849,
-0.01051591, 0.00908568, -0.00773143, 0.00646461, 0.76974538]
Hydrogen molecule example
We are going to prepare two different states using the phases above, as well as the Quantum Signal Processing circuit get_qsp_circuit_no_anc
as defined in arXiv:2002.11649.
We are going to apply it to \(H_2\) in a STO-3G basis with the “scbk” qubit mapping.
from openfermion import get_sparse_operator
import numpy as np
from tangelo.molecule_library import mol_H2_sto3g
from tangelo.toolboxes.qubit_mappings.mapping_transform import fermion_to_qubit_mapping
from tangelo.linq.helpers.circuits.statevector import StateVector
= mol_H2_sto3g.fermionic_hamiltonian
fe_op = fermion_to_qubit_mapping(fe_op, "scbk", n_spinorbitals=4, n_electrons=2)
qu_op
= get_sparse_operator(qu_op).toarray()
ham = np.linalg.eigh(ham)
eigs, vecs = eigs[3]-eigs[0]
alpha
= np.random.random(4)
init_state /= np.linalg.norm(init_state)
init_state
= StateVector(init_state)
sv = sv.initializing_circuit() init_circ
First excited state
We need to shift the Hamiltonian such that the desired eigenvalue (\(\lambda\)) is centered at zero and the spectral range is in [-1, 1]. This is done by defining
\(\tilde{H} = \frac{H-\lambda}{\alpha + \left|\lambda\right|}\)
where \(\alpha\) is the approximation to the spectral range, defined in the previous cell.
from tangelo.linq import Circuit, Gate, get_backend
from tangelo.toolboxes.circuits.qsp import get_qsp_circuit_no_anc
from tangelo.toolboxes.circuits.lcu import get_uprep_uselect, get_lcu_qubit_op_info
= eigs[1]
lamb # Shift Hamiltonian so that spectrum is in [-1, 1] and desired lambda is at 0
= (qu_op - lamb)/(alpha+abs(lamb))
qu_op_tilde
# Generate LCU circuit that applies the Hamiltonian
= get_uprep_uselect(qu_op_tilde)
uprep, uselect, qu_op_qs, m_qs, alpha = uprep + uselect + uprep.inverse()
cua
# Ancilla qubits
= len(m_qs) + 1
n_ancilla
# Total number of qubits needed to apply algorithm
= len(qu_op_qs) + n_ancilla
n_qubits
# Feed in LCU circuit to generate QSP circuit with phases for eigenvalue filtering
= get_qsp_circuit_no_anc(cua, m_qs, phases)
eig_filt_circ
# Add the mesurement gates to the ancilla qubits for the application of the Linear Combination
# of Unitaries plus the extra qubit for the QSP
# Measure gates for LCU ancilla qubits
= init_circ + eig_filt_circ + Circuit([Gate("MEASURE", m) for m in m_qs])
full_circuit # Measure gate for the QSP qubit where the phases are applied
+= Circuit([Gate("MEASURE", m_qs[-1]+1)]) full_circuit
= get_backend()
sim
= sim.simulate(full_circuit, desired_meas_result="0"*n_ancilla, return_statevector=True)
f, sv # Reorder statevector to "msq_first" if statevector in backend is "lsq_first"
if sim.backend_info()["statevector_order"] == "lsq_first":
= np.reshape(sv, [2]*n_qubits).T.flatten()
sv # Pick out part of wavefunction that corresponds to "0000" on ancilla qubits
= np.reshape(sv, (2**4, 4))[0, :]
sv # Reorder statevector to correspond to openfermion ordering
= np.reshape(sv, (2,2)).T.flatten()
sv
print(f'initial overlap = {abs(np.dot(vecs[:,1], init_state))}')
print(f'final overlap = {abs(np.dot(vecs[:,1], sv))}')
initial overlap = 0.18367656363390905
final overlap = 0.9887577317376988
Second excited state
We can then reapply the same approach to the second excited state.
= eigs[2]
lamb # Shift Hamiltonian so that spectrum is in [-1, 1] and desired lambda is at 0
= (qu_op - lamb)/(alpha+abs(lamb))
qu_op_tilde
# Generate LCU circuit that applies the Hamiltonian
= get_uprep_uselect(qu_op_tilde)
uprep, uselect, qu_op_qs, m_qs, alpha = uprep + uselect + uprep.inverse()
cua
# Ancilla qubits
= len(m_qs) + 1
n_ancilla
# Total number of qubits needed to apply algorithm
= len(qu_op_qs) + n_ancilla
n_qubits
# Feed in LCU circuit to generate QSP circuit with phases for eigenvalue filtering
= get_qsp_circuit_no_anc(cua, m_qs, phases)
eig_filt_circ
= init_circ + eig_filt_circ + Circuit([Gate("MEASURE", m) for m in m_qs])
full_circuit += Circuit([Gate("MEASURE", m_qs[-1]+1)]) full_circuit
= get_backend()
sim
= sim.simulate(full_circuit, desired_meas_result="0"*n_ancilla, return_statevector=True)
f, sv # Reorder statevector to "msq_first" if statevector in backend is "lsq_first"
if sim.backend_info()["statevector_order"] == "lsq_first":
= np.reshape(sv, [2]*n_qubits).T.flatten()
sv # Pick out part of wavefunction that corresponds to "0000" on ancilla qubits
= np.reshape(sv, (2**4, 4))[0, :]
sv # Reorder statevector to correspond to openfermion ordering
= np.reshape(sv, (2,2)).T.flatten() sv
print(f'initial overlap = {abs(np.dot(vecs[:,2], init_state))}')
print(f'final overlap = {abs(np.dot(vecs[:,2], sv))}')
initial overlap = 0.18413991077227115
final overlap = 0.9969619094421538
Code to generate phases
This section shows 2 options for converting a polynomial into phases, with either QSPPACK or pyqsp.
The cells are formatted in markdown as additional dependencies are required to run the following phase calculations, but the code below can be run once they are installed.
Using QSPPACK
from sympy.abc import x
from sympy import Rational
from sympy.functions.special.polynomials import chebyshevt
= 20
k # Necessary to work in exact representation for higher order polynomials
= Rational(delta)
delta
def filter_func(x, delta, k):
= 0
val for i in range(k):
= -1 + 2*((x**2-delta**2)/(1-delta**2))
arg = chebyshevt(k, arg)
topk = -1 + 2*(-delta**2/(1-delta**2))
arg = chebyshevt(k, arg)
botk += topk/botk
val return val
We need to convert the filter_func
polynomial to coefficients of expansion in Chebyshev polynomials to use with QSPPACK
# Use chebyshev interpolation ak=2/(n+1)*sum_{j=0}^n p(x_j)T_k(x_j), x_j = cos(\frac{2*j+1}{2*n+2}\pi)
= filter_func(x, delta, k).as_poly()
poly
= 2*k
n = np.zeros(n+1)
xjs = np.zeros(n+1)
aks for j in range(n+1):
= np.cos((2*j+1)/(2*n+2)*np.pi)
xjs[j]
for ki in range(0, n+1, 2):
= 0
aks[ki] for j in range(n+1):
+= 2/(n+1)*poly.eval(xjs[j])*eval_chebyt(ki, xjs[j])
aks[ki]
/= k*np.sqrt(2)
aks 0] /=2
aks[= aks[0: n+1: 2] aks
We can then obtain the phases using oct2py
(with octave installed) and the folder location of QSPPACK
cloned from https://github.com/qsppack/QSPPACK.
from oct2py import octave
= 0.01
eps = 'path to QSPPACK'
folder
octave.addpath(folder)= octave.struct("criteria", eps)
opts = octave.QSP_solver(aks, 0, opts, nout=2) phases, _
Using pyqsp
For \(k=20\), pyqsp with the laurent
method works properly. However, it often fails for higher order polynomials. In that case, you can install tensorflow and use method=tf
. It however seemed much slower than QSPPACK
.
import pyqsp
from pyqsp import angle_sequence
from pyqsp.angle_sequence import AngleFindingError
from sympy.polys.polytools import primitive
from pyqsp.completion import CompletionError
# Compute phases for real part Cos(Ht) of Exp(iHt)
= pyqsp.poly.PolyCosineTX()
pg = primitive(filter_func(x, delta, k))
prefac, poly = poly.as_poly()
poly = poly.as_dict()
polydict
= np.zeros(2*k + 1, dtype=float)
coefs for term, coeff in polydict.items():
0]] = prefac*coeff/k/np.sqrt(2)
coefs[term[
= 100
n_attempts = 'laurent'
method =0.01
eps
for i in range(n_attempts):
try:
= angle_sequence.QuantumSignalProcessingPhases(
phases =eps, suc=1-eps/10, method=method, tolerance=0.01)
coefs, epsexcept (AngleFindingError, CompletionError):
if i == n_attempts-1:
raise RuntimeError("Real phases calculation failed, increase n_attempts or eps")
else:
print(f"Attempt {i+2} for the real coefficients")
else:
break