"""
\********************************************************************************
* Copyright (c) 2023 the Qrisp authors
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0.
*
* This Source Code may also be made available under the following Secondary
* Licenses when the conditions for such availability set forth in the Eclipse
* Public License, v. 2.0 are satisfied: GNU General Public License, version 2
* with the GNU Classpath Exception which is
* available at https://www.gnu.org/software/classpath/license.html.
*
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
********************************************************************************/
"""
import numpy as np
from qrisp import h, x, cx, ry, control, conjugate
from qrisp.operators.qubit import QubitOperator, QubitTerm
from qrisp.operators.fermionic import *
from functools import cache
import itertools
import math
#
# helper functions
#
def verify_symmetries(two_int):
"""
Checks the symmetries of the two_electron integrals tensor in physicist's notation.
Parameters
----------
int_two : numpy.ndarray
The two-electron integrals w.r.t. spin orbitals in physicist's notation.
Returns
-------
"""
M = two_int.shape[0]
for i in range(M):
for j in range(M):
for k in range(M):
for l in range(M):
test1 = abs(two_int[i][j][k][l]-two_int[j][i][l][k])
test2 = abs(two_int[i][j][k][l]-two_int[k][l][i][j])
test3 = abs(two_int[i][j][k][l]-two_int[l][k][j][i])
if test1>1e-9 or test2>1e-9 or test3>1e-9:
return False
return True
def delta(i,j):
if i==j:
return 1
else:
return 0
#
# spacial orbitals to spin orbitals
#
def omega(x):
return x%2
def spacial_to_spin(one_int,two_int):
r"""
Transforms one- and two-electron integrals w.r.t. $M$ spacial orbitals $\psi_0,\dotsc,\psi_{M-1}$ to
one- and two-electron integrals w.r.t. $2M$ spin orbitals $\chi_{2i}=\psi_{i}\alpha$,
$\chi_{2i+1}=\psi_{i}\beta$ for $i=0,\dotsc,M-1$.
That is, given one- and two-electron integrals for spacial orbitals:
.. math::
h_{ij} &= \braket{\psi_i|h|\psi_j} \\
h_{ijkl} &= \braket{\psi_i\psi_j|\psi_k\psi_l}
this methods computes one- and two-electron integrals for spin orbitals:
.. math::
\tilde h_{ijkl} &= \braket{\chi_i|h|\chi_j} \\
\tilde h_{ijkl} &= \braket{\chi_i\chi_j|\chi_k\chi_l}
Parameters
----------
one_int : numpy.ndarray
The one-electron integrals w.r.t. spacial orbitals.
two_int : numpy.ndarray
The two-electron integrals w.r.t. spacial orbitals.
Returns
-------
one_int_spin : numpy.ndarray
The one-electron integrals w.r.t. spin orbitals.
two_int_spin : numpy.ndarray
The two-electron integrals w.r.t. spin orbitals.
"""
num_spacial_orbs = one_int.shape[0]
num_spin_orbs = 2*num_spacial_orbs
# Initialize the spin-orbital one-electron integral tensor
one_int_spin = np.zeros((num_spin_orbs, num_spin_orbs))
for i in range(num_spacial_orbs):
for j in range(num_spacial_orbs):
one_int_spin[2*i][2*j] = one_int[i][j]
one_int_spin[2*i+1][2*j+1] = one_int[i][j]
# Initialize the spin-orbital two-electron integral tensor
two_int_spin = np.zeros((num_spin_orbs, num_spin_orbs, num_spin_orbs, num_spin_orbs))
for i in range(num_spacial_orbs):
for j in range(num_spacial_orbs):
for k in range(num_spacial_orbs):
for l in range (num_spacial_orbs):
two_int_spin[2*i][2*j+1][2*k+1][2*l] = two_int[i][j][k][l]
two_int_spin[2*i+1][2*j][2*k][2*l+1] = two_int[i][j][k][l]
two_int_spin[2*i][2*j][2*k][2*l] = two_int[i][j][k][l]
two_int_spin[2*i+1][2*j+1][2*k+1][2*l+1] = two_int[i][j][k][l]
return one_int_spin, two_int_spin
[docs]
def electronic_data(mol):
"""
A function that utilizes `restricted Hartree-Fock (RHF) <https://pyscf.org/user/scf.html>`_
calculation in the `PySCF <https://pyscf.org>`_ quantum chemistry package to obtain the electronic data for
defining an electronic structure problem.
Parameters
----------
mol : pyscf.gto.Mole
The `molecule <https://pyscf.org/user/gto.html#>`_.
Returns
-------
data : dict
A dictionary specifying the electronic data for a molecule. The following data is provided:
* ``one_int`` : numpy.ndarray
The one-electron integrals w.r.t. spin orbitals (in physicists' notation).
* ``two_int`` : numpy.ndarray
The two-electron integrals w.r.t. spin orbitals (in physicists' notation).
* ``num_orb`` : int
The number of spin orbitals.
* ``num_elec`` : int
The number of electrons.
* ``energy_nuc``
The nuclear repulsion energy.
* ``energy_hf``
The Hartree-Fock ground state energy.
"""
from pyscf import scf, ao2mo
data = {}
threshold = 1e-9
def apply_threshold(matrix, threshold):
matrix[np.abs(matrix) < threshold] = 0
return matrix
# Set verbosity level to 0 to suppress output
mol.verbose = 0
# Perform a Hartree-Fock calculation
mf = scf.RHF(mol)
energy_hf = mf.kernel()
# Extract one-electron integrals
one_int = apply_threshold(mf.mo_coeff.T @ mf.get_hcore() @ mf.mo_coeff,threshold)
# Extract two-electron integrals (electron repulsion integrals)
two_int = ao2mo.kernel(mol,mf.mo_coeff)
# Full tensor with chemist's notation
two_int = apply_threshold(ao2mo.restore(1,two_int,mol.nao_nr()),threshold)
# Full tensor with physicist's notation
two_int = np.transpose(two_int,(0,2,3,1))
# Convert spacial orbital to spin orbitals
one_int, two_int = spacial_to_spin(one_int,two_int)
data['mol'] = mol
data['one_int'] = one_int
data['two_int'] = two_int
data['num_orb'] = 2*mf.mo_coeff.shape[0] # Number of spin orbitals
data['num_elec'] = mol.nelectron
data['energy_nuc'] = mol.energy_nuc()
data['energy_hf'] = energy_hf
return data
[docs]
def create_electronic_hamiltonian(arg, active_orb=None, active_elec=None):
"""
Creates the qubit Hamiltonian for an electronic structure problem.
If an Active Space (AS) is specified, the Hamiltonian is calculated following this `paper <https://arxiv.org/abs/2009.01872>`_.
Parameters
----------
arg : pyscf.gto.Mole or dict
A PySCF `molecule <https://pyscf.org/user/gto.html#>`_ or
a dictionary specifying the electronic data for a molecule. The following data is required:
* ``one_int`` : numpy.ndarray
The one-electron integrals w.r.t. spin orbitals (in physicists' notation).
* ``two_int`` : numpy.ndarray
The two-electron integrals w.r.t. spin orbitals (in physicists' notation).
* ``num_orb`` : int
The number of spin orbitals.
* ``num_elec`` : int
The number of electrons.
active_orb : int, optional
The number of active spin orbitals.
active_elec : int, optional
The number of active electrons.
Returns
-------
H : :ref:`FermionicOperator`
The fermionic Hamiltonian.
Examples
--------
We calucalte the fermionic Hamiltonian for the Hydrogen molecule, and transform it to a Pauli Hamiltonian via Jordan-Wigner transform.
::
from pyscf import gto
from qrisp.vqe.problems.electronic_structure import *
mol = gto.M(
atom = '''H 0 0 0; H 0 0 0.74''',
basis = 'sto-3g')
H = create_electronic_hamiltonian(mol)
H.to_qubit_operator()
Yields:
.. math::
-&0.812170607248714 - 0.0453026155037992 X_0X_1Y_2Y_3 + 0.0453026155037992 X_0Y_1Y_2X_3 - 0.0453026155037992 Y_0Y_1X_2X_3
&+0.171412826447769 Z_0 + 0.168688981703612 Z_0Z_1 + 0.120625234833904 Z_0Z_2 + 0.165927850337703 Z_0Z_3 + 0.171412826447769 Z_1
&+0.165927850337703 Z_1Z_2 + 0.120625234833904 Z_1Z_3 - 0.223431536908133 Z_2 + 0.174412876122615Z Z_2Z_3 - 0.223431536908133 Z_3
"""
import pyscf
if isinstance(arg,pyscf.gto.Mole):
data = electronic_data(arg)
elif isinstance(arg,dict):
data = arg
if not verify_symmetries(data['two_int']):
raise Warning("Failed to verify symmetries for two-electron integrals")
else:
raise TypeError("Cannot create electronic Hamiltonian from type "+str(type(arg)))
one_int = data['one_int']
two_int = data['two_int']
M = data['num_orb']
N = data['num_elec']
K = active_orb
L = active_elec
if K is None or L is None:
K = M
L = N
if L>N or K>M or K<L or K+N-L>M:
raise Exception("Invalid number of active electrons or orbitals")
# number of inactive electrons
I = N-L
# inactive Fock operator
F = one_int.copy()
for p in range(M):
for q in range(M):
for i in range(I):
#F[p][q] += (two_int[i][p][i][q]-two_int[i][q][p][i])
F[p][q] += (two_int[i][p][q][i]-two_int[i][q][i][p])
# inactive energy
E = 0
for j in range(I):
E += (one_int[j][j]+F[j][j])/2
# Hamiltonian
H=E
res_dict = {}
for i in range(K):
for j in range(K):
if F[I+i][I+j]!=0:
term = FermionicTerm([(j, False), (i, True)])
res_dict[term] = F[I+i][I+j]
#H += F[I+i][I+j]*c(i)*a(j)
for i in range(K):
for j in range(K):
for k in range(K):
for l in range(K):
if two_int[I+i][I+j][I+k][I+l]!=0 and i!=j and k!=l:
term = FermionicTerm([(l, False), (k, False), (j, True), (i, True)])
res_dict[term] = (0.5*two_int[I+i][I+j][I+k][I+l])
# H += (0.5*two_int[I+i][I+j][I+k][I+l])*c(i)*c(j)*a(k)*a(l)
temp_H = FermionicOperator(res_dict)
H = E+ temp_H
return H.reduce()
#
# ansatz
#
def conjugator(i,j):
h(i)
cx(i,j)
def pswap(phi,i,j):
with conjugate(conjugator)(i,j):
ry(-phi/2, [i,j])
def conjugator2(i,j,k,l):
cx(i,j)
cx(k,l)
def pswap2(phi,i,j,k,l):
with conjugate(conjugator2)(i,j,k,l):
with control([j,l],ctrl_state='00'):
pswap(phi,i,k)
[docs]
def create_QCCSD_ansatz(M,N):
r"""
This method creates a function for applying one layer of the `QCCSD ansatz <https://arxiv.org/abs/2005.08451>`_.
The chemistry-inspired Qubit Coupled Cluster Single Double (QCCSD) ansatz evolves the initial state,
usually, the Hartree-Fock state
.. math::
\ket{\Psi_{\text{HF}}}=\ket{1_0,\dotsc,1_N,0_{N+1},\dotsc,0_M}
under the action of parametrized (non-commuting) single and double excitation unitaries.
The single (S) excitation unitaries $U_i^r$ implement a continuous swap for qubits $i$ and $r$:
.. math::
U_i^r(\theta) = \begin{pmatrix}
1&0&0&0\\
0&\cos(\theta)&-\sin(\theta)&0\\
0&\sin(\theta)&\cos(\theta)&0\\
0&0&0&1
\end{pmatrix}
Similarly, the double (D) excitation unitaries $U_{ij}^{rs}(\theta)$ implement a continuous swap
for qubit pairs $i,j$ and $r,s$.
Parameters
----------
M : int
The number of (active) spin orbitals.
N : int
The number of (active) electrons.
Returns
-------
ansatz : function
A function that can be applied to a :ref:`QuantumVariable` and a list of parameters.
num_params : int
The number of parameters.
"""
spin_down_occupied = [i for i in range(N) if i%2==0]
spin_down_virtual = [i for i in range(N,M) if i%2==0]
spin_up_occupied = [i for i in range(N) if i%2==1]
spin_up_virtual = [i for i in range(N,M) if i%2==1]
num_singles = len(spin_down_occupied)*len(spin_down_virtual) + len(spin_up_occupied)*len(spin_up_virtual)
num_doubles = len(spin_down_occupied)*len(spin_up_occupied)*len(spin_down_virtual)*len(spin_up_virtual) \
+math.comb(len(spin_down_occupied),2)*math.comb(len(spin_down_virtual),2) \
+math.comb(len(spin_up_occupied),2)*math.comb(len(spin_up_virtual),2)
num_params = num_singles + num_doubles
def ansatz(qv, theta):
num_params = 0
# Single excitations
for i in spin_down_occupied:
for j in spin_down_virtual:
pswap(theta[num_params],qv[i],qv[j])
num_params += 1
for i in spin_up_occupied:
for j in spin_up_virtual:
pswap(theta[num_params],qv[i],qv[j])
num_params += 1
# Double excitation
for i in spin_down_occupied:
for j in spin_up_occupied:
for k in spin_down_virtual:
for l in spin_up_virtual:
pswap2(theta[num_params],qv[i],qv[j],qv[k],qv[l])
num_params += 1
for i,j in itertools.combinations(spin_down_occupied,2):
for k,l in itertools.combinations(spin_down_virtual,2):
pswap2(theta[num_params],qv[i],qv[j],qv[k],qv[l])
num_params += 1
for i,j in itertools.combinations(spin_up_occupied,2):
for k,l in itertools.combinations(spin_up_virtual,2):
pswap2(theta[num_params],qv[i],qv[j],qv[k],qv[l])
num_params += 1
return ansatz, num_params
[docs]
def create_hartree_fock_init_function(M, N):
"""
Creates the function that, when applied to a :ref:`QuantumVariable`, initializes the Hartee-Fock state:
Consistent with the Jordan-Wigner mapping, the first ``N`` qubits are initialized in the $\ket{1}$ state.
Parameters
----------
M : int
The number of (active) spin orbitals.
N : int
The number of (active) electrons.
Returns
-------
init_function : function
A function that can be applied to a :ref:`QuantumVariable`.
"""
def init_function(qv):
for i in range(N):
x(qv[i])
"""
if mapping_type=='parity':
def init_function(qv):
for i in range(N//2):
x(qv[2*i])
# odd number of electrons
if N%2==1:
for i in range(N-1,M):
x(qv[i])
"""
return init_function
[docs]
def electronic_structure_problem(arg, active_orb=None, active_elec=None, ansatz_type='QCCSD', threshold=1e-4):
r"""
Creates a VQE problem instance for an electronic structure problem defined by the
one-electron and two-electron integrals for the spin orbitals (in physicists' notation).
The problem Hamiltonian is given by:
.. math::
H = \sum\limits_{i,j=0}^{M-1}h_{i,j}a^{\dagger}_ia_j + \sum\limits_{i,j,k,l=0}^{M-1}h_{i,j,k,l}a^{\dagger}_ia^{\dagger}_ja_ka_l
for one-electron integrals:
.. math::
h_{i,j}=\int\mathrm dx \chi_i^*(x)\chi_j(x)
and two-electron integrals:
.. math::
h_{i,j,k,l} = \int\mathrm dx_1\mathrm dx_2\chi_i^*(x_1)\chi_j^*(x_2)\frac{1}{|r_1-r_2|}\chi_k(x_1)\chi_l(x_2)
Parameters
----------
arg : pyscf.gto.Mole or dict
A PySCF `molecule <https://pyscf.org/user/gto.html#>`_ or
a dictionary specifying the electronic data for a molecule. The following data is required:
* ``one_int`` : numpy.ndarray
The one-electron integrals w.r.t. spin orbitals (in physicists' notation).
* ``two_int`` : numpy.ndarray
The two-electron integrals w.r.t. spin orbitals (in physicists' notation).
* ``num_orb`` : int
The number of spin orbitals.
* ``num_elec`` : int
The number of electrons.
active_orb : int, optional
The number of active spin orbitals.
active_elec : int, optional
The number of active electrons.
ansatz_type : string, optional
The ansatz type. Availabe is ``QCCSD``. The default is ``QCCSD``.
threshold : float, optional
The threshold for the absolute value of the coefficients of Pauli products in the quantum Hamiltonian. The default is 1e-4.
Returns
-------
VQEProblem
The VQE problem instance.
Examples
--------
We calculate the electronic energy for the Hydrogen molecule at bond distance 0.74 angstroms:
::
from pyscf import gto
from qrisp import QuantumVariable
from qrisp.vqe.problems.electronic_structure import *
mol = gto.M(
atom = '''H 0 0 0; H 0 0 0.74''',
basis = 'sto-3g')
vqe = electronic_structure_problem(mol)
vqe.set_callback()
energy = vqe.run(QuantumVariable(4),depth=1,max_iter=50)
print(energy)
#Yields -1.8461290172512965
"""
from qrisp.vqe import VQEProblem
import pyscf
if isinstance(arg,pyscf.gto.Mole):
data = electronic_data(arg)
elif isinstance(arg,dict):
data = arg
if not verify_symmetries(data['two_int']):
raise Warning("Failed to verify symmetries for two-electron integrals")
else:
raise TypeError("Cannot instantiate VQEProblem from type "+str(type(arg)))
M = data['num_orb']
N = data['num_elec']
K = active_orb
L = active_elec
if K is None or L is None:
K = M
L = N
ansatz, num_params = create_QCCSD_ansatz(K,L)
fermionic_hamiltonian = create_electronic_hamiltonian(data,K,L)
hamiltonian = fermionic_hamiltonian.to_qubit_operator(mapping_type='jordan_wigner')
hamiltonian.apply_threshold(threshold)
return VQEProblem(hamiltonian, ansatz, num_params, init_function=create_hartree_fock_init_function(K,L))