Source code for qrisp.vqe.problems.electronic_structure
"""\********************************************************************************* Copyright (c) 2025 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********************************************************************************/"""importnumpyasnpfromqrispimporth,x,cx,ry,control,conjugatefromqrisp.operators.qubitimportQubitOperator,QubitTermfromqrisp.operators.fermionicimport*fromfunctoolsimportcacheimportitertoolsimportmath## helper functions#defverify_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]foriinrange(M):forjinrange(M):forkinrange(M):forlinrange(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])iftest1>1e-9ortest2>1e-9ortest3>1e-9:returnFalsereturnTruedefdelta(i,j):ifi==j:return1else:return0## spacial orbitals to spin orbitals# defomega(x):returnx%2defspacial_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 tensorone_int_spin=np.zeros((num_spin_orbs,num_spin_orbs))foriinrange(num_spacial_orbs):forjinrange(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 tensortwo_int_spin=np.zeros((num_spin_orbs,num_spin_orbs,num_spin_orbs,num_spin_orbs))foriinrange(num_spacial_orbs):forjinrange(num_spacial_orbs):forkinrange(num_spacial_orbs):forlinrange(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]returnone_int_spin,two_int_spin
[docs]defelectronic_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. """frompyscfimportscf,ao2modata={}threshold=1e-9defapply_threshold(matrix,threshold):matrix[np.abs(matrix)<threshold]=0returnmatrix# Set verbosity level to 0 to suppress outputmol.verbose=0# Perform a Hartree-Fock calculationmf=scf.RHF(mol)energy_hf=mf.kernel()# Extract one-electron integralsone_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 notationtwo_int=apply_threshold(ao2mo.restore(1,two_int,mol.nao_nr()),threshold)# Full tensor with physicist's notationtwo_int=np.transpose(two_int,(0,2,3,1))# Convert spacial orbital to spin orbitalsone_int,two_int=spacial_to_spin(one_int,two_int)data['mol']=moldata['one_int']=one_intdata['two_int']=two_intdata['num_orb']=2*mf.mo_coeff.shape[0]# Number of spin orbitalsdata['num_elec']=mol.nelectrondata['energy_nuc']=mol.energy_nuc()data['energy_hf']=energy_hfreturndata
[docs]defcreate_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 """importpyscfifisinstance(arg,pyscf.gto.Mole):data=electronic_data(arg)elifisinstance(arg,dict):data=argifnotverify_symmetries(data['two_int']):raiseWarning("Failed to verify symmetries for two-electron integrals")else:raiseTypeError("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_orbL=active_elecifKisNoneorLisNone:K=ML=NifL>NorK>MorK<LorK+N-L>M:raiseException("Invalid number of active electrons or orbitals")# number of inactive electrons I=N-L# inactive Fock operatorF=one_int.copy()forpinrange(M):forqinrange(M):foriinrange(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 energyE=0forjinrange(I):E+=(one_int[j][j]+F[j][j])/2# HamiltonianH=Eres_dict={}foriinrange(K):forjinrange(K):ifF[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)foriinrange(K):forjinrange(K):forkinrange(K):forlinrange(K):iftwo_int[I+i][I+j][I+k][I+l]!=0andi!=jandk!=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_HreturnH.reduce()
[docs]defcreate_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=[iforiinrange(N)ifi%2==0]spin_down_virtual=[iforiinrange(N,M)ifi%2==0]spin_up_occupied=[iforiinrange(N)ifi%2==1]spin_up_virtual=[iforiinrange(N,M)ifi%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_doublesdefansatz(qv,theta):num_params=0# Single excitationsforiinspin_down_occupied:forjinspin_down_virtual:pswap(theta[num_params],qv[i],qv[j])num_params+=1foriinspin_up_occupied:forjinspin_up_virtual:pswap(theta[num_params],qv[i],qv[j])num_params+=1# Double excitationforiinspin_down_occupied:forjinspin_up_occupied:forkinspin_down_virtual:forlinspin_up_virtual:pswap2(theta[num_params],qv[i],qv[j],qv[k],qv[l])num_params+=1fori,jinitertools.combinations(spin_down_occupied,2):fork,linitertools.combinations(spin_down_virtual,2):pswap2(theta[num_params],qv[i],qv[j],qv[k],qv[l])num_params+=1fori,jinitertools.combinations(spin_up_occupied,2):fork,linitertools.combinations(spin_up_virtual,2):pswap2(theta[num_params],qv[i],qv[j],qv[k],qv[l])num_params+=1returnansatz,num_params
[docs]defcreate_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`. """definit_function(qv):foriinrange(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]) """returninit_function
[docs]defelectronic_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 """fromqrisp.vqeimportVQEProblemimportpyscfifisinstance(arg,pyscf.gto.Mole):data=electronic_data(arg)elifisinstance(arg,dict):data=argifnotverify_symmetries(data['two_int']):raiseWarning("Failed to verify symmetries for two-electron integrals")else:raiseTypeError("Cannot instantiate VQEProblem from type "+str(type(arg)))M=data['num_orb']N=data['num_elec']K=active_orbL=active_elecifKisNoneorLisNone:K=ML=Nansatz,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)returnVQEProblem(hamiltonian,ansatz,num_params,init_function=create_hartree_fock_init_function(K,L))
Get in touch!
If you are interested in Qrisp or high-level quantum algorithm research in general connect with us on our
Slack workspace.