def get_chi(self, xc='RPA', q_c=[0, 0, 0], direction='x'): """ Returns v^1/2 chi V^1/2""" pd, chi0_wGG, chi0_wxvG, chi0_wvv = self.calculate_chi0(q_c) G_G = pd.G2_qG[0]**0.5 nG = len(G_G) if pd.kd.gamma: G_G[0] = 1.0 if isinstance(direction, str): d_v = { 'x': [1, 0, 0], 'y': [0, 1, 0], 'z': [0, 0, 1] }[direction] else: d_v = direction d_v = np.asarray(d_v) / np.linalg.norm(d_v) W = slice(self.w1, self.w2) chi0_wGG[:, 0] = np.dot(d_v, chi0_wxvG[W, 0]) chi0_wGG[:, :, 0] = np.dot(d_v, chi0_wxvG[W, 1]) chi0_wGG[:, 0, 0] = np.dot(d_v, np.dot(chi0_wvv[W], d_v).T) G_G /= (4 * pi)**0.5 if self.truncation == 'wigner-seitz': kernel = WignerSeitzTruncatedCoulomb(pd.gd.cell_cv, self.chi0.calc.wfs.kd.N_c) K_G = kernel.get_potential(pd) K_G *= G_G**2 if pd.kd.gamma: K_G[0] = 0.0 elif self.truncation == '2D': K_G = truncated_coulomb(pd) K_G *= G_G**2 else: K_G = np.ones(nG) K_GG = np.zeros((nG, nG), dtype=complex) for i in range(nG): K_GG[i, i] = K_G[i] if xc != 'RPA': R_av = self.chi0.calc.atoms.positions / Bohr nt_sG = self.chi0.calc.density.nt_sG K_GG += calculate_Kxc(pd, nt_sG, R_av, self.chi0.calc.wfs.setups, self.chi0.calc.density.D_asp, functional=xc) * G_G * G_G[:, np.newaxis] chi_wGG = [] for chi0_GG in chi0_wGG: chi0_GG[:] = chi0_GG / G_G / G_G[:, np.newaxis] chi_wGG.append( np.dot(np.linalg.inv(np.eye(nG) - np.dot(chi0_GG, K_GG)), chi0_GG)) return chi0_wGG, np.array(chi_wGG)
def get_dielectric_matrix(self, xc='RPA', q_c=[0, 0, 0], direction='x', symmetric=True, calculate_chi=False, q_v=None, add_intraband=True): """Returns the symmetrized dielectric matrix. :: \tilde\epsilon_GG' = v^{-1/2}_G \epsilon_GG' v^{1/2}_G', where:: epsilon_GG' = 1 - v_G * P_GG' and P_GG' is the polarization. :: In RPA: P = chi^0 In TDDFT: P = (1 - chi^0 * f_xc)^{-1} chi^0 in addition to RPA one can use the kernels, ALDA, rALDA, rAPBE, Bootstrap and LRalpha (long-range kerne), where alpha is a user specified parameter (for example xc='LR0.25') The head of the inverse symmetrized dielectric matrix is equal to the head of the inverse dielectric matrix (inverse dielectric function)""" pd, chi0_wGG, chi0_wxvG, chi0_wvv = self.calculate_chi0(q_c) N_c = self.chi0.calc.wfs.kd.N_c if self.truncation == 'wigner-seitz': self.wstc = WignerSeitzTruncatedCoulomb(pd.gd.cell_cv, N_c) else: self.wstc = None K_G = get_coulomb_kernel(pd, N_c, truncation=self.truncation, wstc=self.wstc, q_v=q_v)**0.5 nG = len(K_G) if pd.kd.gamma: if isinstance(direction, str): d_v = { 'x': [1, 0, 0], 'y': [0, 1, 0], 'z': [0, 0, 1] }[direction] else: d_v = direction d_v = np.asarray(d_v) / np.linalg.norm(d_v) W = slice(self.w1, self.w2) if add_intraband: chi0_wGG[:, 0] = np.dot(d_v, chi0_wxvG[W, 0]) chi0_wGG[:, :, 0] = np.dot(d_v, chi0_wxvG[W, 1]) chi0_wGG[:, 0, 0] = np.dot(d_v, np.dot(chi0_wvv[W], d_v).T) if q_v is not None: print('Restoring q dependence of head and wings of chi0') chi0_wGG[:, 1:, 0] *= np.dot(q_v, d_v) chi0_wGG[:, 0, 1:] *= np.dot(q_v, d_v) chi0_wGG[:, 0, 0] *= np.dot(q_v, d_v)**2 if xc != 'RPA': Kxc_sGG = get_xc_kernel(pd, self.chi0, functional=xc, chi0_wGG=chi0_wGG) if calculate_chi: chi_wGG = [] for chi0_GG in chi0_wGG: if xc == 'RPA': P_GG = chi0_GG else: P_GG = np.dot( np.linalg.inv(np.eye(nG) - np.dot(chi0_GG, Kxc_sGG[0])), chi0_GG) if symmetric: e_GG = np.eye(nG) - P_GG * K_G * K_G[:, np.newaxis] else: K_GG = (K_G**2 * np.ones([nG, nG])).T e_GG = np.eye(nG) - P_GG * K_GG if calculate_chi: K_GG = np.diag(K_G**2) if xc != 'RPA': K_GG += Kxc_sGG[0] chi_wGG.append( np.dot(np.linalg.inv(np.eye(nG) - np.dot(chi0_GG, K_GG)), chi0_GG)) chi0_GG[:] = e_GG # chi0_wGG is now the dielectric matrix if not calculate_chi: return chi0_wGG else: # chi_wGG is the full density response function.. return pd, chi0_wGG, np.array(chi_wGG)
def get_chi(self, xc='RPA', q_c=[0, 0, 0], direction='x', return_VchiV=True, q_v=None): """ Returns v^1/2 chi v^1/2. The truncated Coulomb interaction is then included as v^-1/2 v_t v^-1/2. This is in order to conform with the head and wings of chi0, which is treated specially for q=0.""" pd, chi0_wGG, chi0_wxvG, chi0_wvv = self.calculate_chi0(q_c) N_c = self.chi0.calc.wfs.kd.N_c Kbare_G = get_coulomb_kernel(pd, N_c, truncation=None, q_v=q_v) vsqr_G = Kbare_G**0.5 nG = len(vsqr_G) if self.truncation is not None: if self.truncation == 'wigner-seitz': self.wstc = WignerSeitzTruncatedCoulomb(pd.gd.cell_cv, N_c) else: self.wstc = None Ktrunc_G = get_coulomb_kernel(pd, N_c, truncation=self.truncation, wstc=self.wstc, q_v=q_v) K_GG = np.diag(Ktrunc_G / Kbare_G) else: K_GG = np.eye(nG, dtype=complex) if pd.kd.gamma: if isinstance(direction, str): d_v = { 'x': [1, 0, 0], 'y': [0, 1, 0], 'z': [0, 0, 1] }[direction] else: d_v = direction d_v = np.asarray(d_v) / np.linalg.norm(d_v) W = slice(self.w1, self.w2) chi0_wGG[:, 0] = np.dot(d_v, chi0_wxvG[W, 0]) chi0_wGG[:, :, 0] = np.dot(d_v, chi0_wxvG[W, 1]) chi0_wGG[:, 0, 0] = np.dot(d_v, np.dot(chi0_wvv[W], d_v).T) if xc != 'RPA': Kxc_sGG = get_xc_kernel(pd, self.chi0, functional=xc, chi0_wGG=chi0_wGG) K_GG += Kxc_sGG[0] / vsqr_G / vsqr_G[:, np.newaxis] chi_wGG = [] for chi0_GG in chi0_wGG: """v^1/2 chi0 V^1/2""" chi0_GG[:] = chi0_GG * vsqr_G * vsqr_G[:, np.newaxis] chi_GG = np.dot(np.linalg.inv(np.eye(nG) - np.dot(chi0_GG, K_GG)), chi0_GG) if not return_VchiV: chi0_GG /= vsqr_G * vsqr_G[:, np.newaxis] chi_GG /= vsqr_G * vsqr_G[:, np.newaxis] chi_wGG.append(chi_GG) return pd, chi0_wGG, np.array(chi_wGG)
def __init__(self, calc, xc=None, kpts=None, bands=None, ecut=None, omega=None, world=mpi.world, txt=sys.stdout, timer=None): PairDensity.__init__(self, calc, ecut, world=world, txt=txt, timer=timer) if xc is None or xc == 'EXX': self.exx_fraction = 1.0 xc = XC(XCNull()) elif xc == 'PBE0': self.exx_fraction = 0.25 xc = XC('HYB_GGA_XC_PBEH') elif xc == 'HSE03': omega = 0.106 self.exx_fraction = 0.25 xc = XC('HYB_GGA_XC_HSE03') elif xc == 'HSE06': omega = 0.11 self.exx_fraction = 0.25 xc = XC('HYB_GGA_XC_HSE06') elif xc == 'B3LYP': self.exx_fraction = 0.2 xc = XC('HYB_GGA_XC_B3LYP') self.xc = xc self.omega = omega self.exc = np.nan # density dependent part of xc-energy self.kpts = select_kpts(kpts, self.calc) if bands is None: # Do all occupied bands: bands = [0, self.nocc2] prnt('Calculating exact exchange contributions for band index', '%d-%d' % (bands[0], bands[1] - 1), file=self.fd) prnt('for IBZ k-points with indices:', ', '.join(str(i) for i in self.kpts), file=self.fd) self.bands = bands if self.ecut is None: self.ecut = self.calc.wfs.pd.ecut prnt('Plane-wave cutoff: %.3f eV' % (self.ecut * Hartree), file=self.fd) shape = (self.calc.wfs.nspins, len(self.kpts), bands[1] - bands[0]) self.exxvv_sin = np.zeros(shape) # valence-valence exchange energies self.exxvc_sin = np.zeros(shape) # valence-core exchange energies self.f_sin = np.empty(shape) # occupation numbers # The total EXX energy will not be calculated if we are only # interested in a few eigenvalues for a few k-points self.exx = np.nan # total EXX energy self.exxvv = np.nan # valence-valence self.exxvc = np.nan # valence-core self.exxcc = 0.0 # core-core self.mysKn1n2 = None # my (s, K, n1, n2) indices self.distribute_k_points_and_bands(0, self.nocc2) # All occupied states: self.mykpts = [ self.get_k_point(s, K, n1, n2) for s, K, n1, n2 in self.mysKn1n2 ] prnt('Using Wigner-Seitz truncated coulomb interaction.', file=self.fd) self.wstc = WignerSeitzTruncatedCoulomb(self.calc.wfs.gd.cell_cv, self.calc.wfs.kd.N_c, self.fd) self.iG_qG = {} # cache # PAW matrices: self.V_asii = [] # valence-valence correction self.C_aii = [] # valence-core correction self.initialize_paw_exx_corrections()
def __init__(self, calc=None, spinors=False, ecut=10., scale=1.0, nbands=None, valence_bands=None, conduction_bands=None, eshift=None, gw_skn=None, truncation=None, integrate_gamma=1, txt=sys.stdout, mode='BSE', wfile=None, write_h=False, write_v=False): """Creates the BSE object calc: str or calculator object The string should refer to the .gpw file contaning KS orbitals ecut: float Plane wave cutoff energy (eV) nbands: int Number of bands used for the screened interaction valence_bands: list Valence bands used in the BSE Hamiltonian conduction_bands: list Conduction bands used in the BSE Hamiltonian eshift: float Scissors operator opening the gap (eV) gw_skn: list / array List or array defining the gw quasiparticle energies used in the BSE Hamiltonian. Should match spin, k-points and valence/conduction bands truncation: str Coulomb truncation scheme. Can be either wigner-seitz, 2D, 1D, or 0D integrate_gamma: int Method to integrate the Coulomb interaction. 1 is a numerical integration at all q-points with G=[0,0,0] - this breaks the symmetry slightly. 0 is analytical integration at q=[0,0,0] only - this conserves the symmetry. integrate_gamma=2 is the same as 1, but the average is only carried out in the non-periodic directions. txt: str txt output mode: str Theory level used. can be RPA TDHF or BSE. Only BSE is screened. wfile: str File for saving screened interaction and some other stuff needed later write_h: bool If True, write the BSE Hamiltonian to H_SS.ulm. write_v: bool If True, write eigenvalues and eigenstates to v_TS.ulm """ # Calculator if isinstance(calc, str): calc = GPAW(calc, txt=None, communicator=serial_comm) self.calc = calc self.spinors = spinors self.scale = scale assert mode in ['RPA', 'TDHF', 'BSE'] # assert calc.wfs.kd.nbzkpts % world.size == 0 # txt file if world.rank != 0: txt = devnull elif isinstance(txt, str): txt = open(txt, 'w', 1) self.fd = txt self.ecut = ecut / Hartree self.nbands = nbands self.mode = mode self.truncation = truncation if integrate_gamma == 0 and truncation is not None: print('***WARNING*** Analytical Coulomb integration is ' + 'not expected to work with Coulomb truncation. ' + 'Use integrate_gamma=1', file=self.fd) self.integrate_gamma = integrate_gamma self.wfile = wfile self.write_h = write_h self.write_v = write_v # Find q-vectors and weights in the IBZ: self.kd = calc.wfs.kd if -1 in self.kd.bz2bz_ks: print('***WARNING*** Symmetries may not be right ' + 'Use gamma-centered grid to be sure', file=self.fd) offset_c = 0.5 * ((self.kd.N_c + 1) % 2) / self.kd.N_c bzq_qc = monkhorst_pack(self.kd.N_c) + offset_c self.qd = KPointDescriptor(bzq_qc) self.qd.set_symmetry(self.calc.atoms, self.kd.symmetry) self.vol = abs(np.linalg.det(calc.wfs.gd.cell_cv)) # bands self.spins = self.calc.wfs.nspins if self.spins == 2: if self.spinors: self.spinors = False print('***WARNING*** Presently the spinor version' + 'does not work for spin-polarized calculations.' + 'Performing scalar calculation', file=self.fd) assert len(valence_bands[0]) == len(valence_bands[1]) assert len(conduction_bands[0]) == len(conduction_bands[1]) if valence_bands is None: nv = self.calc.wfs.setups.nvalence valence_bands = [[nv // 2 - 1]] if self.spins == 2: valence_bands *= 2 if conduction_bands is None: conduction_bands = [[valence_bands[-1] + 1]] if self.spins == 2: conduction_bands *= 2 self.val_sn = np.array(valence_bands) if len(np.shape(self.val_sn)) == 1: self.val_sn = np.array([self.val_sn]) self.con_sn = np.array(conduction_bands) if len(np.shape(self.con_sn)) == 1: self.con_sn = np.array([self.con_sn]) self.td = True for n in self.val_sn[0]: if n in self.con_sn[0]: self.td = False if len(self.val_sn) == 2: for n in self.val_sn[1]: if n in self.con_sn[1]: self.td = False self.nv = len(self.val_sn[0]) self.nc = len(self.con_sn[0]) if eshift is not None: eshift /= Hartree if gw_skn is not None: assert self.nv + self.nc == len(gw_skn[0, 0]) assert self.kd.nibzkpts == len(gw_skn[0]) gw_skn = gw_skn[:, self.kd.bz2ibz_k] # assert self.kd.nbzkpts == len(gw_skn[0]) gw_skn /= Hartree self.gw_skn = gw_skn self.eshift = eshift # Number of pair orbitals self.nS = self.kd.nbzkpts * self.nv * self.nc * self.spins self.nS *= (self.spinors + 1)**2 # Wigner-Seitz stuff if self.truncation == 'wigner-seitz': self.wstc = WignerSeitzTruncatedCoulomb(self.calc.wfs.gd.cell_cv, self.kd.N_c, self.fd) else: self.wstc = None self.print_initialization(self.td, self.eshift, self.gw_skn)
def get_chi(self, xc='RPA', q_c=[0, 0, 0], spin='all', direction='x', return_VchiV=True, q_v=None, RSrep='gpaw', spinpol_cut=None, density_cut=None, fxc_scaling=None): """ Returns v^1/2 chi v^1/2 for the density response and chi for the spin response. The truncated Coulomb interaction is included as v^-1/2 v_t v^-1/2. This is in order to conform with the head and wings of chi0, which is treated specially for q=0. spin : str or int If 'all' then include all spins. If 0 or 1, only include this specific spin. (not used in transverse reponse functions) RSrep : str real space representation of kernel ('gpaw' or 'grid') spinpol_cut : float cutoff spin polarization below which f_xc is evaluated in unpolarized limit (make sure divergent terms cancel out correctly) density_cut : float cutoff density below which f_xc is set to zero fxc_scaling : list Possible scaling of kernel to hit Goldstone mode. If w=0 is included in the present calculation and fxc_scaling=[True, None], the fxc_scaling to match kappaM_w[0] = 0. will be calculated. If fxc_scaling = [True, float], Kxc will be scaled by float. Default is None, i.e. no scaling """ # XXX generalize to kernel check response = self.chi0.response if response in ['+-', '-+']: assert xc in ('ALDA_x', 'ALDA_X', 'ALDA') pd, chi0_wGG, chi0_wxvG, chi0_wvv = self.calculate_chi0(q_c, spin) if response == 'density': N_c = self.chi0.calc.wfs.kd.N_c Kbare_G = get_coulomb_kernel(pd, N_c, truncation=None, q_v=q_v) vsqr_G = Kbare_G**0.5 nG = len(vsqr_G) if self.truncation is not None: if self.truncation == 'wigner-seitz': self.wstc = WignerSeitzTruncatedCoulomb(pd.gd.cell_cv, N_c) else: self.wstc = None Ktrunc_G = get_coulomb_kernel(pd, N_c, truncation=self.truncation, wstc=self.wstc, q_v=q_v) K_GG = np.diag(Ktrunc_G / Kbare_G) else: K_GG = np.eye(nG, dtype=complex) if pd.kd.gamma: if isinstance(direction, str): d_v = { 'x': [1, 0, 0], 'y': [0, 1, 0], 'z': [0, 0, 1] }[direction] else: d_v = direction d_v = np.asarray(d_v) / np.linalg.norm(d_v) W = slice(self.w1, self.w2) chi0_wGG[:, 0] = np.dot(d_v, chi0_wxvG[W, 0]) chi0_wGG[:, :, 0] = np.dot(d_v, chi0_wxvG[W, 1]) chi0_wGG[:, 0, 0] = np.dot(d_v, np.dot(chi0_wvv[W], d_v).T) if xc != 'RPA': Kxc_GG = get_xc_kernel(pd, self.chi0, functional=xc, chi0_wGG=chi0_wGG, density_cut=density_cut) K_GG += Kxc_GG / vsqr_G / vsqr_G[:, np.newaxis] # Invert Dyson eq. chi_wGG = [] for chi0_GG in chi0_wGG: """v^1/2 chi0 V^1/2""" chi0_GG[:] = chi0_GG * vsqr_G * vsqr_G[:, np.newaxis] chi_GG = np.dot( np.linalg.inv(np.eye(nG) - np.dot(chi0_GG, K_GG)), chi0_GG) if not return_VchiV: chi0_GG /= vsqr_G * vsqr_G[:, np.newaxis] chi_GG /= vsqr_G * vsqr_G[:, np.newaxis] chi_wGG.append(chi_GG) if len(chi_wGG): chi_wGG = np.array(chi_wGG) else: chi_wGG = np.zeros((0, nG, nG), complex) # Spin response else: Kxc_GG = get_xc_kernel(pd, self.chi0, functional=xc, kernel=response[::-1], RSrep=RSrep, chi0_wGG=chi0_wGG, fxc_scaling=fxc_scaling, density_cut=density_cut, spinpol_cut=spinpol_cut) # Invert Dyson equation chi_wGG = [] for chi0_GG in chi0_wGG: chi_GG = np.dot( np.linalg.inv( np.eye(len(chi0_GG)) - np.dot(chi0_GG, Kxc_GG)), chi0_GG) chi_wGG.append(chi_GG) return pd, chi0_wGG, np.array(chi_wGG)
def calculate(self, ecut, nbands=None, spin=False): """Calculate RPA correlation energy for one or several cutoffs. ecut: float or list of floats Plane-wave cutoff(s). nbands: int Number of bands (defaults to number of plane-waves). spin: bool Separate spin in response function. (Only needed for beyond RPA methods that inherit this function). """ p = functools.partial(print, file=self.fd) if isinstance(ecut, (float, int)): ecut = ecut * (1 + 0.5 * np.arange(6))**(-2 / 3) self.ecut_i = np.asarray(np.sort(ecut)) / Hartree ecutmax = max(self.ecut_i) if nbands is None: p('Response function bands : Equal to number of plane waves') else: p('Response function bands : %s' % nbands) p('Plane wave cutoffs (eV) :', end='') for e in self.ecut_i: p(' {0:.3f}'.format(e * Hartree), end='') p() if self.truncation is not None: p('Using %s Coulomb truncation' % self.truncation) p() if self.filename and os.path.isfile(self.filename): self.read() self.world.barrier() chi0 = Chi0(self.calc, 1j * Hartree * self.omega_w, eta=0.0, intraband=False, hilbert=False, txt='chi0.txt', timer=self.timer, world=self.world, nblocks=self.nblocks) self.blockcomm = chi0.blockcomm wfs = self.calc.wfs if self.truncation == 'wigner-seitz': self.wstc = WignerSeitzTruncatedCoulomb(wfs.gd.cell_cv, wfs.kd.N_c, self.fd) else: self.wstc = None nq = len(self.energy_qi) nw = len(self.omega_w) nGmax = max(count_reciprocal_vectors(ecutmax, wfs.gd, q_c) for q_c in self.ibzq_qc[nq:]) mynGmax = (nGmax + self.nblocks - 1) // self.nblocks nx = (1 + spin) * nw * mynGmax * nGmax A1_x = np.empty(nx, complex) if self.nblocks > 1: A2_x = np.empty(nx, complex) else: A2_x = None self.timer.start('RPA') for q_c in self.ibzq_qc[nq:]: if np.allclose(q_c, 0.0) and self.skip_gamma: self.energy_qi.append(len(self.ecut_i) * [0.0]) self.write() p('Not calculating E_c(q) at Gamma') p() continue thisqd = KPointDescriptor([q_c]) pd = PWDescriptor(ecutmax, wfs.gd, complex, thisqd) nG = pd.ngmax mynG = (nG + self.nblocks - 1) // self.nblocks chi0.Ga = self.blockcomm.rank * mynG chi0.Gb = min(chi0.Ga + mynG, nG) shape = (1 + spin, nw, chi0.Gb - chi0.Ga, nG) chi0_swGG = A1_x[:np.prod(shape)].reshape(shape) chi0_swGG[:] = 0.0 if np.allclose(q_c, 0.0): chi0_swxvG = np.zeros((1 + spin, nw, 2, 3, nG), complex) chi0_swvv = np.zeros((1 + spin, nw, 3, 3), complex) else: chi0_swxvG = None chi0_swvv = None # First not completely filled band: m1 = chi0.nocc1 p('# %s - %s' % (len(self.energy_qi), ctime().split()[-2])) p('q = [%1.3f %1.3f %1.3f]' % tuple(q_c)) energy_i = [] for ecut in self.ecut_i: if ecut == ecutmax: # Nothing to cut away: cut_G = None m2 = nbands or nG else: cut_G = np.arange(nG)[pd.G2_qG[0] <= 2 * ecut] m2 = len(cut_G) p('E_cut = %d eV / Bands = %d:' % (ecut * Hartree, m2)) self.fd.flush() energy = self.calculate_q(chi0, pd, chi0_swGG, chi0_swxvG, chi0_swvv, m1, m2, cut_G, A2_x) energy_i.append(energy) m1 = m2 a = 1 / chi0.kncomm.size if ecut < ecutmax and a != 1.0: # Chi0 will be summed again over chicomm, so we divide # by its size: chi0_swGG *= a if chi0_swxvG is not None: chi0_swxvG *= a chi0_swvv *= a self.energy_qi.append(energy_i) self.write() p() e_i = np.dot(self.weight_q, np.array(self.energy_qi)) p('==========================================================') p() p('Total correlation energy:') for e_cut, e in zip(self.ecut_i, e_i): p('%6.0f: %6.4f eV' % (e_cut * Hartree, e * Hartree)) p() self.energy_qi = [] # important if another calculation is performed if len(e_i) > 1: self.extrapolate(e_i) p('Calculation completed at: ', ctime()) p() self.timer.stop('RPA') self.timer.write(self.fd) self.fd.flush() return e_i * Hartree
def get_dielectric_matrix(self, xc='RPA', q_c=[0, 0, 0], direction='x', symmetric=True, calculate_chi=False): """Returns the symmetrized dielectric matrix. :: \tilde\epsilon_GG' = v^{-1/2}_G \epsilon_GG' v^{1/2}_G', where:: epsilon_GG' = 1 - v_G * P_GG' and P_GG' is the polarization. :: In RPA: P = chi^0 In TDDFT: P = (1 - chi^0 * f_xc)^{-1} chi^0 The head of the inverse symmetrized dielectric matrix is equal to the head of the inverse dielectric matrix (inverse dielectric function) """ pd, chi0_wGG, chi0_wxvG, chi0_wvv = self.calculate_chi0(q_c) G_G = pd.G2_qG[0]**0.5 nG = len(G_G) if pd.kd.gamma: G_G[0] = 1.0 if isinstance(direction, str): d_v = { 'x': [1, 0, 0], 'y': [0, 1, 0], 'z': [0, 0, 1] }[direction] else: d_v = direction d_v = np.asarray(d_v) / np.linalg.norm(d_v) W = slice(self.w1, self.w2) chi0_wGG[:, 0] = np.dot(d_v, chi0_wxvG[W, 0]) chi0_wGG[:, :, 0] = np.dot(d_v, chi0_wxvG[W, 1]) chi0_wGG[:, 0, 0] = np.dot(d_v, np.dot(chi0_wvv[W], d_v).T) if self.truncation == 'wigner-seitz': kernel = WignerSeitzTruncatedCoulomb(pd.gd.cell_cv, self.chi0.calc.wfs.kd.N_c) K_G = kernel.get_potential(pd)**0.5 if pd.kd.gamma: K_G[0] = 0.0 elif self.truncation == '2D': K_G = truncated_coulomb(pd) if pd.kd.gamma: K_G[0] = 0.0 else: K_G = (4 * pi)**0.5 / G_G if xc != 'RPA': R_av = self.chi0.calc.atoms.positions / Bohr nt_sG = self.chi0.calc.density.nt_sG Kxc_sGG = calculate_Kxc(pd, nt_sG, R_av, self.chi0.calc.wfs.setups, self.chi0.calc.density.D_asp, functional=xc) if calculate_chi: chi_wGG = [] for chi0_GG in chi0_wGG: if xc == 'RPA': P_GG = chi0_GG else: P_GG = np.dot( np.linalg.inv(np.eye(nG) - np.dot(chi0_GG, Kxc_sGG[0])), chi0_GG) if symmetric: e_GG = np.eye(nG) - P_GG * K_G * K_G[:, np.newaxis] else: K_GG = (K_G**2 * np.ones([nG, nG])).T e_GG = np.eye(nG) - P_GG * K_GG if calculate_chi: K_GG = np.diag(K_G**2) if xc != 'RPA': K_GG += Kxc_sGG[0] chi_wGG.append( np.dot(np.linalg.inv(np.eye(nG) - np.dot(chi0_GG, K_GG)), chi0_GG)) chi0_GG[:] = e_GG # chi0_wGG is now the dielectric matrix if not calculate_chi: return chi0_wGG else: return pd, chi0_wGG, chi_wGG
def calculate_screened_potential(self): """Calculates the screened potential for each q-point in the 1st BZ. Since many q-points are related by symmetry, the actual calculation is only done for q-points in the IBZ and the rest are obtained by symmetry transformations. Results are returned as a generator to that it is not necessary to store a huge matrix for each q-point in the memory.""" # The decorator $timer('W') doesn't work for generators, do we will # have to manually start and stop the timer here: self.timer.start('W') print('Calculating screened Coulomb potential', file=self.fd) if self.wstc: print('Using Wigner-Seitz truncated Coloumb potential', file=self.fd) if self.ppa: print('Using Godby-Needs plasmon-pole approximation:', file=self.fd) print(' Fitting energy: i*E0, E0 = %.3f Hartee' % self.E0, file=self.fd) # use small imaginary frequency to avoid dividing by zero: frequencies = [1e-10j, 1j * self.E0 * Hartree] parameters = {'eta': 0, 'hilbert': False, 'timeordered': False, 'frequencies': frequencies} else: print('Using full frequency integration:', file=self.fd) print(' domega0: {0:g}'.format(self.domega0 * Hartree), file=self.fd) print(' omega2: {0:g}'.format(self.omega2 * Hartree), file=self.fd) parameters = {'eta': self.eta * Hartree, 'hilbert': True, 'timeordered': True, 'domega0': self.domega0 * Hartree, 'omega2': self.omega2 * Hartree} chi0 = Chi0(self.calc, nbands=self.nbands, ecut=self.ecut * Hartree, intraband=False, real_space_derivatives=False, txt=self.filename + '.w.txt', timer=self.timer, keep_occupied_states=True, nblocks=self.blockcomm.size, no_optical_limit=self.wstc, **parameters) if self.wstc: wstc = WignerSeitzTruncatedCoulomb( self.calc.wfs.gd.cell_cv, self.calc.wfs.kd.N_c, chi0.fd) else: wstc = None self.omega_w = chi0.omega_w self.omegamax = chi0.omegamax htp = HilbertTransform(self.omega_w, self.eta, gw=True) htm = HilbertTransform(self.omega_w, -self.eta, gw=True) # Find maximum size of chi-0 matrices: gd = self.calc.wfs.gd nGmax = max(count_reciprocal_vectors(self.ecut, gd, q_c) for q_c in self.qd.ibzk_kc) nw = len(self.omega_w) size = self.blockcomm.size mynGmax = (nGmax + size - 1) // size mynw = (nw + size - 1) // size # Allocate memory in the beginning and use for all q: A1_x = np.empty(nw * mynGmax * nGmax, complex) A2_x = np.empty(max(mynw * nGmax, nw * mynGmax) * nGmax, complex) # Need to pause the timer in between iterations self.timer.stop('W') for iq, q_c in enumerate(self.qd.ibzk_kc): self.timer.start('W') if self.savew: wfilename = self.filename + '.w.q%d.pckl' % iq fd = opencew(wfilename) if self.savew and fd is None: # Read screened potential from file with open(wfilename) as fd: pd, W = pickle.load(fd) else: # First time calculation pd, W = self.calculate_w(chi0, q_c, htp, htm, wstc, A1_x, A2_x) if self.savew: pickle.dump((pd, W), fd, pickle.HIGHEST_PROTOCOL) self.timer.stop('W') # Loop over all k-points in the BZ and find those that are related # to the current IBZ k-point by symmetry Q1 = self.qd.ibz2bz_k[iq] done = set() for s, Q2 in enumerate(self.qd.bz2bz_ks[Q1]): if Q2 >= 0 and Q2 not in done: s = self.qd.sym_k[Q2] self.s = s self.U_cc = self.qd.symmetry.op_scc[s] time_reversal = self.qd.time_reversal_k[Q2] self.sign = 1 - 2 * time_reversal Q_c = self.qd.bzk_kc[Q2] d_c = self.sign * np.dot(self.U_cc, q_c) - Q_c assert np.allclose(d_c.round(), d_c) yield pd, W, Q_c done.add(Q2)