Multi-Reference Quantum Krylov Algorithm (H2 Example)

[ ]:
# Package installs
!pip install pyscf==2.6.2
!pip install openfermionpyscf==0.5
!pip install openfermion==1.6.1
[2]:
import cudaq
import numpy as np
import scipy

# Single-node, single gpu
cudaq.set_target("nvidia")
multi_gpu = False

# Single-node, multi-GPU
#cudaq.set_target("nvidia", option='mqpu,fp64')
#multi_gpu = True

[3]:
# Define H2 molecule
geometry = [('H', (0.0, 0.0, 0.0)), ('H', (0.0, 0.0, 0.7474))]

hamiltonian, data = cudaq.chemistry.create_molecular_hamiltonian(
    geometry, 'sto-3g', 1, 0)

electron_count = data.n_electrons
qubits_num = 2 * data.n_orbitals

spin_ham_matrix = hamiltonian.to_matrix()
e, c = np.linalg.eig(spin_ham_matrix)

# Find the ground state energy and the corresponding eigenvector
print('Ground state energy (classical simulation)= ', np.min(e), ', index= ',
      np.argmin(e))
min_indices = np.argmin(e)

# Eigen vector can be used to initialize the qubits
vec = c[:, min_indices]
Ground state energy (classical simulation)=  (-1.1371757102406843+0j) , index=  3
[4]:
# Collect coefficients from a spin operator so we can pass them to a kernel
def termCoefficients(ham: cudaq.SpinOperator) -> list[complex]:
    result = []
    ham.for_each_term(lambda term: result.append(term.get_coefficient()))
    return result


# Collect Pauli words from a spin operator so we can pass them to a kernel
def termWords(ham: cudaq.SpinOperator) -> list[str]:
    result = []
    ham.for_each_term(lambda term: result.append(term.to_string(False)))
    return result


coefficient = termCoefficients(hamiltonian)
pauli_string = termWords(hamiltonian)

[5]:
@cudaq.kernel
def U_psi(qubits: cudaq.qview, dt: float, coefficients: list[complex],
          words: list[cudaq.pauli_word]):
    # Compute U_m = exp(-i m dt H)
    for i in range(len(coefficients)):
        exp_pauli(dt * coefficients[i].real, qubits, words[i])


@cudaq.kernel
def U_phi(qubits: cudaq.qview, dt: float, coefficients: list[complex],
          words: list[cudaq.pauli_word]):
    # Compute U_n = exp(-i n dt H)
    for i in range(len(coefficients)):
        exp_pauli(dt * coefficients[i].real, qubits, words[i])


@cudaq.kernel
def apply_pauli(qubits: cudaq.qview, word: list[int]):

    # Add H (Hamiltonian operator)
    for i in range(len(word)):
        if word[i] == 1:
            x(qubits[i])
        if word[i] == 2:
            y(qubits[i])
        if word[i] == 3:
            z(qubits[i])


@cudaq.kernel
def qfd_kernel(dt_alpha: float, dt_beta: float, coefficients: list[complex],
               words: list[cudaq.pauli_word], word_list: list[int],
               vec: list[complex]):

    ancilla = cudaq.qubit()
    qreg = cudaq.qvector(vec)

    h(ancilla)

    x(ancilla)
    cudaq.control(U_psi, ancilla, qreg, dt_alpha, coefficients, words)
    x(ancilla)

    cudaq.control(apply_pauli, ancilla, qreg, word_list)
    cudaq.control(U_phi, ancilla, qreg, dt_beta, coefficients, words)
[6]:
def pauli_str(pauli_word, qubits_num):

    my_list = []
    for i in range(qubits_num):
        if str(pauli_word[i]) == 'I':
            my_list.append(0)
        if str(pauli_word[i]) == 'X':
            my_list.append(1)
        if str(pauli_word[i]) == 'Y':
            my_list.append(2)
        if str(pauli_word[i]) == 'Z':
            my_list.append(3)
    return my_list
[7]:
# Define the spin-op x for real component and y for the imaginary component.

x_0 = cudaq.spin.x(0)
y_0 = cudaq.spin.y(0)
[8]:
#Define parameters for the quantum Krylov space

dt = 0.5

# Dimension of the Krylov space
m_qfd = 4
[9]:
# Compute the basis overlap matrix


## Single GPU:
if not multi_gpu:

    # Add identity operator to compute overlap of basis

    observe_op = 1.0
    for m in range(qubits_num):
        observe_op *= cudaq.spin.i(m)

    identity_word = observe_op.to_string(False)

    pauli_list = pauli_str(identity_word, qubits_num)
    #print(pauli_list)

    wf_overlap = np.zeros((m_qfd, m_qfd), dtype=complex)

    for m in range(m_qfd):
        dt_m = dt * m
        for n in range(m, m_qfd):
            dt_n = dt * n
            results = cudaq.observe(qfd_kernel, [x_0, y_0], dt_m, dt_n,
                                    coefficient, pauli_string, pauli_list, vec)
            temp = [result.expectation() for result in results]
            wf_overlap[m, n] = temp[0] + temp[1] * 1j
            if n != m:
                wf_overlap[n, m] = np.conj(wf_overlap[m, n])

else:

    ## Multi-GPU

    # Compute the basis overlap matrix

    # Add identity operator to compute overlap of basis

    observe_op = 1.0
    for m in range(qubits_num):
        observe_op *= cudaq.spin.i(m)

    identity_word = observe_op.to_string(False)

    pauli_list = pauli_str(identity_word, qubits_num)
    #print(pauli_list)

    wf_overlap = np.zeros((m_qfd, m_qfd), dtype=complex)

    collect_overlap_real = []
    collect_overlap_img = []

    count=0
    for m in range(m_qfd):
        dt_m = dt * m
        for n in range(m, m_qfd):
            dt_n = dt * n

            count_id=count%2
            #print(count_id)
            collect_overlap_real.append(cudaq.observe_async(qfd_kernel, x_0, dt_m, dt_n,
                                    coefficient, pauli_string, pauli_list, vec, qpu_id=count_id))

            collect_overlap_img.append(cudaq.observe_async(qfd_kernel, y_0, dt_m, dt_n,
                                    coefficient, pauli_string, pauli_list, vec, qpu_id=count_id+2))
            count += 1

    tot_dim = 0

    for n in range(m_qfd):
        for m in range(n,m_qfd):
            observe_result = collect_overlap_real[tot_dim].get()
            real_val = observe_result.expectation()

            observe_result=collect_overlap_img[tot_dim].get()
            img_val=observe_result.expectation()

            wf_overlap[m, n] = real_val + img_val * 1j
            if n != m:
                wf_overlap[n, m] = np.conj(wf_overlap[m, n])

            tot_dim += 1
[10]:
# Compute the matrix Hamiltonian


## Single GPU:
if not multi_gpu:
    ham_matrx = np.zeros((m_qfd, m_qfd), dtype=complex)

    for m in range(m_qfd):
        dt_m = dt * m
        for n in range(m, m_qfd):
            dt_n = dt * n

            tot_e = np.zeros(2)
            for coef, word in zip(coefficient, pauli_string):
                #print(coef,word)

                pauli_list = pauli_str(word, qubits_num)
                #print(pauli_list)

                results = cudaq.observe(qfd_kernel, [x_0, y_0], dt_m, dt_n,
                                        coefficient, pauli_string, pauli_list, vec)

                temp = [result.expectation() for result in results]
                #print(temp)
                temp[0] = coef.real * temp[0]
                temp[1] = coef.imag * temp[1]

                tot_e[0] += temp[0]
                tot_e[1] += temp[1]

            ham_matrx[m, n] = tot_e[0] + tot_e[1] * 1j
            if n != m:
                ham_matrx[n, m] = np.conj(ham_matrx[m, n])
else:
    ## Multi-GPU

    ham_matrx = np.zeros((m_qfd, m_qfd), dtype=complex)


    for m in range(m_qfd):
        dt_m = dt * m
        for n in range(m, m_qfd):
            dt_n = dt * n

            ham_matrix_real = []
            ham_matrix_imag = []

            count=0
            tot_e = np.zeros(2)
            for coef, word in zip(coefficient, pauli_string):
                count_id=count%2
                #print(coef,word)

                pauli_list = pauli_str(word, qubits_num)
                #print(pauli_list)

                ham_matrix_real.append(cudaq.observe_async(qfd_kernel, x_0, dt_m, dt_n,
                                        coefficient, pauli_string, pauli_list, vec, qpu_id=count_id))
                ham_matrix_imag.append(cudaq.observe_async(qfd_kernel, y_0, dt_m, dt_n,
                                        coefficient, pauli_string, pauli_list, vec, qpu_id=count_id+2))

                count += 1

            i = 0
            for coef in coefficient:

                observe_result = ham_matrix_real[i].get()
                real_val = observe_result.expectation()

                observe_result=ham_matrix_imag[i].get()
                img_val=observe_result.expectation()

                tot_e[0] += real_val * coef.real
                tot_e[1] += img_val * coef.imag

                i+=1

            ham_matrx[m, n] = tot_e[0] + tot_e[1] * 1j
            if n != m:
                ham_matrx[n, m] = np.conj(ham_matrx[m, n])
[11]:
# Diagonalize the matrix


def eig(H, s):
    #Solver for generalized eigenvalue problem

    # HC = SCE

    THRESHOLD = 1e-20
    s_diag, u = scipy.linalg.eig(s)
    s_prime = []
    for sii in s_diag:
        if np.imag(sii) > 1e-7:
            raise ValueError(
                "S may not be hermitian, large imag. eval component.")
        if np.real(sii) > THRESHOLD:
            s_prime.append(np.real(sii))

    X_prime = np.zeros((len(s_diag), len(s_prime)), dtype=complex)

    for i in range(len(s_diag)):
        for j in range(len(s_prime)):
            X_prime[i][j] = u[i][j] / np.sqrt(s_prime[j])

    H_prime = (((X_prime.conjugate()).transpose()).dot(H)).dot(X_prime)
    e_prime, C_prime = scipy.linalg.eig(H_prime)
    C = X_prime.dot(C_prime)

    return e_prime, C
[12]:
eigen_value, eigen_vect = eig(ham_matrx[0:m_qfd, 0:m_qfd], wf_overlap[0:m_qfd,
                                                                      0:m_qfd])
print('Energy from QFD:')
print(np.min(eigen_value))
Energy from QFD:
(-1.1367554712424826+3.373828079812224e-20j)