Source code for pyscf.gw.gw_cd

#!/usr/bin/env python
# Copyright 2014-2020 The PySCF Developers. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# Author: Tianyu Zhu <zhutianyu1991@gmail.com>
#

'''
Spin-restricted G0W0 approximation with contour deformation

This implementation has the same scaling (N^4) as GW-AC, more robust but slower.
GW-CD is particularly recommended for accurate core and high-energy states.

Method:
    See T. Zhu and G.K.-L. Chan, arxiv:2007.03148 (2020) for details
    Compute Sigma directly on real axis with density fitting
    through a contour deformation method

Useful References:
    J. Chem. Theory Comput. 14, 4856-4869 (2018)
'''

from functools import reduce
import numpy
import numpy as np
import h5py
from scipy.optimize import newton, least_squares

from pyscf import lib
from pyscf.lib import logger
from pyscf.ao2mo import _ao2mo
from pyscf import df, scf
from pyscf.mp.mp2 import get_nocc, get_nmo, get_frozen_mask
from pyscf import __config__

einsum = lib.einsum

[docs] def kernel(gw, mo_energy, mo_coeff, Lpq=None, orbs=None, nw=None, vhf_df=False, verbose=logger.NOTE): '''GW-corrected quasiparticle orbital energies Returns: A list : converged, mo_energy, mo_coeff ''' mf = gw._scf if gw.frozen is None: frozen = 0 else: frozen = gw.frozen assert frozen == 0 if Lpq is None: Lpq = gw.ao2mo(mo_coeff) if orbs is None: orbs = range(gw.nmo) # v_xc v_mf = mf.get_veff() - mf.get_j() v_mf = reduce(numpy.dot, (mo_coeff.T, v_mf, mo_coeff)) nocc = gw.nocc nmo = gw.nmo # v_hf from DFT/HF density if vhf_df: # and frozen == 0: # density fitting for vk vk = -einsum('Lni,Lim->nm',Lpq[:,:,:nocc],Lpq[:,:nocc,:]) else: # exact vk without density fitting dm = mf.make_rdm1() rhf = scf.RHF(gw.mol) vk = rhf.get_veff(gw.mol,dm) - rhf.get_j(gw.mol,dm) vk = reduce(numpy.dot, (mo_coeff.T, vk, mo_coeff)) # Grids for integration on imaginary axis freqs,wts = _get_scaled_legendre_roots(nw) # Compute Wmn(iw) on imaginary axis logger.debug(gw, "Computing the imaginary part") Wmn = get_WmnI_diag(gw, orbs, Lpq, freqs) conv = True mo_energy = np.zeros_like(gw._scf.mo_energy) for p in orbs: if gw.linearized: # FIXME logger.warn(gw,'linearization with CD leads to wrong quasiparticle energy') raise NotImplementedError else: def quasiparticle(omega): sigma = get_sigma_diag(gw, omega, p, Lpq, Wmn[:,p-orbs[0],:], freqs, wts).real return omega - gw._scf.mo_energy[p] - (sigma.real + vk[p,p] - v_mf[p,p]) try: if p < nocc: delta = -1e-2 else: delta = 1e-2 e = newton(quasiparticle, gw._scf.mo_energy[p]+delta, tol=1e-6, maxiter=50) logger.debug(gw, "Computing poles for QP (orb: %s)"%(p)) mo_energy[p] = e except RuntimeError: conv = False mo_coeff = gw._scf.mo_coeff if gw.verbose >= logger.DEBUG: numpy.set_printoptions(threshold=nmo) logger.debug(gw, ' GW mo_energy =\n%s', mo_energy) numpy.set_printoptions(threshold=1000) return conv, mo_energy, mo_coeff
[docs] def get_sigma_diag(gw, ep, p, Lpq, Wmn, freqs, wts): ''' Compute self-energy on real axis using contour deformation ''' nocc = gw.nocc ef = (gw._scf.mo_energy[nocc-1] + gw._scf.mo_energy[nocc])/2. sign = np.sign(ef-gw._scf.mo_energy) sigmaI = get_sigmaI_diag(gw, ep, Wmn, sign, freqs, wts) sigmaR = get_sigmaR_diag(gw, ep, p, ef, Lpq) return sigmaI + sigmaR
[docs] def get_rho_response(omega, mo_energy, Lpq): ''' Compute density response function in auxiliary basis at freq iw ''' naux, nocc, nvir = Lpq.shape eia = mo_energy[:nocc,None] - mo_energy[None,nocc:] eia = eia/(omega**2+eia*eia) Pia = einsum('Pia,ia->Pia',Lpq,eia) # Response from both spin-up and spin-down density Pi = 4. * einsum('Pia,Qia->PQ',Pia,Lpq) return Pi
[docs] def get_WmnI_diag(gw, orbs, Lpq, freqs): ''' Compute W_mn(iw) on imarginary axis grids Return: Wmn: (Nmo, Norbs, Nw) ''' mo_energy = gw._scf.mo_energy nocc = gw.nocc nmo = gw.nmo nw = len(freqs) naux = Lpq.shape[0] norbs = len(orbs) Wmn = np.zeros((nmo,norbs,nw)) for w in range(nw): Pi = get_rho_response(freqs[w], mo_energy, Lpq[:,:nocc,nocc:]) Pi_inv = np.linalg.inv(np.eye(naux)-Pi)-np.eye(naux) Qnm = einsum('Pnm,PQ->Qnm',Lpq[:,orbs,:],Pi_inv) Wmn[:,:,w] = einsum('Qnm,Qmn->mn',Qnm,Lpq[:,:,orbs]) return Wmn
[docs] def get_sigmaI_diag(gw, omega, Wmn, sign, freqs, wts): ''' Compute self-energy by integrating on imaginary axis ''' mo_energy = gw._scf.mo_energy emo = omega - 1j*gw.eta*sign - mo_energy g0 = wts[None,:]*emo[:,None] / ((emo**2)[:,None]+(freqs**2)[None,:]) sigma = -einsum('mw,mw',g0,Wmn)/np.pi return sigma
[docs] def get_rho_response_R(gw, omega, Lpq): ''' Compute density response function in auxiliary basis at poles ''' naux, nocc, nvir = Lpq.shape mo_energy = gw._scf.mo_energy eia = mo_energy[:nocc,None] - mo_energy[None,nocc:] eia = 1./(omega+eia+2j*gw.eta) + 1./(-omega+eia) Pia = einsum('Pia,ia->Pia',Lpq,eia) # Response from both spin-up and spin-down density Pi = 2. * einsum('Pia,Qia->PQ',Pia,Lpq) return Pi
[docs] def get_sigmaR_diag(gw, omega, orbp, ef, Lpq): ''' Compute self-energy for poles inside contour (more and more expensive away from Fermi surface) ''' mo_energy = gw._scf.mo_energy nocc = gw.nocc naux = Lpq.shape[0] if omega > ef: fm = 1.0 idx = np.where((mo_energy<omega) & (mo_energy>ef))[0] else: fm = -1.0 idx = np.where((mo_energy>omega) & (mo_energy<ef))[0] sigmaR = 0j if len(idx) > 0: for m in idx: em = mo_energy[m] - omega Pi = get_rho_response_R(gw, abs(em), Lpq[:,:nocc,nocc:]) Pi_inv = np.linalg.inv(np.eye(naux)-Pi)-np.eye(naux) sigmaR += fm * np.dot(np.dot(Lpq[:,orbp,m],Pi_inv), Lpq[:,m,orbp]) return sigmaR
def _get_scaled_legendre_roots(nw): """ Scale nw Legendre roots, which lie in the interval [-1, 1], so that they lie in [0, inf) Ref: www.cond-mat.de/events/correl19/manuscripts/ren.pdf Returns: freqs : 1D ndarray wts : 1D ndarray """ freqs, wts = np.polynomial.legendre.leggauss(nw) x0 = 0.5 freqs_new = x0*(1.+freqs)/(1.-freqs) wts = wts*2.*x0/(1.-freqs)**2 return freqs_new, wts def _get_clenshaw_curtis_roots(nw): """ Clenshaw-Curtis quadrature on [0,inf) Ref: J. Chem. Phys. 132, 234114 (2010) Returns: freqs : 1D ndarray wts : 1D ndarray """ freqs = np.zeros(nw) wts = np.zeros(nw) a = 0.2 for w in range(nw): t = (w+1.0)/nw * np.pi/2. freqs[w] = a / np.tan(t) if w != nw-1: wts[w] = a*np.pi/2./nw/(np.sin(t)**2) else: wts[w] = a*np.pi/4./nw/(np.sin(t)**2) return freqs[::-1], wts[::-1]
[docs] class GWCD(lib.StreamObject): eta = getattr(__config__, 'gw_gw_GW_eta', 1e-3) linearized = getattr(__config__, 'gw_gw_GW_linearized', False) _keys = { 'eta', 'linearized', 'mol', 'frozen', 'with_df', 'mo_energy', 'mo_coeff', 'mo_occ', 'sigma', } def __init__(self, mf, frozen=None): self.mol = mf.mol self._scf = mf self.verbose = self.mol.verbose self.stdout = self.mol.stdout self.max_memory = mf.max_memory self.frozen = frozen #TODO: implement frozen orbs if not (self.frozen is None or self.frozen == 0): raise NotImplementedError # DF-GW must use density fitting integrals if getattr(mf, 'with_df', None): self.with_df = mf.with_df else: self.with_df = df.DF(mf.mol) self.with_df.auxbasis = df.make_auxbasis(mf.mol, mp2fit=True) ################################################## # don't modify the following attributes, they are not input options self._nocc = None self._nmo = None # self.mo_energy: GW quasiparticle energy, not scf mo_energy self.mo_energy = None self.mo_coeff = mf.mo_coeff self.mo_occ = mf.mo_occ self.sigma = None
[docs] def dump_flags(self): log = logger.Logger(self.stdout, self.verbose) log.info('') log.info('******** %s ********', self.__class__) log.info('method = %s', self.__class__.__name__) nocc = self.nocc nvir = self.nmo - nocc log.info('GW nocc = %d, nvir = %d', nocc, nvir) if self.frozen is not None: log.info('frozen = %s', self.frozen) logger.info(self, 'use perturbative linearized QP eqn = %s', self.linearized) return self
@property def nocc(self): return self.get_nocc() @nocc.setter def nocc(self, n): self._nocc = n @property def nmo(self): return self.get_nmo() @nmo.setter def nmo(self, n): self._nmo = n get_nocc = get_nocc get_nmo = get_nmo get_frozen_mask = get_frozen_mask
[docs] def kernel(self, mo_energy=None, mo_coeff=None, Lpq=None, orbs=None, nw=100, vhf_df=False): """ Input: orbs: self-energy orbs nw: grid number Output: mo_energy: GW quasiparticle energy """ if mo_coeff is None: mo_coeff = self._scf.mo_coeff if mo_energy is None: mo_energy = self._scf.mo_energy cput0 = (logger.process_clock(), logger.perf_counter()) self.dump_flags() self.converged, self.mo_energy, self.mo_coeff = \ kernel(self, mo_energy, mo_coeff, Lpq=Lpq, orbs=orbs, nw=nw, vhf_df=vhf_df, verbose=self.verbose) logger.timer(self, 'GW', *cput0) return self.mo_energy
[docs] def ao2mo(self, mo_coeff=None): if mo_coeff is None: mo_coeff = self.mo_coeff nmo = self.nmo naux = self.with_df.get_naoaux() mem_incore = (2*nmo**2*naux) * 8/1e6 mem_now = lib.current_memory()[0] mo = numpy.asarray(mo_coeff, order='F') ijslice = (0, nmo, 0, nmo) Lpq = None if (mem_incore + mem_now < 0.99*self.max_memory) or self.mol.incore_anyway: Lpq = _ao2mo.nr_e2(self.with_df._cderi, mo, ijslice, aosym='s2', out=Lpq) return Lpq.reshape(naux,nmo,nmo) else: logger.warn(self, 'Memory may not be enough!') raise NotImplementedError
if __name__ == '__main__': from pyscf import gto, dft, tddft mol = gto.Mole() mol.verbose = 4 mol.atom = [ [8 , (0. , 0. , 0.)], [1 , (0. , -0.7571 , 0.5861)], [1 , (0. , 0.7571 , 0.5861)]] mol.basis = 'def2-svp' mol.build() mf = dft.RKS(mol) mf.xc = 'pbe' mf.kernel() nocc = mol.nelectron//2 nmo = mf.mo_energy.size nvir = nmo-nocc gw = GWCD(mf) gw.kernel(orbs=range(0,nocc+3)) print(gw.mo_energy) assert (abs(gw.mo_energy[nocc-1]--0.41284735)<1e-5) assert (abs(gw.mo_energy[nocc]-0.16574524)<1e-5) assert (abs(gw.mo_energy[0]--19.53387986)<1e-5)