def get_ibz_q_points(self, bz_k_points): # Get all q-points all_qs = [] for k1 in bz_k_points: for k2 in bz_k_points: all_qs.append(k1-k2) all_qs = np.array(all_qs) # Fold q-points into Brillouin zone all_qs[np.where(all_qs > 0.501)] -= 1. all_qs[np.where(all_qs < -0.499)] += 1. # Make list of non-identical q-points in full BZ bz_qs = [all_qs[0]] for q_a in all_qs: q_in_list = False for q_b in bz_qs: if (abs(q_a[0]-q_b[0]) < 0.01 and abs(q_a[1]-q_b[1]) < 0.01 and abs(q_a[2]-q_b[2]) < 0.01): q_in_list = True break if q_in_list == False: bz_qs.append(q_a) self.bz_q_points = bz_qs # Obtain q-points and weights in the irreducible part of the BZ kpt_descriptor = KPointDescriptor(bz_qs, self.nspins) kpt_descriptor.set_symmetry(self.atoms, self.setups, usesymm=True) ibz_q_points = kpt_descriptor.ibzk_kc q_weights = kpt_descriptor.weight_k return ibz_q_points, q_weights
def create_kpoint_descriptor(self, nspins): par = self.parameters bzkpts_kc = kpts2ndarray(par.kpts, self.atoms) kpt_refine = par.experimental.get('kpt_refine') if kpt_refine is None: kd = KPointDescriptor(bzkpts_kc, nspins) self.timer.start('Set symmetry') kd.set_symmetry(self.atoms, self.symmetry, comm=self.world) self.timer.stop('Set symmetry') else: self.timer.start('Set k-point refinement') kd = create_kpoint_descriptor_with_refinement(kpt_refine, bzkpts_kc, nspins, self.atoms, self.symmetry, comm=self.world, timer=self.timer) self.timer.stop('Set k-point refinement') # Update quantities which might have changed, if symmetry # was changed self.symmetry = kd.symmetry self.setups.set_symmetry(kd.symmetry) self.log(kd) return kd
def get_ibz_q_points(self, bz_k_points): # Get all q-points all_qs = [] for k1 in bz_k_points: for k2 in bz_k_points: all_qs.append(k1 - k2) all_qs = np.array(all_qs) # Fold q-points into Brillouin zone all_qs[np.where(all_qs > 0.501)] -= 1. all_qs[np.where(all_qs < -0.499)] += 1. # Make list of non-identical q-points in full BZ bz_qs = [all_qs[0]] for q_a in all_qs: q_in_list = False for q_b in bz_qs: if (abs(q_a[0] - q_b[0]) < 0.01 and abs(q_a[1] - q_b[1]) < 0.01 and abs(q_a[2] - q_b[2]) < 0.01): q_in_list = True break if q_in_list == False: bz_qs.append(q_a) self.bz_q_points = bz_qs # Obtain q-points and weights in the irreducible part of the BZ kpt_descriptor = KPointDescriptor(bz_qs, self.nspins) kpt_descriptor.set_symmetry(self.atoms, self.setups, usesymm=True) ibz_q_points = kpt_descriptor.ibzk_kc q_weights = kpt_descriptor.weight_k return ibz_q_points, q_weights
def ibz2bz(self, atoms): """Transform wave functions in IBZ to the full BZ.""" assert self.kd.comm.size == 1 # New k-point descriptor for full BZ: kd = KPointDescriptor(self.kd.bzk_kc, nspins=self.nspins) kd.set_symmetry(atoms, self.setups, usesymm=None) kd.set_communicator(serial_comm) self.pt = LFC(self.gd, [setup.pt_j for setup in self.setups], kd, dtype=self.dtype) self.pt.set_positions(atoms.get_scaled_positions()) self.initialize_wave_functions_from_restart_file() weight = 2.0 / kd.nspins / kd.nbzkpts # Build new list of k-points: kpt_u = [] for s in range(self.nspins): for k in range(kd.nbzkpts): # Index of symmetry related point in the IBZ ik = self.kd.bz2ibz_k[k] r, u = self.kd.get_rank_and_index(s, ik) assert r == 0 kpt = self.kpt_u[u] phase_cd = np.exp(2j * np.pi * self.gd.sdisp_cd * kd.bzk_kc[k, :, np.newaxis]) # New k-point: kpt2 = KPoint(weight, s, k, k, phase_cd) kpt2.f_n = kpt.f_n / kpt.weight / kd.nbzkpts * 2 / self.nspins kpt2.eps_n = kpt.eps_n.copy() # Transform wave functions using symmetry operation: Psit_nG = self.gd.collect(kpt.psit_nG) if Psit_nG is not None: Psit_nG = Psit_nG.copy() for Psit_G in Psit_nG: Psit_G[:] = self.kd.transform_wave_function(Psit_G, k) kpt2.psit_nG = self.gd.empty(self.bd.nbands, dtype=self.dtype) self.gd.distribute(Psit_nG, kpt2.psit_nG) # Calculate PAW projections: kpt2.P_ani = self.pt.dict(len(kpt.psit_nG)) self.pt.integrate(kpt2.psit_nG, kpt2.P_ani, k) kpt_u.append(kpt2) self.kd = kd self.kpt_u = kpt_u
class UTGaussianWavefunctionSetup(UTDomainParallelSetup): __doc__ = UTDomainParallelSetup.__doc__ + """ The pseudo wavefunctions are moving gaussians centered around each atom.""" allocated = False dtype = None # Default arguments for scaled Gaussian wave _sigma0 = 2.0 #0.75 _k0_c = 2*np.pi*np.array([1/5., 1/3., 0.]) def setUp(self): UTDomainParallelSetup.setUp(self) for virtvar in ['dtype']: assert getattr(self,virtvar) is not None, 'Virtual "%s"!' % virtvar # Create randomized atoms self.atoms = create_random_atoms(self.gd, 5) # also tested: 10xNH3/BDA # XXX DEBUG START if False: from ase import view view(self.atoms*(1+2*self.gd.pbc_c)) # XXX DEBUG END # Do we agree on the atomic positions? pos_ac = self.atoms.get_positions() pos_rac = np.empty((world.size,)+pos_ac.shape, pos_ac.dtype) world.all_gather(pos_ac, pos_rac) if (pos_rac-pos_rac[world.rank,...][np.newaxis,...]).any(): raise RuntimeError('Discrepancy in atomic positions detected.') # Create setups for atoms self.Z_a = self.atoms.get_atomic_numbers() self.setups = Setups(self.Z_a, p.setups, p.basis, p.lmax, xc) # K-point descriptor bzk_kc = np.array([[0, 0, 0]], dtype=float) self.kd = KPointDescriptor(bzk_kc, 1) self.kd.set_symmetry(self.atoms, self.setups, usesymm=True) self.kd.set_communicator(self.kpt_comm) # Create gamma-point dummy wavefunctions self.wfs = FDWFS(self.gd, self.bd, self.kd, self.setups, self.dtype) spos_ac = self.atoms.get_scaled_positions() % 1.0 self.wfs.set_positions(spos_ac) self.pt = self.wfs.pt # XXX shortcut ## Also create pseudo partial waveves #from gpaw.lfc import LFC #self.phit = LFC(self.gd, [setup.phit_j for setup in self.setups], \ # self.kpt_comm, dtype=self.dtype) #self.phit.set_positions(spos_ac) self.r_cG = None self.buf_G = None self.psit_nG = None self.allocate() def tearDown(self): UTDomainParallelSetup.tearDown(self) del self.r_cG, self.buf_G, self.psit_nG del self.pt, self.setups, self.atoms self.allocated = False def allocate(self): self.r_cG = self.gd.get_grid_point_coordinates() cell_cv = self.atoms.get_cell() / Bohr assert np.abs(cell_cv-self.gd.cell_cv).max() < 1e-9 center_c = 0.5*cell_cv.diagonal() self.buf_G = self.gd.empty(dtype=self.dtype) self.psit_nG = self.gd.empty(self.bd.mynbands, dtype=self.dtype) for myn,psit_G in enumerate(self.psit_nG): n = self.bd.global_index(myn) psit_G[:] = self.get_scaled_gaussian_wave(center_c, scale=10+2j*n) k_c = 2*np.pi*np.array([1/2., -1/7., 0.]) for pos_c in self.atoms.get_positions() / Bohr: sigma = self._sigma0/(1+np.sum(pos_c**2))**0.5 psit_G += self.get_scaled_gaussian_wave(pos_c, sigma, k_c, n+5j) self.allocated = True def get_scaled_gaussian_wave(self, pos_c, sigma=None, k_c=None, scale=None): if sigma is None: sigma = self._sigma0 if k_c is None: k_c = self._k0_c if scale is None: A = None else: # 4*pi*int(exp(-r^2/(2*w^2))^2*r^2, r=0...infinity)= w^3*pi^(3/2) # = scale/A^2 -> A = scale*(sqrt(Pi)*w)^(-3/2) hence int -> scale^2 A = scale/(sigma*(np.pi)**0.5)**1.5 return gaussian_wave(self.r_cG, pos_c, sigma, k_c, A, self.dtype, self.buf_G) def check_and_plot(self, P_ani, P0_ani, digits, keywords=''): # Collapse into viewable matrices P_In = self.wfs.collect_projections(P_ani) P0_In = self.wfs.collect_projections(P0_ani) # Construct fingerprint of input matrices for comparison fingerprint = np.array([md5_array(P_In, numeric=True), md5_array(P0_In, numeric=True)]) # Compare fingerprints across all processors fingerprints = np.empty((world.size, 2), np.int64) world.all_gather(fingerprint, fingerprints) if fingerprints.ptp(0).any(): raise RuntimeError('Distributed matrices are not identical!') # If assertion fails, catch temporarily while plotting, then re-raise try: self.assertAlmostEqual(np.abs(P_In-P0_In).max(), 0, digits) except AssertionError: if world.rank == 0 and mpl is not None: from matplotlib.figure import Figure fig = Figure() ax = fig.add_axes([0.0, 0.1, 1.0, 0.83]) ax.set_title(self.__class__.__name__) im = ax.imshow(np.abs(P_In-P0_In), interpolation='nearest') fig.colorbar(im) fig.text(0.5, 0.05, 'Keywords: ' + keywords, \ horizontalalignment='center', verticalalignment='top') from matplotlib.backends.backend_agg import FigureCanvasAgg img = 'ut_invops_%s_%s.png' % (self.__class__.__name__, \ '_'.join(keywords.split(','))) FigureCanvasAgg(fig).print_figure(img.lower(), dpi=90) raise # ================================= def test_projection_linearity(self): kpt = self.wfs.kpt_u[0] Q_ani = self.pt.dict(self.bd.mynbands) self.pt.integrate(self.psit_nG, Q_ani, q=kpt.q) for Q_ni in Q_ani.values(): self.assertTrue(Q_ni.dtype == self.dtype) P0_ani = dict([(a,Q_ni.copy()) for a,Q_ni in Q_ani.items()]) self.pt.add(self.psit_nG, Q_ani, q=kpt.q) self.pt.integrate(self.psit_nG, P0_ani, q=kpt.q) #rank_a = self.gd.get_ranks_from_positions(spos_ac) #my_atom_indices = np.argwhere(self.gd.comm.rank == rank_a).ravel() # ~ a ~ a' #TODO XXX should fix PairOverlap-ish stuff for < p | phi > overlaps # i i' #spos_ac = self.pt.spos_ac # NewLFC doesn't have this spos_ac = self.atoms.get_scaled_positions() % 1.0 gpo = GridPairOverlap(self.gd, self.setups) B_aa = gpo.calculate_overlaps(spos_ac, self.pt) # Compare fingerprints across all processors fingerprint = np.array([md5_array(B_aa, numeric=True)]) fingerprints = np.empty(world.size, np.int64) world.all_gather(fingerprint, fingerprints) if fingerprints.ptp(0).any(): raise RuntimeError('Distributed matrices are not identical!') P_ani = dict([(a,Q_ni.copy()) for a,Q_ni in Q_ani.items()]) for a1 in range(len(self.atoms)): if a1 in P_ani.keys(): P_ni = P_ani[a1] else: # Atom a1 is not in domain so allocate a temporary buffer P_ni = np.zeros((self.bd.mynbands,self.setups[a1].ni,), dtype=self.dtype) for a2, Q_ni in Q_ani.items(): # B_aa are the projector overlaps across atomic pairs B_ii = gpo.extract_atomic_pair_matrix(B_aa, a1, a2) P_ni += np.dot(Q_ni, B_ii.T) #sum over a2 and last i in B_ii self.gd.comm.sum(P_ni) self.check_and_plot(P_ani, P0_ani, 8, 'projection,linearity') def test_extrapolate_overlap(self): kpt = self.wfs.kpt_u[0] ppo = ProjectorPairOverlap(self.wfs, self.atoms) # Compare fingerprints across all processors fingerprint = np.array([md5_array(ppo.B_aa, numeric=True)]) fingerprints = np.empty(world.size, np.int64) world.all_gather(fingerprint, fingerprints) if fingerprints.ptp(0).any(): raise RuntimeError('Distributed matrices are not identical!') work_nG = np.empty_like(self.psit_nG) P_ani = ppo.apply(self.psit_nG, work_nG, self.wfs, kpt, \ calculate_P_ani=True, extrapolate_P_ani=True) P0_ani = self.pt.dict(self.bd.mynbands) self.pt.integrate(work_nG, P0_ani, kpt.q) del work_nG self.check_and_plot(P_ani, P0_ani, 11, 'extrapolate,overlap') def test_extrapolate_inverse(self): kpt = self.wfs.kpt_u[0] ppo = ProjectorPairOverlap(self.wfs, self.atoms) # Compare fingerprints across all processors fingerprint = np.array([md5_array(ppo.B_aa, numeric=True)]) fingerprints = np.empty(world.size, np.int64) world.all_gather(fingerprint, fingerprints) if fingerprints.ptp(0).any(): raise RuntimeError('Distributed matrices are not identical!') work_nG = np.empty_like(self.psit_nG) P_ani = ppo.apply_inverse(self.psit_nG, work_nG, self.wfs, kpt, \ calculate_P_ani=True, extrapolate_P_ani=True) P0_ani = self.pt.dict(self.bd.mynbands) self.pt.integrate(work_nG, P0_ani, kpt.q) del work_nG self.check_and_plot(P_ani, P0_ani, 11, 'extrapolate,inverse') def test_overlap_inverse_after(self): kpt = self.wfs.kpt_u[0] kpt.P_ani = self.pt.dict(self.bd.mynbands) ppo = ProjectorPairOverlap(self.wfs, self.atoms) # Compare fingerprints across all processors fingerprint = np.array([md5_array(ppo.B_aa, numeric=True)]) fingerprints = np.empty(world.size, np.int64) world.all_gather(fingerprint, fingerprints) if fingerprints.ptp(0).any(): raise RuntimeError('Distributed matrices are not identical!') work_nG = np.empty_like(self.psit_nG) self.pt.integrate(self.psit_nG, kpt.P_ani, kpt.q) P0_ani = dict([(a,P_ni.copy()) for a,P_ni in kpt.P_ani.items()]) ppo.apply(self.psit_nG, work_nG, self.wfs, kpt, calculate_P_ani=False) res_nG = np.empty_like(self.psit_nG) ppo.apply_inverse(work_nG, res_nG, self.wfs, kpt, calculate_P_ani=True) del work_nG P_ani = self.pt.dict(self.bd.mynbands) self.pt.integrate(res_nG, P_ani, kpt.q) abserr = np.empty(1, dtype=float) for n in range(self.nbands): band_rank, myn = self.bd.who_has(n) if band_rank == self.bd.comm.rank: abserr[:] = np.abs(self.psit_nG[myn] - res_nG[myn]).max() self.gd.comm.max(abserr) self.bd.comm.broadcast(abserr, band_rank) self.assertAlmostEqual(abserr.item(), 0, 10) self.check_and_plot(P_ani, P0_ani, 10, 'overlap,inverse,after') def test_overlap_inverse_before(self): kpt = self.wfs.kpt_u[0] kpt.P_ani = self.pt.dict(self.bd.mynbands) ppo = ProjectorPairOverlap(self.wfs, self.atoms) # Compare fingerprints across all processors fingerprint = np.array([md5_array(ppo.B_aa, numeric=True)]) fingerprints = np.empty(world.size, np.int64) world.all_gather(fingerprint, fingerprints) if fingerprints.ptp(0).any(): raise RuntimeError('Distributed matrices are not identical!') work_nG = np.empty_like(self.psit_nG) self.pt.integrate(self.psit_nG, kpt.P_ani, kpt.q) P0_ani = dict([(a,P_ni.copy()) for a,P_ni in kpt.P_ani.items()]) ppo.apply_inverse(self.psit_nG, work_nG, self.wfs, kpt, calculate_P_ani=False) res_nG = np.empty_like(self.psit_nG) ppo.apply(work_nG, res_nG, self.wfs, kpt, calculate_P_ani=True) del work_nG P_ani = self.pt.dict(self.bd.mynbands) self.pt.integrate(res_nG, P_ani, kpt.q) abserr = np.empty(1, dtype=float) for n in range(self.nbands): band_rank, myn = self.bd.who_has(n) if band_rank == self.bd.comm.rank: abserr[:] = np.abs(self.psit_nG[myn] - res_nG[myn]).max() self.gd.comm.max(abserr) self.bd.comm.broadcast(abserr, band_rank) self.assertAlmostEqual(abserr.item(), 0, 10) self.check_and_plot(P_ani, P0_ani, 10, 'overlap,inverse,before')
def create_wave_functions(self, mode, realspace, nspins, nbands, nao, nvalence, setups, magmom_a, cell_cv, pbc_c): par = self.parameters bzkpts_kc = kpts2ndarray(par.kpts, self.atoms) kd = KPointDescriptor(bzkpts_kc, nspins) self.timer.start('Set symmetry') kd.set_symmetry(self.atoms, self.symmetry, comm=self.world) self.timer.stop('Set symmetry') self.log(kd) parallelization = mpi.Parallelization(self.world, nspins * kd.nibzkpts) parsize_kpt = self.parallel['kpt'] parsize_domain = self.parallel['domain'] parsize_bands = self.parallel['band'] ndomains = None if parsize_domain is not None: ndomains = np.prod(parsize_domain) if mode.name == 'pw': if ndomains is not None and ndomains > 1: raise ValueError('Planewave mode does not support ' 'domain decomposition.') ndomains = 1 parallelization.set(kpt=parsize_kpt, domain=ndomains, band=parsize_bands) comms = parallelization.build_communicators() domain_comm = comms['d'] kpt_comm = comms['k'] band_comm = comms['b'] kptband_comm = comms['D'] domainband_comm = comms['K'] self.comms = comms if par.gpts is not None: if par.h is not None: raise ValueError("""You can't use both "gpts" and "h"!""") N_c = np.array(par.gpts) else: h = par.h if h is not None: h /= Bohr N_c = get_number_of_grid_points(cell_cv, h, mode, realspace, kd.symmetry) self.symmetry.check_grid(N_c) kd.set_communicator(kpt_comm) parstride_bands = self.parallel['stridebands'] # Unfortunately we need to remember that we adjusted the # number of bands so we can print a warning if it differs # from the number specified by the user. (The number can # be inferred from the input parameters, but it's tricky # because we allow negative numbers) self.nbands_parallelization_adjustment = -nbands % band_comm.size nbands += self.nbands_parallelization_adjustment bd = BandDescriptor(nbands, band_comm, parstride_bands) # Construct grid descriptor for coarse grids for wave functions: gd = self.create_grid_descriptor(N_c, cell_cv, pbc_c, domain_comm, parsize_domain) if hasattr(self, 'time') or mode.force_complex_dtype: dtype = complex else: if kd.gamma: dtype = float else: dtype = complex wfs_kwargs = dict(gd=gd, nvalence=nvalence, setups=setups, bd=bd, dtype=dtype, world=self.world, kd=kd, kptband_comm=kptband_comm, timer=self.timer) if self.parallel['sl_auto']: # Choose scalapack parallelization automatically for key, val in self.parallel.items(): if (key.startswith('sl_') and key != 'sl_auto' and val is not None): raise ValueError("Cannot use 'sl_auto' together " "with '%s'" % key) max_scalapack_cpus = bd.comm.size * gd.comm.size nprow = max_scalapack_cpus npcol = 1 # Get a sort of reasonable number of columns/rows while npcol < nprow and nprow % 2 == 0: npcol *= 2 nprow //= 2 assert npcol * nprow == max_scalapack_cpus # ScaLAPACK creates trouble if there aren't at least a few # whole blocks; choose block size so there will always be # several blocks. This will crash for small test systems, # but so will ScaLAPACK in any case blocksize = min(-(-nbands // 4), 64) sl_default = (nprow, npcol, blocksize) else: sl_default = self.parallel['sl_default'] if mode.name == 'lcao': # Layouts used for general diagonalizer sl_lcao = self.parallel['sl_lcao'] if sl_lcao is None: sl_lcao = sl_default lcaoksl = get_KohnSham_layouts(sl_lcao, 'lcao', gd, bd, domainband_comm, dtype, nao=nao, timer=self.timer) self.wfs = mode(lcaoksl, **wfs_kwargs) elif mode.name == 'fd' or mode.name == 'pw': # buffer_size keyword only relevant for fdpw buffer_size = self.parallel['buffer_size'] # Layouts used for diagonalizer sl_diagonalize = self.parallel['sl_diagonalize'] if sl_diagonalize is None: sl_diagonalize = sl_default diagksl = get_KohnSham_layouts( sl_diagonalize, 'fd', # XXX # choice of key 'fd' not so nice gd, bd, domainband_comm, dtype, buffer_size=buffer_size, timer=self.timer) # Layouts used for orthonormalizer sl_inverse_cholesky = self.parallel['sl_inverse_cholesky'] if sl_inverse_cholesky is None: sl_inverse_cholesky = sl_default if sl_inverse_cholesky != sl_diagonalize: message = 'sl_inverse_cholesky != sl_diagonalize ' \ 'is not implemented.' raise NotImplementedError(message) orthoksl = get_KohnSham_layouts(sl_inverse_cholesky, 'fd', gd, bd, domainband_comm, dtype, buffer_size=buffer_size, timer=self.timer) # Use (at most) all available LCAO for initialization lcaonbands = min(nbands, nao // band_comm.size * band_comm.size) try: lcaobd = BandDescriptor(lcaonbands, band_comm, parstride_bands) except RuntimeError: initksl = None else: # Layouts used for general diagonalizer # (LCAO initialization) sl_lcao = self.parallel['sl_lcao'] if sl_lcao is None: sl_lcao = sl_default initksl = get_KohnSham_layouts(sl_lcao, 'lcao', gd, lcaobd, domainband_comm, dtype, nao=nao, timer=self.timer) self.wfs = mode(diagksl, orthoksl, initksl, **wfs_kwargs) else: self.wfs = mode(self, **wfs_kwargs) self.log(self.wfs, '\n')
class PhononCalculator: """This class defines the interface for phonon calculations.""" def __init__(self, calc, gamma=True, symmetry=False, e_ph=False, communicator=serial_comm): """Inititialize class with a list of atoms. The atoms object must contain a converged ground-state calculation. The set of q-vectors in which the dynamical matrix will be calculated is determined from the ``symmetry`` kwarg. For now, only time-reversal symmetry is used to generate the irrecducible BZ. Add a little note on parallelization strategy here. Parameters ---------- calc: str or Calculator Calculator containing a ground-state calculation. gamma: bool Gamma-point calculation with respect to the q-vector of the dynamical matrix. When ``False``, the Monkhorst-Pack grid from the ground-state calculation is used. symmetry: bool Use symmetries to reduce the q-vectors of the dynamcial matrix (None, False or True). The different options are equivalent to the old style options in a ground-state calculation (see usesymm). e_ph: bool Save the derivative of the effective potential. communicator: Communicator Communicator for parallelization over k-points and real-space domain. """ # XXX assert symmetry in [None, False], "Spatial symmetries not allowed yet" if isinstance(calc, str): self.calc = GPAW(calc, communicator=serial_comm, txt=None) else: self.calc = calc cell_cv = self.calc.atoms.get_cell() setups = self.calc.wfs.setups # XXX - no clue how to get magmom - ignore it for the moment # m_av = magmom_av.round(decimals=3) # round off # id_a = zip(setups.id_a, *m_av.T) id_a = setups.id_a if symmetry is None: self.symmetry = Symmetry(id_a, cell_cv, point_group=False, time_reversal=False) else: self.symmetry = Symmetry(id_a, cell_cv, point_group=False, time_reversal=True) # Make sure localized functions are initialized self.calc.set_positions() # Note that this under some circumstances (e.g. when called twice) # allocates a new array for the P_ani coefficients !! # Store useful objects self.atoms = self.calc.get_atoms() # Get rid of ``calc`` attribute self.atoms.calc = None # Boundary conditions pbc_c = self.calc.atoms.get_pbc() if np.all(pbc_c == False): self.gamma = True self.dtype = float kpts = None # Multigrid Poisson solver poisson_solver = PoissonSolver() else: if gamma: self.gamma = True self.dtype = float kpts = None else: self.gamma = False self.dtype = complex # Get k-points from ground-state calculation kpts = self.calc.input_parameters.kpts # FFT Poisson solver poisson_solver = FFTPoissonSolver(dtype=self.dtype) # K-point descriptor for the q-vectors of the dynamical matrix # Note, no explicit parallelization here. self.kd = KPointDescriptor(kpts, 1) self.kd.set_symmetry(self.atoms, self.symmetry) self.kd.set_communicator(serial_comm) # Number of occupied bands nvalence = self.calc.wfs.nvalence nbands = nvalence // 2 + nvalence % 2 assert nbands <= self.calc.wfs.bd.nbands # Extract other useful objects # Ground-state k-point descriptor - used for the k-points in the # ResponseCalculator # XXX replace communicators when ready to parallelize kd_gs = self.calc.wfs.kd gd = self.calc.density.gd kpt_u = self.calc.wfs.kpt_u setups = self.calc.wfs.setups dtype_gs = self.calc.wfs.dtype # WaveFunctions wfs = WaveFunctions(nbands, kpt_u, setups, kd_gs, gd, dtype=dtype_gs) # Linear response calculator self.response_calc = ResponseCalculator(self.calc, wfs, dtype=self.dtype) # Phonon perturbation self.perturbation = PhononPerturbation(self.calc, self.kd, poisson_solver, dtype=self.dtype) # Dynamical matrix self.dyn = DynamicalMatrix(self.atoms, self.kd, dtype=self.dtype) # Electron-phonon couplings if e_ph: self.e_ph = ElectronPhononCoupling(self.atoms, gd, self.kd, dtype=self.dtype) else: self.e_ph = None # Initialization flag self.initialized = False # Parallel communicator for parallelization over kpts and domain self.comm = communicator def initialize(self): """Initialize response calculator and perturbation.""" # Get scaled atomic positions spos_ac = self.atoms.get_scaled_positions() self.perturbation.initialize(spos_ac) self.response_calc.initialize(spos_ac) self.initialized = True def __getstate__(self): """Method used when pickling. Bound method attributes cannot be pickled and must therefore be deleted before an instance is dumped to file. """ # Get state of object and take care of troublesome attributes state = dict(self.__dict__) state['kd'].__dict__['comm'] = serial_comm state.pop('calc') state.pop('perturbation') state.pop('response_calc') return state def run(self, qpts_q=None, clean=False, name=None, path=None): """Run calculation for atomic displacements and update matrix. Parameters ---------- qpts: List List of q-points indices for which the dynamical matrix will be calculated (only temporary). """ if not self.initialized: self.initialize() if self.gamma: qpts_q = [0] elif qpts_q is None: qpts_q = range(self.kd.nibzkpts) else: assert isinstance(qpts_q, list) # Update name and path attributes self.set_name_and_path(name=name, path=path) # Get string template for filenames filename_str = self.get_filename_string() # Delay the ranks belonging to the same k-point/domain decomposition # equally time.sleep(rank // self.comm.size) # XXX Make a single ground_state_contributions member function # Ground-state contributions to the force constants self.dyn.density_ground_state(self.calc) # self.dyn.wfs_ground_state(self.calc, self.response_calc) # Calculate linear response wrt q-vectors and displacements of atoms for q in qpts_q: if not self.gamma: self.perturbation.set_q(q) # First-order contributions to the force constants for a in self.dyn.indices: for v in [0, 1, 2]: # Check if the calculation has already been done filename = filename_str % (q, a, v) # Wait for all sub-ranks to enter self.comm.barrier() if os.path.isfile(os.path.join(self.path, filename)): continue if self.comm.rank == 0: fd = open(os.path.join(self.path, filename), 'w') # Wait for all sub-ranks here self.comm.barrier() components = ['x', 'y', 'z'] symbols = self.atoms.get_chemical_symbols() print("q-vector index: %i" % q) print("Atom index: %i" % a) print("Atomic symbol: %s" % symbols[a]) print("Component: %s" % components[v]) # Set atom and cartesian component of perturbation self.perturbation.set_av(a, v) # Calculate linear response self.response_calc(self.perturbation) # Calculate row of the matrix of force constants self.dyn.calculate_row(self.perturbation, self.response_calc) # Write force constants to file if self.comm.rank == 0: self.dyn.write(fd, q, a, v) fd.close() # Store effective potential derivative if self.e_ph is not None: v1_eff_G = self.perturbation.v1_G + \ self.response_calc.vHXC1_G self.e_ph.v1_eff_qavG.append(v1_eff_G) # Wait for the file-writing rank here self.comm.barrier() # XXX # Check that all files are valid and collect in a single file # Remove the files if clean: self.clean() def get_atoms(self): """Return atoms.""" return self.atoms def get_dynamical_matrix(self): """Return reference to ``dyn`` attribute.""" return self.dyn def get_filename_string(self): """Return string template for force constant filenames.""" name_str = (self.name + '.' + 'q_%%0%ii_' % len(str(self.kd.nibzkpts)) + 'a_%%0%ii_' % len(str(len(self.atoms))) + 'v_%i' + '.pckl') return name_str def set_atoms(self, atoms): """Set atoms to be included in the calculation. Parameters ---------- atoms: list Can be either a list of strings, ints or ... """ assert isinstance(atoms, list) if isinstance(atoms[0], str): assert np.all([isinstance(atom, str) for atom in atoms]) sym_a = self.atoms.get_chemical_symbols() # List for atomic indices indices = [] for type in atoms: indices.extend( [a for a, atom in enumerate(sym_a) if atom == type]) else: assert np.all([isinstance(atom, int) for atom in atoms]) indices = atoms self.dyn.set_indices(indices) def set_name_and_path(self, name=None, path=None): """Set name and path of the force constant files. name: str Base name for the files which the elements of the matrix of force constants will be written to. path: str Path specifying the directory where the files will be dumped. """ if name is None: self.name = 'phonon.' + self.atoms.get_chemical_formula() else: self.name = name # self.name += '.nibzkpts_%i' % self.kd.nibzkpts if path is None: self.path = '.' else: self.path = path # Set corresponding attributes in the ``dyn`` attribute filename_str = self.get_filename_string() self.dyn.set_name_and_path(filename_str, self.path) def clean(self): """Delete generated files.""" filename_str = self.get_filename_string() for q in range(self.kd.nibzkpts): for a in range(len(self.atoms)): for v in [0, 1, 2]: filename = filename_str % (q, a, v) if os.path.isfile(os.path.join(self.path, filename)): os.remove(filename) def band_structure(self, path_kc, modes=False, acoustic=True): """Calculate phonon dispersion along a path in the Brillouin zone. The dynamical matrix at arbitrary q-vectors is obtained by Fourier transforming the real-space matrix. In case of negative eigenvalues (squared frequency), the corresponding negative frequency is returned. Parameters ---------- path_kc: ndarray List of k-point coordinates (in units of the reciprocal lattice vectors) specifying the path in the Brillouin zone for which the dynamical matrix will be calculated. modes: bool Returns both frequencies and modes (mass scaled) when True. acoustic: bool Restore the acoustic sum-rule in the calculated force constants. """ for k_c in path_kc: assert np.all(np.asarray(k_c) <= 1.0), \ "Scaled coordinates must be given" # Assemble the dynanical matrix from calculated force constants self.dyn.assemble(acoustic=acoustic) # Get the dynamical matrix in real-space DR_lmn, R_clmn = self.dyn.real_space() # Reshape for the evaluation of the fourier sums shape = DR_lmn.shape DR_m = DR_lmn.reshape((-1, ) + shape[-2:]) R_cm = R_clmn.reshape((3, -1)) # Lists for frequencies and modes along path omega_kn = [] u_kn = [] # Number of atoms included N = len(self.dyn.get_indices()) # Mass prefactor for the normal modes m_inv_av = self.dyn.get_mass_array() for q_c in path_kc: # Evaluate fourier transform phase_m = np.exp(-2.j * pi * np.dot(q_c, R_cm)) # Dynamical matrix in unit of Ha / Bohr**2 / amu D_q = np.sum(phase_m[:, np.newaxis, np.newaxis] * DR_m, axis=0) if modes: omega2_n, u_avn = la.eigh(D_q, UPLO='L') # Sort eigenmodes according to eigenvalues (see below) and # multiply with mass prefactor u_nav = u_avn[:, omega2_n.argsort()].T.copy() * m_inv_av # Multiply with mass prefactor u_kn.append(u_nav.reshape((3 * N, -1, 3))) else: omega2_n = la.eigvalsh(D_q, UPLO='L') # Sort eigenvalues in increasing order omega2_n.sort() # Use dtype=complex to handle negative eigenvalues omega_n = np.sqrt(omega2_n.astype(complex)) # Take care of imaginary frequencies if not np.all(omega2_n >= 0.): indices = np.where(omega2_n < 0)[0] print(("WARNING, %i imaginary frequencies at " "q = (% 5.2f, % 5.2f, % 5.2f) ; (omega_q =% 5.3e*i)" % (len(indices), q_c[0], q_c[1], q_c[2], omega_n[indices][0].imag))) omega_n[indices] = -1 * np.sqrt(np.abs(omega2_n[indices].real)) omega_kn.append(omega_n.real) # Conversion factor from sqrt(Ha / Bohr**2 / amu) -> eV s = units.Hartree**0.5 * units._hbar * 1.e10 / \ (units._e * units._amu)**(0.5) / units.Bohr # Convert to eV and Ang omega_kn = s * np.asarray(omega_kn) if modes: u_kn = np.asarray(u_kn) * units.Bohr return omega_kn, u_kn return omega_kn def write_modes(self, q_c, branches=0, kT=units.kB * 300, repeat=(1, 1, 1), nimages=30, acoustic=True): """Write mode to trajectory file. The classical equipartioning theorem states that each normal mode has an average energy:: <E> = 1/2 * k_B * T = 1/2 * omega^2 * Q^2 => Q = sqrt(k_B*T) / omega at temperature T. Here, Q denotes the normal coordinate of the mode. Parameters ---------- q_c: ndarray q-vector of the modes. branches: int or list Branch index of calculated modes. kT: float Temperature in units of eV. Determines the amplitude of the atomic displacements in the modes. repeat: tuple Repeat atoms (l, m, n) times in the directions of the lattice vectors. Displacements of atoms in repeated cells carry a Bloch phase factor given by the q-vector and the cell lattice vector R_m. nimages: int Number of images in an oscillation. """ if isinstance(branches, int): branch_n = [branches] else: branch_n = list(branches) # Calculate modes omega_n, u_n = self.band_structure([q_c], modes=True, acoustic=acoustic) # Repeat atoms atoms = self.atoms * repeat pos_mav = atoms.positions.copy() # Total number of unit cells M = np.prod(repeat) # Corresponding lattice vectors R_m R_cm = np.indices(repeat[::-1]).reshape(3, -1)[::-1] # Bloch phase phase_m = np.exp(2.j * pi * np.dot(q_c, R_cm)) phase_ma = phase_m.repeat(len(self.atoms)) for n in branch_n: omega = omega_n[0, n] u_av = u_n[0, n] # .reshape((-1, 3)) # Mean displacement at high T ? u_av *= sqrt(kT / abs(omega)) mode_av = np.zeros((len(self.atoms), 3), dtype=self.dtype) indices = self.dyn.get_indices() mode_av[indices] = u_av mode_mav = (np.vstack([mode_av] * M) * phase_ma[:, np.newaxis]).real traj = Trajectory('%s.mode.%d.traj' % (self.name, n), 'w') for x in np.linspace(0, 2 * pi, nimages, endpoint=False): # XXX Is it correct to take out the sine component here ? atoms.set_positions(pos_mav + sin(x) * mode_mav) traj.write(atoms) traj.close()
def create_kpoint_descriptor(bzkpts_kc, nspins, atoms, symmetry, comm): kd = KPointDescriptor(bzkpts_kc, nspins) # self.timer.start('Set symmetry') kd.set_symmetry(atoms, symmetry, comm=comm) # self.timer.stop('Set symmetry') return kd
class Phonons(phonons.Phonons): """DFPT version of the ``Phonons`` class from ase. This class recycle the functionality from the ASE class. """ def __init__(self, atoms, kpts, symmetry): """Initialize base class and attributes. Parameters ---------- atoms: Atoms ASE atoms. kpts: tuple or list of tuples Shape of Monkhorst-Pack grid or list of k-points used in the dfpt calculation. symmetry: bool or None Symmetry parameter used in dfpt calculation. """ # Init base class with ``Atoms`` object phonons.Phonons.__init__(atoms) # Create k-point descriptor self.kd = KPointDescriptor(kpts, 1) self.kd.set_symmetry(self.atoms, self.calc.wfs.setups, symmetry) # Overwrite ``N_c`` attribute self.N_c = tuple(self.kd.N_c) # Index of the gamma point -- for the acoustic sum-rule self.gamma_index = None if self.kd.gamma: self.gamma_index = 0 self.dtype == float else: self.dtype == comples for k, k_c in enumerate(self.kd.ibzk_kc): if np.all(k_c == 0.): self.gamma_index = k assert self.gamma_index is not None def run(self): """Overwrite base class member function.""" raise RuntimeError, "Use only this class for post-processing" def read(self): """Read force constants from files.""" # Data structure for force constants self.C_qaavv = [dict([(a, dict([(a_, np.zeros((3, 3), dtype=dtype)) for a_ in self.indices])) for a in self.indices]) for q in range(self.kd.nibzkpts)] assert self.name is not None assert self.path is not None for q in range(self.kd.nibzkpts): for a in self.indices: for v in [0, 1, 2]: filename = self.name % (q, a, v) try: fd = open(os.path.join(self.path, filename)) except EOFError: print ("Redo file %s " % os.path.join(self.path, filename)) C_qav_a = pickle.load(fd) fd.close() for a_ in self.indices: self.C_qaavv[q][a][a_][v] = C_qav_a[a_] def assemble(self, acoustic=True): """Assemble dynamical matrix from the force constants attribute. The elements of the dynamical matrix are given by:: D_ij(q) = 1/(M_i + M_j) * C_ij(q) , where i and j are collective atomic and cartesian indices. During the assembly, various symmetries of the dynamical matrix are enforced:: 1) Hermiticity 2) Acoustic sum-rule 3) D(q) = D*(-q) Parameters ---------- acoustic: bool When True, the diagonal of the matrix of force constants is corrected to ensure that the acoustic sum-rule is fulfilled. """ # Read force constants from files self.read() # Number of atoms included N = len(self.indices) # Assemble matrix of force constants self.C_q = [] for q, C_aavv in enumerate(self.C_qaavv): C_avav = np.zeros((3*N, 3*N), dtype=self.dtype) for i, a in enumerate(self.indices): for j, a_ in enumerate(self.indices): C_avav[3*i : 3*i + 3, 3*j : 3*j + 3] += C_aavv[a][a_] self.C_q.append(C_avav) # XXX Figure out in which order the corrections should be done # Make C(q) Hermitian for C in self.C_q: C *= 0.5 C += C.T.conj() # Get matrix of force constants in the Gamma-point (is real!) C_gamma = self.C_q[self.gamma_index].real # Make Gamma-component real self.C_q[self.gamma_index] = C_gamma.copy() # Apply acoustic sum-rule if requested if acoustic: # Correct atomic diagonal for each q-vector for C in self.C_q: for a in range(N): for a_ in range(N): C[3*a : 3*a + 3, 3*a : 3*a + 3] -= \ C_gamma[3*a: 3*a+3, 3*a_: 3*a_+3] # Check sum-rule for Gamma-component in debug mode if debug: C = self.C_q[self.gamma_index] assert np.all(np.sum(C.reshape((3*N, N, 3)), axis=1) < 1e-15) # Move this bit to an ``unfold`` member function # XXX Time-reversal symmetry C_q = np.asarray(self.C_q) if self.kd.nibzkpts != self.kd.nbzkpts: self.D_k = np.concatenate((C_q[:0:-1].conjugate(), C_q)) else: self.D_k = 0.5 * C_q self.D_k += self.D_k[::-1].conjugate() # Mass prefactor for the dynamical matrix m = self.atoms.get_masses() self.m_inv_av = np.repeat(m[self.indices]**-0.5, 3) M_avav = self.m_inv_av[:, np.newaxis] * self.m_inv_av for C in self.D_k: C *= M_avav self.assembled = True def real_space(self): """Fourier transform the dynamical matrix to real-space.""" if not self.assembled: self.assemble() # Shape of q-point grid N_c = self.N_c # Reshape before Fourier transforming shape = self.D_k.shape Dq_lmn = self.D_k.reshape(N_c + shape[1:]) DR_lmn = fft.ifftn(fft.ifftshift(Dq_lmn, axes=(0, 1, 2)), axes=(0, 1, 2)) if debug: # Check that D_R is real enough assert np.all(DR_lmn.imag < 1e-8) DR_lmn = DR_lmn.real # Corresponding R_m vectors in units of the basis vectors R_cm = np.indices(N_c).reshape(3, -1) N1_c = np.array(N_c)[:, np.newaxis] R_cm += N1_c // 2 R_cm %= N1_c R_cm -= N1_c // 2 R_clmn = R_cm.reshape((3,) + N_c) return DR_lmn, R_clmn def fourier_interpolate(self, N_c): """Fourier interpolate dynamical matrix onto a finer q-vector grid.""" # Move this member function to the ASE class raise NotImplementedError
class PhononCalculator: """This class defines the interface for phonon calculations.""" def __init__(self, calc, gamma=True, symmetry=False, e_ph=False, communicator=serial_comm): """Inititialize class with a list of atoms. The atoms object must contain a converged ground-state calculation. The set of q-vectors in which the dynamical matrix will be calculated is determined from the ``symmetry`` kwarg. For now, only time-reversal symmetry is used to generate the irrecducible BZ. Add a little note on parallelization strategy here. Parameters ---------- calc: str or Calculator Calculator containing a ground-state calculation. gamma: bool Gamma-point calculation with respect to the q-vector of the dynamical matrix. When ``False``, the Monkhorst-Pack grid from the ground-state calculation is used. symmetry: bool Use symmetries to reduce the q-vectors of the dynamcial matrix (None, False or True). The different options are equivalent to the options in a ground-state calculation. e_ph: bool Save the derivative of the effective potential. communicator: Communicator Communicator for parallelization over k-points and real-space domain. """ # XXX assert symmetry in [None, False], "Spatial symmetries not allowed yet" self.symmetry = symmetry if isinstance(calc, str): self.calc = GPAW(calc, communicator=serial_comm, txt=None) else: self.calc = calc # Make sure localized functions are initialized self.calc.set_positions() # Note that this under some circumstances (e.g. when called twice) # allocates a new array for the P_ani coefficients !! # Store useful objects self.atoms = self.calc.get_atoms() # Get rid of ``calc`` attribute self.atoms.calc = None # Boundary conditions pbc_c = self.calc.atoms.get_pbc() if np.all(pbc_c == False): self.gamma = True self.dtype = float kpts = None # Multigrid Poisson solver poisson_solver = PoissonSolver() else: if gamma: self.gamma = True self.dtype = float kpts = None else: self.gamma = False self.dtype = complex # Get k-points from ground-state calculation kpts = self.calc.input_parameters.kpts # FFT Poisson solver poisson_solver = FFTPoissonSolver(dtype=self.dtype) # K-point descriptor for the q-vectors of the dynamical matrix # Note, no explicit parallelization here. self.kd = KPointDescriptor(kpts, 1) self.kd.set_symmetry(self.atoms, self.calc.wfs.setups, usesymm=symmetry) self.kd.set_communicator(serial_comm) # Number of occupied bands nvalence = self.calc.wfs.nvalence nbands = nvalence / 2 + nvalence % 2 assert nbands <= self.calc.wfs.bd.nbands # Extract other useful objects # Ground-state k-point descriptor - used for the k-points in the # ResponseCalculator # XXX replace communicators when ready to parallelize kd_gs = self.calc.wfs.kd gd = self.calc.density.gd kpt_u = self.calc.wfs.kpt_u setups = self.calc.wfs.setups dtype_gs = self.calc.wfs.dtype # WaveFunctions wfs = WaveFunctions(nbands, kpt_u, setups, kd_gs, gd, dtype=dtype_gs) # Linear response calculator self.response_calc = ResponseCalculator(self.calc, wfs, dtype=self.dtype) # Phonon perturbation self.perturbation = PhononPerturbation(self.calc, self.kd, poisson_solver, dtype=self.dtype) # Dynamical matrix self.dyn = DynamicalMatrix(self.atoms, self.kd, dtype=self.dtype) # Electron-phonon couplings if e_ph: self.e_ph = ElectronPhononCoupling(self.atoms, gd, self.kd, dtype=self.dtype) else: self.e_ph = None # Initialization flag self.initialized = False # Parallel communicator for parallelization over kpts and domain self.comm = communicator def initialize(self): """Initialize response calculator and perturbation.""" # Get scaled atomic positions spos_ac = self.atoms.get_scaled_positions() self.perturbation.initialize(spos_ac) self.response_calc.initialize(spos_ac) self.initialized = True def __getstate__(self): """Method used when pickling. Bound method attributes cannot be pickled and must therefore be deleted before an instance is dumped to file. """ # Get state of object and take care of troublesome attributes state = dict(self.__dict__) state['kd'].__dict__['comm'] = serial_comm state.pop('calc') state.pop('perturbation') state.pop('response_calc') return state def run(self, qpts_q=None, clean=False, name=None, path=None): """Run calculation for atomic displacements and update matrix. Parameters ---------- qpts: List List of q-points indices for which the dynamical matrix will be calculated (only temporary). """ if not self.initialized: self.initialize() if self.gamma: qpts_q = [0] elif qpts_q is None: qpts_q = range(self.kd.nibzkpts) else: assert isinstance(qpts_q, list) # Update name and path attributes self.set_name_and_path(name=name, path=path) # Get string template for filenames filename_str = self.get_filename_string() # Delay the ranks belonging to the same k-point/domain decomposition # equally time.sleep(rank // self.comm.size) # XXX Make a single ground_state_contributions member function # Ground-state contributions to the force constants self.dyn.density_ground_state(self.calc) # self.dyn.wfs_ground_state(self.calc, self.response_calc) # Calculate linear response wrt q-vectors and displacements of atoms for q in qpts_q: if not self.gamma: self.perturbation.set_q(q) # First-order contributions to the force constants for a in self.dyn.indices: for v in [0, 1, 2]: # Check if the calculation has already been done filename = filename_str % (q, a, v) # Wait for all sub-ranks to enter self.comm.barrier() if os.path.isfile(os.path.join(self.path, filename)): continue if self.comm.rank == 0: fd = open(os.path.join(self.path, filename), 'w') # Wait for all sub-ranks here self.comm.barrier() components = ['x', 'y', 'z'] symbols = self.atoms.get_chemical_symbols() print "q-vector index: %i" % q print "Atom index: %i" % a print "Atomic symbol: %s" % symbols[a] print "Component: %s" % components[v] # Set atom and cartesian component of perturbation self.perturbation.set_av(a, v) # Calculate linear response self.response_calc(self.perturbation) # Calculate row of the matrix of force constants self.dyn.calculate_row(self.perturbation, self.response_calc) # Write force constants to file if self.comm.rank == 0: self.dyn.write(fd, q, a, v) fd.close() # Store effective potential derivative if self.e_ph is not None: v1_eff_G = self.perturbation.v1_G + \ self.response_calc.vHXC1_G self.e_ph.v1_eff_qavG.append(v1_eff_G) # Wait for the file-writing rank here self.comm.barrier() # XXX # Check that all files are valid and collect in a single file # Remove the files if clean: self.clean() def get_atoms(self): """Return atoms.""" return self.atoms def get_dynamical_matrix(self): """Return reference to ``dyn`` attribute.""" return self.dyn def get_filename_string(self): """Return string template for force constant filenames.""" name_str = (self.name + '.' + 'q_%%0%ii_' % len(str(self.kd.nibzkpts)) + 'a_%%0%ii_' % len(str(len(self.atoms))) + 'v_%i' + '.pckl') return name_str def set_atoms(self, atoms): """Set atoms to be included in the calculation. Parameters ---------- atoms: list Can be either a list of strings, ints or ... """ assert isinstance(atoms, list) if isinstance(atoms[0], str): assert np.all([isinstance(atom, str) for atom in atoms]) sym_a = self.atoms.get_chemical_symbols() # List for atomic indices indices = [] for type in atoms: indices.extend([a for a, atom in enumerate(sym_a) if atom == type]) else: assert np.all([isinstance(atom, int) for atom in atoms]) indices = atoms self.dyn.set_indices(indices) def set_name_and_path(self, name=None, path=None): """Set name and path of the force constant files. name: str Base name for the files which the elements of the matrix of force constants will be written to. path: str Path specifying the directory where the files will be dumped. """ if name is None: self.name = 'phonon.' + self.atoms.get_chemical_formula() else: self.name = name # self.name += '.nibzkpts_%i' % self.kd.nibzkpts if path is None: self.path = '.' else: self.path = path # Set corresponding attributes in the ``dyn`` attribute filename_str = self.get_filename_string() self.dyn.set_name_and_path(filename_str, self.path) def clean(self): """Delete generated files.""" filename_str = self.get_filename_string() for q in range(self.kd.nibzkpts): for a in range(len(self.atoms)): for v in [0, 1, 2]: filename = filename_str % (q, a, v) if os.path.isfile(os.path.join(self.path, filename)): os.remove(filename) def band_structure(self, path_kc, modes=False, acoustic=True): """Calculate phonon dispersion along a path in the Brillouin zone. The dynamical matrix at arbitrary q-vectors is obtained by Fourier transforming the real-space matrix. In case of negative eigenvalues (squared frequency), the corresponding negative frequency is returned. Parameters ---------- path_kc: ndarray List of k-point coordinates (in units of the reciprocal lattice vectors) specifying the path in the Brillouin zone for which the dynamical matrix will be calculated. modes: bool Returns both frequencies and modes (mass scaled) when True. acoustic: bool Restore the acoustic sum-rule in the calculated force constants. """ for k_c in path_kc: assert np.all(np.asarray(k_c) <= 1.0), \ "Scaled coordinates must be given" # Assemble the dynanical matrix from calculated force constants self.dyn.assemble(acoustic=acoustic) # Get the dynamical matrix in real-space DR_lmn, R_clmn = self.dyn.real_space() # Reshape for the evaluation of the fourier sums shape = DR_lmn.shape DR_m = DR_lmn.reshape((-1,) + shape[-2:]) R_cm = R_clmn.reshape((3, -1)) # Lists for frequencies and modes along path omega_kn = [] u_kn = [] # Number of atoms included N = len(self.dyn.get_indices()) # Mass prefactor for the normal modes m_inv_av = self.dyn.get_mass_array() for q_c in path_kc: # Evaluate fourier transform phase_m = np.exp(-2.j * pi * np.dot(q_c, R_cm)) # Dynamical matrix in unit of Ha / Bohr**2 / amu D_q = np.sum(phase_m[:, np.newaxis, np.newaxis] * DR_m, axis=0) if modes: omega2_n, u_avn = la.eigh(D_q, UPLO='L') # Sort eigenmodes according to eigenvalues (see below) and # multiply with mass prefactor u_nav = u_avn[:, omega2_n.argsort()].T.copy() * m_inv_av # Multiply with mass prefactor u_kn.append(u_nav.reshape((3*N, -1, 3))) else: omega2_n = la.eigvalsh(D_q, UPLO='L') # Sort eigenvalues in increasing order omega2_n.sort() # Use dtype=complex to handle negative eigenvalues omega_n = np.sqrt(omega2_n.astype(complex)) # Take care of imaginary frequencies if not np.all(omega2_n >= 0.): indices = np.where(omega2_n < 0)[0] print ("WARNING, %i imaginary frequencies at " "q = (% 5.2f, % 5.2f, % 5.2f) ; (omega_q =% 5.3e*i)" % (len(indices), q_c[0], q_c[1], q_c[2], omega_n[indices][0].imag)) omega_n[indices] = -1 * np.sqrt(np.abs(omega2_n[indices].real)) omega_kn.append(omega_n.real) # Conversion factor from sqrt(Ha / Bohr**2 / amu) -> eV s = units.Hartree**0.5 * units._hbar * 1.e10 / \ (units._e * units._amu)**(0.5) / units.Bohr # Convert to eV and Ang omega_kn = s * np.asarray(omega_kn) if modes: u_kn = np.asarray(u_kn) * units.Bohr return omega_kn, u_kn return omega_kn def write_modes(self, q_c, branches=0, kT=units.kB*300, repeat=(1, 1, 1), nimages=30, acoustic=True): """Write mode to trajectory file. The classical equipartioning theorem states that each normal mode has an average energy:: <E> = 1/2 * k_B * T = 1/2 * omega^2 * Q^2 => Q = sqrt(k_B*T) / omega at temperature T. Here, Q denotes the normal coordinate of the mode. Parameters ---------- q_c: ndarray q-vector of the modes. branches: int or list Branch index of calculated modes. kT: float Temperature in units of eV. Determines the amplitude of the atomic displacements in the modes. repeat: tuple Repeat atoms (l, m, n) times in the directions of the lattice vectors. Displacements of atoms in repeated cells carry a Bloch phase factor given by the q-vector and the cell lattice vector R_m. nimages: int Number of images in an oscillation. """ if isinstance(branches, int): branch_n = [branches] else: branch_n = list(branches) # Calculate modes omega_n, u_n = self.band_structure([q_c], modes=True, acoustic=acoustic) # Repeat atoms atoms = self.atoms * repeat pos_mav = atoms.positions.copy() # Total number of unit cells M = np.prod(repeat) # Corresponding lattice vectors R_m R_cm = np.indices(repeat[::-1]).reshape(3, -1)[::-1] # Bloch phase phase_m = np.exp(2.j * pi * np.dot(q_c, R_cm)) phase_ma = phase_m.repeat(len(self.atoms)) for n in branch_n: omega = omega_n[0, n] u_av = u_n[0, n] # .reshape((-1, 3)) # Mean displacement at high T ? u_av *= sqrt(kT / abs(omega)) mode_av = np.zeros((len(self.atoms), 3), dtype=self.dtype) indices = self.dyn.get_indices() mode_av[indices] = u_av mode_mav = (np.vstack([mode_av]*M) * phase_ma[:, np.newaxis]).real traj = PickleTrajectory('%s.mode.%d.traj' % (self.name, n), 'w') for x in np.linspace(0, 2*pi, nimages, endpoint=False): # XXX Is it correct to take out the sine component here ? atoms.set_positions(pos_mav + sin(x) * mode_mav) traj.write(atoms) traj.close()
class HybridXC(XCFunctional): orbital_dependent = True def __init__(self, name, hybrid=None, xc=None, finegrid=False, alpha=None): """Mix standard functionals with exact exchange. name: str Name of hybrid functional. hybrid: float Fraction of exact exchange. xc: str or XCFunctional object Standard DFT functional with scaled down exchange. finegrid: boolean Use fine grid for energy functional evaluations? """ if name == 'EXX': assert hybrid is None and xc is None hybrid = 1.0 xc = XC(XCNull()) elif name == 'PBE0': assert hybrid is None and xc is None hybrid = 0.25 xc = XC('HYB_GGA_XC_PBEH') elif name == 'B3LYP': assert hybrid is None and xc is None hybrid = 0.2 xc = XC('HYB_GGA_XC_B3LYP') if isinstance(xc, str): xc = XC(xc) self.hybrid = hybrid self.xc = xc self.type = xc.type self.alpha = alpha self.exx = 0.0 XCFunctional.__init__(self, name) def get_setup_name(self): return 'PBE' def calculate_radial(self, rgd, n_sLg, Y_L, v_sg, dndr_sLg=None, rnablaY_Lv=None, tau_sg=None, dedtau_sg=None): return self.xc.calculate_radial(rgd, n_sLg, Y_L, v_sg, dndr_sLg, rnablaY_Lv) def initialize(self, density, hamiltonian, wfs, occupations): self.xc.initialize(density, hamiltonian, wfs, occupations) self.nspins = wfs.nspins self.setups = wfs.setups self.density = density self.kpt_u = wfs.kpt_u self.gd = density.gd self.kd = wfs.kd self.bd = wfs.bd N_c = self.gd.N_c N = self.gd.N_c.prod() vol = self.gd.dv * N if self.alpha is None: self.alpha = 6 * vol**(2 / 3.0) / pi**2 self.gamma = (vol / (2 * pi)**2 * sqrt(pi / self.alpha) * self.kd.nbzkpts) ecut = 0.5 * pi**2 / (self.gd.h_cv**2).sum(1).max() if self.kd.N_c is None: self.bzk_kc = np.zeros((1, 3)) dfghdfgh else: n = self.kd.N_c * 2 - 1 bzk_kc = np.indices(n).transpose((1, 2, 3, 0)) bzk_kc.shape = (-1, 3) bzk_kc -= self.kd.N_c - 1 self.bzk_kc = bzk_kc.astype(float) / self.kd.N_c self.pwd = PWDescriptor(ecut, self.gd, self.bzk_kc) n = 0 for k_c, Gpk2_G in zip(self.bzk_kc[:], self.pwd.G2_qG): if (k_c > -0.5).all() and (k_c <= 0.5).all(): #XXX??? if k_c.any(): self.gamma -= np.dot(np.exp(-self.alpha * Gpk2_G), Gpk2_G**-1) else: self.gamma -= np.dot(np.exp(-self.alpha * Gpk2_G[1:]), Gpk2_G[1:]**-1) n += 1 assert n == self.kd.N_c.prod() self.ghat = LFC(self.gd, [setup.ghat_l for setup in density.setups], dtype=complex) self.ghat.set_k_points(self.bzk_kc) self.fullkd = KPointDescriptor(self.kd.bzk_kc, nspins=1) class S: id_a = [] def set_symmetry(self, s): pass self.fullkd.set_symmetry(Atoms(pbc=True), S(), False) self.fullkd.set_communicator(world) self.pt = LFC(self.gd, [setup.pt_j for setup in density.setups], dtype=complex) self.pt.set_k_points(self.fullkd.ibzk_kc) self.interpolator = density.interpolator def set_positions(self, spos_ac): self.ghat.set_positions(spos_ac) self.pt.set_positions(spos_ac) def calculate(self, gd, n_sg, v_sg=None, e_g=None): # Normal XC contribution: exc = self.xc.calculate(gd, n_sg, v_sg, e_g) # Add EXX contribution: return exc + self.exx def calculate_exx(self): """Non-selfconsistent calculation.""" kd = self.kd K = self.fullkd.nibzkpts assert self.nspins == 1 Q = K // world.size assert Q * world.size == K parallel = (world.size > self.nspins) self.exx = 0.0 self.exx_skn = np.zeros((self.nspins, K, self.bd.nbands)) kpt_u = [] for k in range(world.rank * Q, (world.rank + 1) * Q): k_c = self.fullkd.ibzk_kc[k] for k1, k1_c in enumerate(kd.bzk_kc): if abs(k1_c - k_c).max() < 1e-10: break # Index of symmetry related point in the irreducible BZ ik = kd.kibz_k[k1] kpt = self.kpt_u[ik] # KPoint from ground-state calculation phase_cd = np.exp(2j * pi * self.gd.sdisp_cd * k_c[:, np.newaxis]) kpt2 = KPoint0(kpt.weight, kpt.s, k, None, phase_cd) kpt2.psit_nG = np.empty_like(kpt.psit_nG) kpt2.f_n = kpt.f_n / kpt.weight / K * 2 for n, psit_G in enumerate(kpt2.psit_nG): psit_G[:] = kd.transform_wave_function(kpt.psit_nG[n], k1) kpt2.P_ani = self.pt.dict(len(kpt.psit_nG)) self.pt.integrate(kpt2.psit_nG, kpt2.P_ani, k) kpt_u.append(kpt2) for s in range(self.nspins): kpt1_q = [KPoint(self.fullkd, kpt) for kpt in kpt_u if kpt.s == s] kpt2_q = kpt1_q[:] if len(kpt1_q) == 0: # No s-spins on this CPU: continue # Send rank: srank = self.fullkd.get_rank_and_index(s, (kpt1_q[0].k - 1) % K)[0] # Receive rank: rrank = self.fullkd.get_rank_and_index(s, (kpt1_q[-1].k + 1) % K)[0] # Shift k-points K // 2 times: for i in range(K // 2 + 1): if i < K // 2: if parallel: kpt = kpt2_q[-1].next() kpt.start_receiving(rrank) kpt2_q[0].start_sending(srank) else: kpt = kpt2_q[0] for kpt1, kpt2 in zip(kpt1_q, kpt2_q): if 2 * i == K: self.apply(kpt1, kpt2, invert=(kpt1.k > kpt2.k)) else: self.apply(kpt1, kpt2) self.apply(kpt1, kpt2, invert=True) if i < K // 2: if parallel: kpt.wait() kpt2_q[0].wait() kpt2_q.pop(0) kpt2_q.append(kpt) self.exx = world.sum(self.exx) world.sum(self.exx_skn) self.exx += self.calculate_paw_correction() def apply(self, kpt1, kpt2, invert=False): #print world.rank,kpt1.k,kpt2.k,invert k1_c = self.fullkd.ibzk_kc[kpt1.k] k2_c = self.fullkd.ibzk_kc[kpt2.k] if invert: k2_c = -k2_c k12_c = k1_c - k2_c N_c = self.gd.N_c eikr_R = np.exp(2j * pi * np.dot(np.indices(N_c).T, k12_c / N_c).T) for q, k_c in enumerate(self.bzk_kc): if abs(k_c + k12_c).max() < 1e-9: q0 = q break for q, k_c in enumerate(self.bzk_kc): if abs(k_c - k12_c).max() < 1e-9: q00 = q break Gpk2_G = self.pwd.G2_qG[q0] if Gpk2_G[0] == 0: Gpk2_G = Gpk2_G.copy() Gpk2_G[0] = 1.0 / self.gamma N = N_c.prod() vol = self.gd.dv * N nspins = self.nspins same = (kpt1.k == kpt2.k) for n1, psit1_R in enumerate(kpt1.psit_nG): f1 = kpt1.f_n[n1] for n2, psit2_R in enumerate(kpt2.psit_nG): if same and n2 > n1: continue f2 = kpt2.f_n[n2] nt_R = self.calculate_pair_density(n1, n2, kpt1, kpt2, q0, invert) nt_G = self.pwd.fft(nt_R * eikr_R) / N vt_G = nt_G.copy() vt_G *= -pi * vol / Gpk2_G e = np.vdot(nt_G, vt_G).real * nspins * self.hybrid if same and n1 == n2: e /= 2 self.exx += e * f1 * f2 self.ekin -= 2 * e * f1 * f2 self.exx_skn[kpt1.s, kpt1.k, n1] += f2 * e self.exx_skn[kpt2.s, kpt2.k, n2] += f1 * e calculate_potential = not True if calculate_potential: vt_R = self.pwd.ifft(vt_G).conj() * eikr_R * N / vol if kpt1 is kpt2 and not invert and n1 == n2: kpt1.vt_nG[n1] = 0.5 * f1 * vt_R if invert: kpt1.Htpsit_nG[n1] += \ f2 * nspins * psit2_R.conj() * vt_R else: kpt1.Htpsit_nG[n1] += f2 * nspins * psit2_R * vt_R if kpt1 is not kpt2: if invert: kpt2.Htpsit_nG[n2] += (f1 * nspins * psit1_R.conj() * vt_R) else: kpt2.Htpsit_nG[n2] += (f1 * nspins * psit1_R * vt_R.conj()) def calculate_paw_correction(self): exx = 0 deg = 2 // self.nspins # spin degeneracy for a, D_sp in self.density.D_asp.items(): setup = self.setups[a] for D_p in D_sp: D_ii = unpack2(D_p) ni = len(D_ii) for i1 in range(ni): for i2 in range(ni): A = 0.0 for i3 in range(ni): p13 = packed_index(i1, i3, ni) for i4 in range(ni): p24 = packed_index(i2, i4, ni) A += setup.M_pp[p13, p24] * D_ii[i3, i4] p12 = packed_index(i1, i2, ni) exx -= self.hybrid / deg * D_ii[i1, i2] * A if setup.X_p is not None: exx -= self.hybrid * np.dot(D_p, setup.X_p) exx += self.hybrid * setup.ExxC return exx def calculate_pair_density(self, n1, n2, kpt1, kpt2, q, invert): if invert: nt_G = kpt1.psit_nG[n1].conj() * kpt2.psit_nG[n2].conj() else: nt_G = kpt1.psit_nG[n1].conj() * kpt2.psit_nG[n2] Q_aL = {} for a, P1_ni in kpt1.P_ani.items(): P1_i = P1_ni[n1] P2_i = kpt2.P_ani[a][n2] if invert: D_ii = np.outer(P1_i.conj(), P2_i.conj()) else: D_ii = np.outer(P1_i.conj(), P2_i) D_p = pack(D_ii) Q_aL[a] = np.dot(D_p, self.setups[a].Delta_pL) self.ghat.add(nt_G, Q_aL, q) return nt_G
def initialize(self, atoms=None): """Inexpensive initialization.""" if atoms is None: atoms = self.atoms else: # Save the state of the atoms: self.atoms = atoms.copy() par = self.input_parameters world = par.communicator if world is None: world = mpi.world elif hasattr(world, 'new_communicator'): # Check for whether object has correct type already # # Using isinstance() is complicated because of all the # combinations, serial/parallel/debug... pass else: # world should be a list of ranks: world = mpi.world.new_communicator(np.asarray(world)) self.wfs.world = world self.set_text(par.txt, par.verbose) natoms = len(atoms) pos_av = atoms.get_positions() / Bohr cell_cv = atoms.get_cell() pbc_c = atoms.get_pbc() Z_a = atoms.get_atomic_numbers() magmom_a = atoms.get_initial_magnetic_moments() magnetic = magmom_a.any() spinpol = par.spinpol if par.hund: if natoms != 1: raise ValueError('hund=True arg only valid for single atoms!') spinpol = True if spinpol is None: spinpol = magnetic elif magnetic and not spinpol: raise ValueError('Non-zero initial magnetic moment for a ' 'spin-paired calculation!') nspins = 1 + int(spinpol) if isinstance(par.xc, str): xc = XC(par.xc) else: xc = par.xc setups = Setups(Z_a, par.setups, par.basis, par.lmax, xc, world) # K-point descriptor kd = KPointDescriptor(par.kpts, nspins) width = par.width if width is None: if kd.gamma: width = 0.0 else: width = 0.1 # eV else: assert par.occupations is None if par.gpts is not None and par.h is None: N_c = np.array(par.gpts) else: if par.h is None: self.text('Using default value for grid spacing.') h = 0.2 else: h = par.h N_c = h2gpts(h, cell_cv) cell_cv /= Bohr if hasattr(self, 'time') or par.dtype==complex: dtype = complex else: if kd.gamma: dtype = float else: dtype = complex kd.set_symmetry(atoms, setups, par.usesymm, N_c) nao = setups.nao nvalence = setups.nvalence - par.charge nbands = par.nbands if nbands is None: nbands = nao elif nbands > nao and par.mode == 'lcao': raise ValueError('Too many bands for LCAO calculation: ' + '%d bands and only %d atomic orbitals!' % (nbands, nao)) if nvalence < 0: raise ValueError( 'Charge %f is not possible - not enough valence electrons' % par.charge) M = magmom_a.sum() if par.hund: f_si = setups[0].calculate_initial_occupation_numbers( magmom=0, hund=True, charge=par.charge, nspins=nspins) Mh = f_si[0].sum() - f_si[1].sum() if magnetic and M != Mh: raise RuntimeError('You specified a magmom that does not' 'agree with hunds rule!') else: M = Mh if nbands <= 0: nbands = int(nvalence + M + 0.5) // 2 + (-nbands) if nvalence > 2 * nbands: raise ValueError('Too few bands! Electrons: %d, bands: %d' % (nvalence, nbands)) if par.width is not None: self.text('**NOTE**: please start using ' 'occupations=FermiDirac(width).') if par.fixmom: self.text('**NOTE**: please start using ' 'occupations=FermiDirac(width, fixmagmom=True).') if self.occupations is None: if par.occupations is None: # Create object for occupation numbers: self.occupations = occupations.FermiDirac(width, par.fixmom) else: self.occupations = par.occupations self.occupations.magmom = M cc = par.convergence if par.mode == 'lcao': niter_fixdensity = 0 else: niter_fixdensity = None if self.scf is None: self.scf = SCFLoop( cc['eigenstates'] * nvalence, cc['energy'] / Hartree * max(nvalence, 1), cc['density'] * nvalence, par.maxiter, par.fixdensity, niter_fixdensity) parsize, parsize_bands = par.parallel['domain'], par.parallel['band'] if parsize_bands is None: parsize_bands = 1 # TODO delete/restructure so all checks are in BandDescriptor if nbands % parsize_bands != 0: raise RuntimeError('Cannot distribute %d bands to %d processors' % (nbands, parsize_bands)) if not self.wfs: if parsize == 'domain only': #XXX this was silly! parsize = world.size domain_comm, kpt_comm, band_comm = mpi.distribute_cpus(parsize, parsize_bands, nspins, kd.nibzkpts, world, par.idiotproof) kd.set_communicator(kpt_comm) parstride_bands = par.parallel['stridebands'] bd = BandDescriptor(nbands, band_comm, parstride_bands) if (self.density is not None and self.density.gd.comm.size != domain_comm.size): # Domain decomposition has changed, so we need to # reinitialize density and hamiltonian: if par.fixdensity: raise RuntimeError("I'm confused - please specify parsize." ) self.density = None self.hamiltonian = None # Construct grid descriptor for coarse grids for wave functions: gd = self.grid_descriptor_class(N_c, cell_cv, pbc_c, domain_comm, parsize) # do k-point analysis here? XXX args = (gd, nvalence, setups, bd, dtype, world, kd, self.timer) if par.mode == 'lcao': # Layouts used for general diagonalizer sl_lcao = par.parallel['sl_lcao'] if sl_lcao is None: sl_lcao = par.parallel['sl_default'] lcaoksl = get_KohnSham_layouts(sl_lcao, 'lcao', gd, bd, dtype, nao=nao, timer=self.timer) self.wfs = LCAOWaveFunctions(lcaoksl, *args) elif par.mode == 'fd' or isinstance(par.mode, PW): # buffer_size keyword only relevant for fdpw buffer_size = par.parallel['buffer_size'] # Layouts used for diagonalizer sl_diagonalize = par.parallel['sl_diagonalize'] if sl_diagonalize is None: sl_diagonalize = par.parallel['sl_default'] diagksl = get_KohnSham_layouts(sl_diagonalize, 'fd', gd, bd, dtype, buffer_size=buffer_size, timer=self.timer) # Layouts used for orthonormalizer sl_inverse_cholesky = par.parallel['sl_inverse_cholesky'] if sl_inverse_cholesky is None: sl_inverse_cholesky = par.parallel['sl_default'] if sl_inverse_cholesky != sl_diagonalize: message = 'sl_inverse_cholesky != sl_diagonalize ' \ 'is not implemented.' raise NotImplementedError(message) orthoksl = get_KohnSham_layouts(sl_inverse_cholesky, 'fd', gd, bd, dtype, buffer_size=buffer_size, timer=self.timer) # Use (at most) all available LCAO for initialization lcaonbands = min(nbands, nao) lcaobd = BandDescriptor(lcaonbands, band_comm, parstride_bands) assert nbands <= nao or bd.comm.size == 1 assert lcaobd.mynbands == min(bd.mynbands, nao) #XXX # Layouts used for general diagonalizer (LCAO initialization) sl_lcao = par.parallel['sl_lcao'] if sl_lcao is None: sl_lcao = par.parallel['sl_default'] initksl = get_KohnSham_layouts(sl_lcao, 'lcao', gd, lcaobd, dtype, nao=nao, timer=self.timer) if par.mode == 'fd': self.wfs = FDWaveFunctions(par.stencils[0], diagksl, orthoksl, initksl, *args) else: # Planewave basis: self.wfs = par.mode(diagksl, orthoksl, initksl, gd, nvalence, setups, bd, world, kd, self.timer) else: self.wfs = par.mode(self, *args) else: self.wfs.set_setups(setups) if not self.wfs.eigensolver: # Number of bands to converge: nbands_converge = cc['bands'] if nbands_converge == 'all': nbands_converge = nbands elif nbands_converge != 'occupied': assert isinstance(nbands_converge, int) if nbands_converge < 0: nbands_converge += nbands eigensolver = get_eigensolver(par.eigensolver, par.mode, par.convergence) eigensolver.nbands_converge = nbands_converge # XXX Eigensolver class doesn't define an nbands_converge property self.wfs.set_eigensolver(eigensolver) if self.density is None: gd = self.wfs.gd if par.stencils[1] != 9: # Construct grid descriptor for fine grids for densities # and potentials: finegd = gd.refine() else: # Special case (use only coarse grid): finegd = gd self.density = Density(gd, finegd, nspins, par.charge + setups.core_charge) self.density.initialize(setups, par.stencils[1], self.timer, magmom_a, par.hund) self.density.set_mixer(par.mixer) if self.hamiltonian is None: gd, finegd = self.density.gd, self.density.finegd self.hamiltonian = Hamiltonian(gd, finegd, nspins, setups, par.stencils[1], self.timer, xc, par.poissonsolver, par.external) xc.initialize(self.density, self.hamiltonian, self.wfs, self.occupations) self.text() self.print_memory_estimate(self.txt, maxdepth=memory_estimate_depth) self.txt.flush() if dry_run: self.dry_run() self.initialized = True
class G0W0(PairDensity): """This class defines the G0W0 calculator. The G0W0 calculator is used is used to calculate the quasi particle energies through the G0W0 approximation for a number of states. Note: So far the G0W0 calculation only works for spin-paired systems. Parameters: calc: str or PAW object GPAW calculator object or filename of saved calculator object. filename: str Base filename of output files. kpts: list List of indices of the IBZ k-points to calculate the quasi particle energies for. bands: tuple Range of band indices, like (n1, n2+1), to calculate the quasi particle energies for. Note that the second band index is not included. ecut: float Plane wave cut-off energy in eV. nbands: int Number of bands to use in the calculation. If :None: the number will be determined from :ecut: to yield a number close to the number of plane waves used. ppa: bool Sets whether the Godby-Needs plasmon-pole approximation for the dielectric function should be used. E0: float Energy (in eV) used for fitting in the plasmon-pole approximation. domega0: float Minimum frequency step (in eV) used in the generation of the non- linear frequency grid. omega2: float Control parameter for the non-linear frequency grid, equal to the frequency where the grid spacing has doubled in size. wstc: bool Sets whether a Wigner-Seitz truncation should be used for the Coloumb potential. """ def __init__(self, calc, filename='gw', kpts=None, bands=None, nbands=None, ppa=False, wstc=False, ecut=150.0, eta=0.1, E0=1.0 * Hartree, domega0=0.025, omega2=10.0, nblocks=1, savew=False, world=mpi.world): if world.rank != 0: txt = devnull else: txt = open(filename + '.txt', 'w') p = functools.partial(print, file=txt) p(' ___ _ _ _ ') p(' | || | | |') p(' | | || | | |') p(' |__ ||_____|') p(' |___|') p() PairDensity.__init__(self, calc, ecut, world=world, nblocks=nblocks, txt=txt) self.filename = filename self.savew = savew ecut /= Hartree self.ppa = ppa self.wstc = wstc self.eta = eta / Hartree self.E0 = E0 / Hartree self.domega0 = domega0 / Hartree self.omega2 = omega2 / Hartree self.kpts = select_kpts(kpts, self.calc) if bands is None: bands = [0, self.nocc2] self.bands = bands b1, b2 = bands self.shape = shape = (self.calc.wfs.nspins, len(self.kpts), b2 - b1) self.eps_sin = np.empty(shape) # KS-eigenvalues self.f_sin = np.empty(shape) # occupation numbers self.sigma_sin = np.zeros(shape) # self-energies self.dsigma_sin = np.zeros(shape) # derivatives of self-energies self.vxc_sin = None # KS XC-contributions self.exx_sin = None # exact exchange contributions self.Z_sin = None # renormalization factors if nbands is None: nbands = int(self.vol * ecut**1.5 * 2**0.5 / 3 / pi**2) self.nbands = nbands p() p('Quasi particle states:') if kpts is None: p('All k-points in IBZ') else: kptstxt = ', '.join(['{0:d}'.format(k) for k in self.kpts]) p('k-points (IBZ indices): [' + kptstxt + ']') p('Band range: ({0:d}, {1:d})'.format(b1, b2)) p() p('Computational parameters:') p('Plane wave cut-off: {0:g} eV'.format(self.ecut * Hartree)) p('Number of bands: {0:d}'.format(self.nbands)) p('Broadening: {0:g} eV'.format(self.eta * Hartree)) kd = self.calc.wfs.kd self.mysKn1n2 = None # my (s, K, n1, n2) indices self.distribute_k_points_and_bands(b1, b2, kd.ibz2bz_k[self.kpts]) # Find q-vectors and weights in the IBZ: assert -1 not in kd.bz2bz_ks offset_c = 0.5 * ((kd.N_c + 1) % 2) / kd.N_c bzq_qc = monkhorst_pack(kd.N_c) + offset_c self.qd = KPointDescriptor(bzq_qc) self.qd.set_symmetry(self.calc.atoms, kd.symmetry) assert self.calc.wfs.nspins == 1 @timer('G0W0') def calculate(self, ecuts=None): """Starts the G0W0 calculation. Returns a dict with the results with the following key/value pairs: f: (s, k, n) ndarray Occupation numbers eps: (s, k, n) ndarray Kohn-Sham eigenvalues in eV vxc: (s, k, n) ndarray Exchange-correlation contributions in eV exx: (s, k, n) ndarray Exact exchange contributions in eV sigma: (s, k, n) ndarray Self-energy contributions in eV Z: (s, k, n) ndarray Renormalization factors qp: (s, k, n) ndarray Quasi particle energies in eV """ kd = self.calc.wfs.kd self.calculate_ks_xc_contribution() self.calculate_exact_exchange() # Reset calculation self.sigma_sin = np.zeros(self.shape) # self-energies self.dsigma_sin = np.zeros(self.shape) # derivatives of self-energies # Get KS eigenvalues and occupation numbers: b1, b2 = self.bands for i, k in enumerate(self.kpts): kpt = self.calc.wfs.kpt_u[k] self.eps_sin[0, i] = kpt.eps_n[b1:b2] self.f_sin[0, i] = kpt.f_n[b1:b2] / kpt.weight # My part of the states we want to calculate QP-energies for: mykpts = [self.get_k_point(s, K, n1, n2) for s, K, n1, n2 in self.mysKn1n2] # Loop over q in the IBZ: for pd0, W0, q_c in self.calculate_screened_potential(): for kpt1 in mykpts: K2 = kd.find_k_plus_q(q_c, [kpt1.K])[0] kpt2 = self.get_k_point(0, K2, 0, self.nbands, block=True) k1 = kd.bz2ibz_k[kpt1.K] i = self.kpts.index(k1) self.calculate_q(i, kpt1, kpt2, pd0, W0) self.world.sum(self.sigma_sin) self.world.sum(self.dsigma_sin) self.Z_sin = 1 / (1 - self.dsigma_sin) self.qp_sin = self.eps_sin + self.Z_sin * (self.sigma_sin + self.exx_sin - self.vxc_sin) results = {'f': self.f_sin, 'eps': self.eps_sin * Hartree, 'vxc': self.vxc_sin * Hartree, 'exx': self.exx_sin * Hartree, 'sigma': self.sigma_sin * Hartree, 'Z': self.Z_sin, 'qp': self.qp_sin * Hartree} self.print_results(results) return results def calculate_q(self, i, kpt1, kpt2, pd0, W0): """Calculates the contribution to the self-energy and its derivative for a given set of k-points, kpt1 and kpt2.""" wfs = self.calc.wfs N_c = pd0.gd.N_c i_cG = self.sign * np.dot(self.U_cc, np.unravel_index(pd0.Q_qG[0], N_c)) q_c = wfs.kd.bzk_kc[kpt2.K] - wfs.kd.bzk_kc[kpt1.K] q0 = np.allclose(q_c, 0) and not self.wstc shift0_c = q_c - self.sign * np.dot(self.U_cc, pd0.kd.bzk_kc[0]) assert np.allclose(shift0_c.round(), shift0_c) shift0_c = shift0_c.round().astype(int) shift_c = kpt1.shift_c - kpt2.shift_c - shift0_c I_G = np.ravel_multi_index(i_cG + shift_c[:, None], N_c, 'wrap') G_Gv = pd0.get_reciprocal_vectors() pos_av = np.dot(self.spos_ac, pd0.gd.cell_cv) M_vv = np.dot(pd0.gd.cell_cv.T, np.dot(self.U_cc.T, np.linalg.inv(pd0.gd.cell_cv).T)) Q_aGii = [] for a, Q_Gii in enumerate(self.Q_aGii): x_G = np.exp(1j * np.dot(G_Gv, (pos_av[a] - self.sign * np.dot(M_vv, pos_av[a])))) U_ii = self.calc.wfs.setups[a].R_sii[self.s] Q_Gii = np.dot(np.dot(U_ii, Q_Gii * x_G[:, None, None]), U_ii.T).transpose(1, 0, 2) Q_aGii.append(Q_Gii) if debug: self.check(i_cG, shift0_c, N_c, q_c, Q_aGii) if self.ppa: calculate_sigma = self.calculate_sigma_ppa else: calculate_sigma = self.calculate_sigma for n in range(kpt1.n2 - kpt1.n1): ut1cc_R = kpt1.ut_nR[n].conj() eps1 = kpt1.eps_n[n] C1_aGi = [np.dot(Qa_Gii, P1_ni[n].conj()) for Qa_Gii, P1_ni in zip(Q_aGii, kpt1.P_ani)] n_mG = self.calculate_pair_densities(ut1cc_R, C1_aGi, kpt2, pd0, I_G) if self.sign == 1: n_mG = n_mG.conj() if q0: n_mG[:, 0] = 0 m = n + kpt1.n1 - kpt2.n1 if 0 <= m < len(n_mG): n_mG[m, 0] = 1.0 f_m = kpt2.f_n deps_m = eps1 - kpt2.eps_n sigma, dsigma = calculate_sigma(n_mG, deps_m, f_m, W0) nn = kpt1.n1 + n - self.bands[0] self.sigma_sin[kpt1.s, i, nn] += sigma self.dsigma_sin[kpt1.s, i, nn] += dsigma def check(self, i_cG, shift0_c, N_c, q_c, Q_aGii): I0_G = np.ravel_multi_index(i_cG - shift0_c[:, None], N_c, 'wrap') qd1 = KPointDescriptor([q_c]) pd1 = PWDescriptor(self.ecut, self.calc.wfs.gd, complex, qd1) G_I = np.empty(N_c.prod(), int) G_I[:] = -1 I1_G = pd1.Q_qG[0] G_I[I1_G] = np.arange(len(I0_G)) G_G = G_I[I0_G] assert len(I0_G) == len(I1_G) assert (G_G >= 0).all() for a, Q_Gii in enumerate(self.initialize_paw_corrections(pd1)): e = abs(Q_aGii[a] - Q_Gii[G_G]).max() assert e < 1e-12 @timer('Sigma') def calculate_sigma(self, n_mG, deps_m, f_m, C_swGG): """Calculates a contribution to the self-energy and its derivative for a given (k, k-q)-pair from its corresponding pair-density and energy.""" o_m = abs(deps_m) # Add small number to avoid zeros for degenerate states: sgn_m = np.sign(deps_m + 1e-15) # Pick +i*eta or -i*eta: s_m = (1 + sgn_m * np.sign(0.5 - f_m)).astype(int) // 2 beta = (2**0.5 - 1) * self.domega0 / self.omega2 w_m = (o_m / (self.domega0 + beta * o_m)).astype(int) o1_m = self.omega_w[w_m] o2_m = self.omega_w[w_m + 1] x = 1.0 / (self.qd.nbzkpts * 2 * pi * self.vol) sigma = 0.0 dsigma = 0.0 # Performing frequency integration for o, o1, o2, sgn, s, w, n_G in zip(o_m, o1_m, o2_m, sgn_m, s_m, w_m, n_mG): C1_GG = C_swGG[s][w] C2_GG = C_swGG[s][w + 1] p = x * sgn myn_G = n_G[self.Ga:self.Gb] sigma1 = p * np.dot(np.dot(myn_G, C1_GG), n_G.conj()).imag sigma2 = p * np.dot(np.dot(myn_G, C2_GG), n_G.conj()).imag sigma += ((o - o1) * sigma2 + (o2 - o) * sigma1) / (o2 - o1) dsigma += sgn * (sigma2 - sigma1) / (o2 - o1) return sigma, dsigma 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) @timer('WW') def calculate_w(self, chi0, q_c, htp, htm, wstc, A1_x, A2_x): """Calculates the screened potential for a specified q-point.""" pd, chi0_wGG = chi0.calculate(q_c, A_x=A1_x)[:2] self.Q_aGii = chi0.Q_aGii self.Ga = chi0.Ga self.Gb = chi0.Gb if self.blockcomm.size > 1: A1_x = chi0_wGG.ravel() chi0_wGG = chi0.redistribute(chi0_wGG, A2_x) if self.wstc: iG_G = (wstc.get_potential(pd) / (4 * pi))**0.5 if np.allclose(q_c, 0): chi0_wGG[:, 0] = 0.0 chi0_wGG[:, :, 0] = 0.0 G0inv = 0.0 G20inv = 0.0 else: G0inv = None G20inv = None else: if np.allclose(q_c, 0): dq3 = (2 * pi)**3 / (self.qd.nbzkpts * self.vol) qc = (dq3 / 4 / pi * 3)**(1 / 3) G0inv = 2 * pi * qc**2 / dq3 G20inv = 4 * pi * qc / dq3 G_G = pd.G2_qG[0]**0.5 G_G[0] = 1 iG_G = 1 / G_G else: iG_G = pd.G2_qG[0]**-0.5 G0inv = None G20inv = None delta_GG = np.eye(len(iG_G)) if self.ppa: return pd, self.ppa_w(chi0_wGG, iG_G, delta_GG, G0inv, G20inv, q_c) self.timer.start('Dyson eq.') # Calculate W and store it in chi0_wGG ndarray: for chi0_GG in chi0_wGG: e_GG = (delta_GG - 4 * pi * chi0_GG * iG_G * iG_G[:, np.newaxis]) W_GG = chi0_GG W_GG[:] = 4 * pi * (np.linalg.inv(e_GG) - delta_GG) * iG_G * iG_G[:, np.newaxis] if np.allclose(q_c, 0): W_GG[0, 0] *= G20inv W_GG[1:, 0] *= G0inv W_GG[0, 1:] *= G0inv if self.blockcomm.size > 1: Wm_wGG = chi0.redistribute(chi0_wGG, A1_x) else: Wm_wGG = chi0_wGG Wp_wGG = A2_x[:Wm_wGG.size].reshape(Wm_wGG.shape) Wp_wGG[:] = Wm_wGG with self.timer('Hilbert transform'): htp(Wp_wGG) htm(Wm_wGG) self.timer.stop('Dyson eq.') return pd, [Wp_wGG, Wm_wGG] @timer('Kohn-Sham XC-contribution') def calculate_ks_xc_contribution(self): name = self.filename + '.vxc.npy' fd = opencew(name) if fd is None: print('Reading Kohn-Sham XC contribution from file:', name, file=self.fd) with open(name) as fd: self.vxc_sin = np.load(fd) assert self.vxc_sin.shape == self.shape, self.vxc_sin.shape return print('Calculating Kohn-Sham XC contribution', file=self.fd) if self.reader is not None: self.calc.wfs.read_projections(self.reader) vxc_skn = vxc(self.calc, self.calc.hamiltonian.xc) / Hartree n1, n2 = self.bands self.vxc_sin = vxc_skn[:, self.kpts, n1:n2] np.save(fd, self.vxc_sin) @timer('EXX') def calculate_exact_exchange(self): name = self.filename + '.exx.npy' fd = opencew(name) if fd is None: print('Reading EXX contribution from file:', name, file=self.fd) with open(name) as fd: self.exx_sin = np.load(fd) assert self.exx_sin.shape == self.shape, self.exx_sin.shape return print('Calculating EXX contribution', file=self.fd) exx = EXX(self.calc, kpts=self.kpts, bands=self.bands, txt=self.filename + '.exx.txt', timer=self.timer) exx.calculate() self.exx_sin = exx.get_eigenvalue_contributions() / Hartree np.save(fd, self.exx_sin) def print_results(self, results): description = ['f: Occupation numbers', 'eps: KS-eigenvalues [eV]', 'vxc: KS vxc [eV]', 'exx: Exact exchange [eV]', 'sigma: Self-energies [eV]', 'Z: Renormalization factors', 'qp: QP-energies [eV]'] print('\nResults:', file=self.fd) for line in description: print(line, file=self.fd) b1, b2 = self.bands names = [line.split(':', 1)[0] for line in description] ibzk_kc = self.calc.wfs.kd.ibzk_kc for s in range(self.calc.wfs.nspins): for i, ik in enumerate(self.kpts): print('\nk-point ' + '{0} ({1}): ({2:.3f}, {3:.3f}, {4:.3f})'.format( i, ik, *ibzk_kc[ik]), file=self.fd) print('band' + ''.join('{0:>8}'.format(name) for name in names), file=self.fd) for n in range(b2 - b1): print('{0:4}'.format(n + b1) + ''.join('{0:8.3f}'.format(results[name][s, i, n]) for name in names), file=self.fd) self.timer.write(self.fd) @timer('PPA') def ppa_w(self, chi0_wGG, iG_G, delta_GG, G0inv, G20inv, q_c): einv_wGG = [] for chi0_GG in chi0_wGG: e_GG = (delta_GG - 4 * pi * chi0_GG * iG_G * iG_G[:, np.newaxis]) einv_wGG.append(np.linalg.inv(e_GG) - delta_GG) if self.wstc and np.allclose(q_c, 0): einv_wGG[0][0] = 42 einv_wGG[0][:, 0] = 42 omegat_GG = self.E0 * np.sqrt(einv_wGG[1] / (einv_wGG[0] - einv_wGG[1])) R_GG = -0.5 * omegat_GG * einv_wGG[0] W_GG = 4 * pi**2 * R_GG * iG_G * iG_G[:, np.newaxis] if np.allclose(q_c, 0): W_GG[0, 0] *= G20inv W_GG[1:, 0] *= G0inv W_GG[0, 1:] *= G0inv return [W_GG, omegat_GG] @timer('PPA-Sigma') def calculate_sigma_ppa(self, n_mG, deps_m, f_m, W): W_GG, omegat_GG = W sigma = 0.0 dsigma = 0.0 # init variables (is this necessary?) nG = n_mG.shape[1] deps_GG = np.empty((nG, nG)) sign_GG = np.empty((nG, nG)) x1_GG = np.empty((nG, nG)) x2_GG = np.empty((nG, nG)) x3_GG = np.empty((nG, nG)) x4_GG = np.empty((nG, nG)) x_GG = np.empty((nG, nG)) dx_GG = np.empty((nG, nG)) nW_G = np.empty(nG) for m in range(np.shape(n_mG)[0]): deps_GG = deps_m[m] sign_GG = 2 * f_m[m] - 1 x1_GG = 1 / (deps_GG + omegat_GG - 1j * self.eta) x2_GG = 1 / (deps_GG - omegat_GG + 1j * self.eta) x3_GG = 1 / (deps_GG + omegat_GG - 1j * self.eta * sign_GG) x4_GG = 1 / (deps_GG - omegat_GG - 1j * self.eta * sign_GG) x_GG = W_GG * (sign_GG * (x1_GG - x2_GG) + x3_GG + x4_GG) dx_GG = W_GG * (sign_GG * (x1_GG**2 - x2_GG**2) + x3_GG**2 + x4_GG**2) nW_G = np.dot(n_mG[m], x_GG) sigma += np.vdot(n_mG[m], nW_G).real nW_G = np.dot(n_mG[m], dx_GG) dsigma -= np.vdot(n_mG[m], nW_G).real x = 1 / (self.qd.nbzkpts * 2 * pi * self.vol) return x * sigma, x * dsigma
class BSE: 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 calculate(self, optical=True, ac=1.0): if self.spinors: """Calculate spinors. Here m is index of eigenvalues with SOC and n is the basis of eigenstates withour SOC. Below m is used for unoccupied states and n is used for occupied states so be careful!""" print('Diagonalizing spin-orbit Hamiltonian', file=self.fd) param = self.calc.parameters if not param['symmetry'] == 'off': print('Calculating KS wavefunctions without symmetry ' + 'for spin-orbit', file=self.fd) if not op.isfile('gs_nosym.gpw'): calc_so = GPAW(**param) calc_so.set(symmetry='off', fixdensity=True, txt='gs_nosym.txt') calc_so.atoms = self.calc.atoms calc_so.density = self.calc.density calc_so.get_potential_energy() calc_so.write('gs_nosym.gpw') calc_so = GPAW('gs_nosym.gpw', txt=None, communicator=serial_comm) e_mk, v_knm = get_spinorbit_eigenvalues(calc_so, return_wfs=True, scale=self.scale) del calc_so else: e_mk, v_knm = get_spinorbit_eigenvalues(self.calc, return_wfs=True, scale=self.scale) e_mk /= Hartree # Parallelization stuff nK = self.kd.nbzkpts myKrange, myKsize, mySsize = self.parallelisation_sizes() # Calculate exchange interaction qd0 = KPointDescriptor([self.q_c]) pd0 = PWDescriptor(self.ecut, self.calc.wfs.gd, complex, qd0) ikq_k = self.kd.find_k_plus_q(self.q_c) v_G = get_coulomb_kernel(pd0, self.kd.N_c, truncation=self.truncation, wstc=self.wstc) if optical: v_G[0] = 0.0 self.pair = PairDensity(self.calc, self.ecut, world=serial_comm, txt='pair.txt') # Calculate direct (screened) interaction and PAW corrections if self.mode == 'RPA': Q_aGii = self.pair.initialize_paw_corrections(pd0) else: self.get_screened_potential(ac=ac) if (self.qd.ibzk_kc - self.q_c < 1.0e-6).all(): iq0 = self.qd.bz2ibz_k[self.kd.where_is_q(self.q_c, self.qd.bzk_kc)] Q_aGii = self.Q_qaGii[iq0] else: Q_aGii = self.pair.initialize_paw_corrections(pd0) # Calculate pair densities, eigenvalues and occupations so = self.spinors + 1 Nv, Nc = so * self.nv, so * self.nc Ns = self.spins rhoex_KsmnG = np.zeros((nK, Ns, Nv, Nc, len(v_G)), complex) # rhoG0_Ksmn = np.zeros((nK, Ns, Nv, Nc), complex) df_Ksmn = np.zeros((nK, Ns, Nv, Nc), float) # -(ev - ec) deps_ksmn = np.zeros((myKsize, Ns, Nv, Nc), float) # -(fv - fc) if np.allclose(self.q_c, 0.0): optical_limit = True else: optical_limit = False get_pair = self.pair.get_kpoint_pair get_rho = self.pair.get_pair_density if self.spinors: # Get all pair densities to allow for SOC mixing # Use twice as many no-SOC states as BSE bands to allow mixing vi_s = [2 * self.val_sn[0, 0] - self.val_sn[0, -1] - 1] vf_s = [2 * self.con_sn[0, -1] - self.con_sn[0, 0] + 2] if vi_s[0] < 0: vi_s[0] = 0 ci_s, cf_s = vi_s, vf_s ni, nf = vi_s[0], vf_s[0] mvi = 2 * self.val_sn[0, 0] mvf = 2 * (self.val_sn[0, -1] + 1) mci = 2 * self.con_sn[0, 0] mcf = 2 * (self.con_sn[0, -1] + 1) else: vi_s, vf_s = self.val_sn[:, 0], self.val_sn[:, -1] + 1 ci_s, cf_s = self.con_sn[:, 0], self.con_sn[:, -1] + 1 for ik, iK in enumerate(myKrange): for s in range(Ns): pair = get_pair(pd0, s, iK, vi_s[s], vf_s[s], ci_s[s], cf_s[s]) m_m = np.arange(vi_s[s], vf_s[s]) n_n = np.arange(ci_s[s], cf_s[s]) if self.gw_skn is not None: iKq = self.calc.wfs.kd.find_k_plus_q(self.q_c, [iK])[0] epsv_m = self.gw_skn[s, iK, :self.nv] epsc_n = self.gw_skn[s, iKq, self.nv:] deps_ksmn[ik] = -(epsv_m[:, np.newaxis] - epsc_n) elif self.spinors: iKq = self.calc.wfs.kd.find_k_plus_q(self.q_c, [iK])[0] epsv_m = e_mk[mvi:mvf, iK] epsc_n = e_mk[mci:mcf, iKq] deps_ksmn[ik, s] = -(epsv_m[:, np.newaxis] - epsc_n) else: deps_ksmn[ik, s] = -pair.get_transition_energies(m_m, n_n) df_mn = pair.get_occupation_differences(self.val_sn[s], self.con_sn[s]) rho_mnG = get_rho(pd0, pair, m_m, n_n, optical_limit=optical_limit, direction=self.direction, Q_aGii=Q_aGii, extend_head=False) if self.spinors: if optical_limit: deps0_mn = -pair.get_transition_energies(m_m, n_n) rho_mnG[:, :, 0] *= deps0_mn df_Ksmn[iK, s, ::2, ::2] = df_mn df_Ksmn[iK, s, ::2, 1::2] = df_mn df_Ksmn[iK, s, 1::2, ::2] = df_mn df_Ksmn[iK, s, 1::2, 1::2] = df_mn vecv0_nm = v_knm[iK][::2][ni:nf, mvi:mvf] vecc0_nm = v_knm[iKq][::2][ni:nf, mci:mcf] rho_0mnG = np.dot(vecv0_nm.T.conj(), np.dot(vecc0_nm.T, rho_mnG)) vecv1_nm = v_knm[iK][1::2][ni:nf, mvi:mvf] vecc1_nm = v_knm[iKq][1::2][ni:nf, mci:mcf] rho_1mnG = np.dot(vecv1_nm.T.conj(), np.dot(vecc1_nm.T, rho_mnG)) rhoex_KsmnG[iK, s] = rho_0mnG + rho_1mnG if optical_limit: rhoex_KsmnG[iK, s, :, :, 0] /= deps_ksmn[ik, s] else: df_Ksmn[iK, s] = pair.get_occupation_differences(m_m, n_n) rhoex_KsmnG[iK, s] = rho_mnG if self.eshift is not None: deps_ksmn[np.where(df_Ksmn[myKrange] > 1.0e-3)] += self.eshift deps_ksmn[np.where(df_Ksmn[myKrange] < -1.0e-3)] -= self.eshift world.sum(df_Ksmn) world.sum(rhoex_KsmnG) self.rhoG0_S = np.reshape(rhoex_KsmnG[:, :, :, :, 0], -1) if hasattr(self, 'H_sS'): return # Calculate Hamiltonian t0 = time() print('Calculating %s matrix elements at q_c = %s' % (self.mode, self.q_c), file=self.fd) H_ksmnKsmn = np.zeros((myKsize, Ns, Nv, Nc, nK, Ns, Nv, Nc), complex) for ik1, iK1 in enumerate(myKrange): for s1 in range(Ns): kptv1 = self.pair.get_k_point(s1, iK1, vi_s[s1], vf_s[s1]) kptc1 = self.pair.get_k_point(s1, ikq_k[iK1], ci_s[s1], cf_s[s1]) rho1_mnG = rhoex_KsmnG[iK1, s1] #rhoG0_Ksmn[iK1, s1] = rho1_mnG[:, :, 0] rho1ccV_mnG = rho1_mnG.conj()[:, :] * v_G for s2 in range(Ns): for Q_c in self.qd.bzk_kc: iK2 = self.kd.find_k_plus_q(Q_c, [kptv1.K])[0] rho2_mnG = rhoex_KsmnG[iK2, s2] rho2_mGn = np.swapaxes(rho2_mnG, 1, 2) H_ksmnKsmn[ik1, s1, :, :, iK2, s2, :, :] += ( np.dot(rho1ccV_mnG, rho2_mGn)) if not self.mode == 'RPA' and s1 == s2: ikq = ikq_k[iK2] kptv2 = self.pair.get_k_point(s1, iK2, vi_s[s1], vf_s[s1]) kptc2 = self.pair.get_k_point(s1, ikq, ci_s[s1], cf_s[s1]) rho3_mmG, iq = self.get_density_matrix(kptv1, kptv2) rho4_nnG, iq = self.get_density_matrix(kptc1, kptc2) if self.spinors: vec0_nm = v_knm[iK1][::2][ni:nf, mvi:mvf] vec1_nm = v_knm[iK1][1::2][ni:nf, mvi:mvf] vec2_nm = v_knm[iK2][::2][ni:nf, mvi:mvf] vec3_nm = v_knm[iK2][1::2][ni:nf, mvi:mvf] rho_0mnG = np.dot(vec0_nm.T.conj(), np.dot(vec2_nm.T, rho3_mmG)) rho_1mnG = np.dot(vec1_nm.T.conj(), np.dot(vec3_nm.T, rho3_mmG)) rho3_mmG = rho_0mnG + rho_1mnG vec0_nm = v_knm[ikq_k[iK1]][::2][ni:nf, mci:mcf] vec1_nm = v_knm[ikq_k[iK1]][1::2][ni:nf,mci:mcf] vec2_nm = v_knm[ikq][::2][ni:nf, mci:mcf] vec3_nm = v_knm[ikq][1::2][ni:nf, mci:mcf] rho_0mnG = np.dot(vec0_nm.T.conj(), np.dot(vec2_nm.T, rho4_nnG)) rho_1mnG = np.dot(vec1_nm.T.conj(), np.dot(vec3_nm.T, rho4_nnG)) rho4_nnG = rho_0mnG + rho_1mnG rho3ccW_mmG = np.dot(rho3_mmG.conj(), self.W_qGG[iq]) W_mmnn = np.dot(rho3ccW_mmG, np.swapaxes(rho4_nnG, 1, 2)) W_mnmn = np.swapaxes(W_mmnn, 1, 2) * Ns * so H_ksmnKsmn[ik1, s1, :, :, iK2, s1] -= 0.5 * W_mnmn if iK1 % (myKsize // 5 + 1) == 0: dt = time() - t0 tleft = dt * myKsize / (iK1 + 1) - dt print(' Finished %s pair orbitals in %s - Estimated %s left' % ((iK1 + 1) * Nv * Nc * Ns * world.size, timedelta(seconds=round(dt)), timedelta(seconds=round(tleft))), file=self.fd) #if self.mode == 'BSE': # del self.Q_qaGii, self.W_qGG, self.pd_q H_ksmnKsmn /= self.vol mySsize = myKsize * Nv * Nc * Ns if myKsize > 0: iS0 = myKrange[0] * Nv * Nc * Ns #world.sum(rhoG0_Ksmn) #self.rhoG0_S = np.reshape(rhoG0_Ksmn, -1) self.df_S = np.reshape(df_Ksmn, -1) if not self.td: self.excludef_S = np.where(np.abs(self.df_S) < 0.001)[0] # multiply by 2 when spin-paired and no SOC self.df_S *= 2.0 / nK / Ns / so self.deps_s = np.reshape(deps_ksmn, -1) H_sS = np.reshape(H_ksmnKsmn, (mySsize, self.nS)) for iS in range(mySsize): # Multiply by occupations and adiabatic coupling H_sS[iS] *= self.df_S[iS0 + iS] * ac # add bare transition energies H_sS[iS, iS0 + iS] += self.deps_s[iS] self.H_sS = H_sS if self.write_h: self.par_save('H_SS.ulm', 'H_SS', self.H_sS) def get_density_matrix(self, kpt1, kpt2): Q_c = self.kd.bzk_kc[kpt2.K] - self.kd.bzk_kc[kpt1.K] iQ = self.qd.where_is_q(Q_c, self.qd.bzk_kc) iq = self.qd.bz2ibz_k[iQ] q_c = self.qd.ibzk_kc[iq] # Find symmetry that transforms Q_c into q_c sym = self.qd.sym_k[iQ] U_cc = self.qd.symmetry.op_scc[sym] time_reversal = self.qd.time_reversal_k[iQ] sign = 1 - 2 * time_reversal d_c = sign * np.dot(U_cc, q_c) - Q_c assert np.allclose(d_c.round(), d_c) pd = self.pd_q[iq] N_c = pd.gd.N_c i_cG = sign * np.dot(U_cc, np.unravel_index(pd.Q_qG[0], N_c)) shift0_c = Q_c - sign * np.dot(U_cc, q_c) assert np.allclose(shift0_c.round(), shift0_c) shift0_c = shift0_c.round().astype(int) shift_c = kpt1.shift_c - kpt2.shift_c - shift0_c I_G = np.ravel_multi_index(i_cG + shift_c[:, None], N_c, 'wrap') G_Gv = pd.get_reciprocal_vectors() pos_ac = self.calc.spos_ac pos_av = np.dot(pos_ac, pd.gd.cell_cv) M_vv = np.dot(pd.gd.cell_cv.T, np.dot(U_cc.T, np.linalg.inv(pd.gd.cell_cv).T)) Q_aGii = [] for a, Q_Gii in enumerate(self.Q_qaGii[iq]): x_G = np.exp(1j * np.dot(G_Gv, (pos_av[a] - np.dot(M_vv, pos_av[a])))) U_ii = self.calc.wfs.setups[a].R_sii[sym] Q_Gii = np.dot(np.dot(U_ii, Q_Gii * x_G[:, None, None]), U_ii.T).transpose(1, 0, 2) if sign == -1: Q_Gii = Q_Gii.conj() Q_aGii.append(Q_Gii) rho_mnG = np.zeros((len(kpt1.eps_n), len(kpt2.eps_n), len(G_Gv)), complex) for m in range(len(rho_mnG)): C1_aGi = [np.dot(Qa_Gii, P1_ni[m].conj()) for Qa_Gii, P1_ni in zip(Q_aGii, kpt1.P_ani)] ut1cc_R = kpt1.ut_nR[m].conj() rho_mnG[m] = self.pair.calculate_pair_densities(ut1cc_R, C1_aGi, kpt2, pd, I_G) return rho_mnG, iq def get_screened_potential(self, ac=1.0): if hasattr(self, 'W_qGG'): return if self.wfile is not None: # Read screened potential from file try: data = np.load(self.wfile + '.npz') self.Q_qaGii = data['Q'] self.W_qGG = data['W'] self.pd_q = data['pd'] print('Reading screened potential from % s' % self.wfile, file=self.fd) except: self.calculate_screened_potential(ac) print('Saving screened potential to % s' % self.wfile, file=self.fd) if world.rank == 0: np.savez(self.wfile, Q=self.Q_qaGii, pd=self.pd_q, W=self.W_qGG) else: self.calculate_screened_potential(ac) def calculate_screened_potential(self, ac): """Calculate W_GG(q)""" chi0 = Chi0(self.calc, frequencies=[0.0], eta=0.001, ecut=self.ecut, intraband=False, hilbert=False, nbands=self.nbands, txt='chi0.txt', world=world, ) self.blockcomm = chi0.blockcomm wfs = self.calc.wfs self.Q_qaGii = [] self.W_qGG = [] self.pd_q = [] t0 = time() print('Calculating screened potential', file=self.fd) for iq, q_c in enumerate(self.qd.ibzk_kc): thisqd = KPointDescriptor([q_c]) pd = PWDescriptor(self.ecut, wfs.gd, complex, thisqd) nG = pd.ngmax chi0.Ga = self.blockcomm.rank * nG chi0.Gb = min(chi0.Ga + nG, nG) chi0_wGG = np.zeros((1, nG, nG), complex) if np.allclose(q_c, 0.0): chi0_wxvG = np.zeros((1, 2, 3, nG), complex) chi0_wvv = np.zeros((1, 3, 3), complex) else: chi0_wxvG = None chi0_wvv = None chi0._calculate(pd, chi0_wGG, chi0_wxvG, chi0_wvv, 0, self.nbands, spins='all', extend_head=False) chi0_GG = chi0_wGG[0] # Calculate eps^{-1}_GG if pd.kd.gamma: # Generate fine grid in vicinity of gamma kd = self.calc.wfs.kd N = 4 N_c = np.array([N, N, N]) if self.truncation is not None: # Only average periodic directions if trunction is used N_c[kd.N_c == 1] = 1 qf_qc = monkhorst_pack(N_c) / kd.N_c qf_qc *= 1.0e-6 U_scc = kd.symmetry.op_scc qf_qc = kd.get_ibz_q_points(qf_qc, U_scc)[0] weight_q = kd.q_weights qf_qv = 2 * np.pi * np.dot(qf_qc, pd.gd.icell_cv) a_q = np.sum(np.dot(chi0_wvv[0], qf_qv.T) * qf_qv.T, axis=0) a0_qG = np.dot(qf_qv, chi0_wxvG[0, 0]) a1_qG = np.dot(qf_qv, chi0_wxvG[0, 1]) einv_GG = np.zeros((nG, nG), complex) # W_GG = np.zeros((nG, nG), complex) for iqf in range(len(qf_qv)): chi0_GG[0] = a0_qG[iqf] chi0_GG[:, 0] = a1_qG[iqf] chi0_GG[0, 0] = a_q[iqf] sqrV_G = get_coulomb_kernel(pd, kd.N_c, truncation=self.truncation, wstc=self.wstc, q_v=qf_qv[iqf])**0.5 sqrV_G *= ac**0.5 # Multiply by adiabatic coupling e_GG = np.eye(nG) - chi0_GG * sqrV_G * sqrV_G[:, np.newaxis] einv_GG += np.linalg.inv(e_GG) * weight_q[iqf] # einv_GG = np.linalg.inv(e_GG) * weight_q[iqf] # W_GG += (einv_GG * sqrV_G * sqrV_G[:, np.newaxis] # * weight_q[iqf]) else: sqrV_G = get_coulomb_kernel(pd, self.kd.N_c, truncation=self.truncation, wstc=self.wstc)**0.5 sqrV_G *= ac**0.5 # Multiply by adiabatic coupling e_GG = np.eye(nG) - chi0_GG * sqrV_G * sqrV_G[:, np.newaxis] einv_GG = np.linalg.inv(e_GG) # W_GG = einv_GG * sqrV_G * sqrV_G[:, np.newaxis] # Now calculate W_GG if pd.kd.gamma: # Reset bare Coulomb interaction sqrV_G = get_coulomb_kernel(pd, self.kd.N_c, truncation=self.truncation, wstc=self.wstc)**0.5 W_GG = einv_GG * sqrV_G * sqrV_G[:, np.newaxis] if self.integrate_gamma != 0: # Numerical integration of Coulomb interaction at all q-points if self.integrate_gamma == 2: reduced = True else: reduced = False V0, sqrV0 = get_integrated_kernel(pd, self.kd.N_c, truncation=self.truncation, reduced=reduced, N=100) W_GG[0, 0] = einv_GG[0, 0] * V0 W_GG[0, 1:] = einv_GG[0, 1:] * sqrV0 * sqrV_G[1:] W_GG[1:, 0] = einv_GG[1:, 0] * sqrV_G[1:] * sqrV0 elif self.integrate_gamma == 0 and pd.kd.gamma: # Analytical integration at gamma bzvol = (2 * np.pi)**3 / self.vol / self.qd.nbzkpts Rq0 = (3 * bzvol / (4 * np.pi))**(1. / 3.) V0 = 16 * np.pi**2 * Rq0 / bzvol sqrV0 = (4 * np.pi)**(1.5) * Rq0**2 / bzvol / 2 W_GG[0, 0] = einv_GG[0, 0] * V0 W_GG[0, 1:] = einv_GG[0, 1:] * sqrV0 * sqrV_G[1:] W_GG[1:, 0] = einv_GG[1:, 0] * sqrV_G[1:] * sqrV0 else: pass if pd.kd.gamma: e = 1 / einv_GG[0, 0].real print(' RPA dielectric constant is: %3.3f' % e, file=self.fd) self.Q_qaGii.append(chi0.Q_aGii) self.pd_q.append(pd) self.W_qGG.append(W_GG) if iq % (self.qd.nibzkpts // 5 + 1) == 2: dt = time() - t0 tleft = dt * self.qd.nibzkpts / (iq + 1) - dt print(' Finished %s q-points in %s - Estimated %s left' % (iq + 1, timedelta(seconds=round(dt)), timedelta(seconds=round(tleft))), file=self.fd) def diagonalize(self): print('Diagonalizing Hamiltonian', file=self.fd) """The t and T represent local and global eigenstates indices respectively """ # Non-Hermitian matrix can only use linalg.eig if not self.td: print(' Using numpy.linalg.eig...', file=self.fd) print(' Eliminated %s pair orbitals' % len(self.excludef_S), file=self.fd) self.H_SS = self.collect_A_SS(self.H_sS) self.w_T = np.zeros(self.nS - len(self.excludef_S), complex) if world.rank == 0: self.H_SS = np.delete(self.H_SS, self.excludef_S, axis=0) self.H_SS = np.delete(self.H_SS, self.excludef_S, axis=1) self.w_T, self.v_ST = np.linalg.eig(self.H_SS) world.broadcast(self.w_T, 0) self.df_S = np.delete(self.df_S, self.excludef_S) self.rhoG0_S = np.delete(self.rhoG0_S, self.excludef_S) # Here the eigenvectors are returned as complex conjugated rows else: if world.size == 1: print(' Using lapack...', file=self.fd) from gpaw.utilities.lapack import diagonalize self.w_T = np.zeros(self.nS) diagonalize(self.H_sS, self.w_T) self.v_St = self.H_sS.conj().T else: print(' Using scalapack...', file=self.fd) nS = self.nS ns = -(-self.kd.nbzkpts // world.size) * (self.nv * self.nc * self.spins * (self.spinors + 1)**2) grid = BlacsGrid(world, world.size, 1) desc = grid.new_descriptor(nS, nS, ns, nS) desc2 = grid.new_descriptor(nS, nS, 2, 2) H_tmp = desc2.zeros(dtype=complex) r = Redistributor(world, desc, desc2) r.redistribute(self.H_sS, H_tmp) self.w_T = np.empty(nS) v_tmp = desc2.empty(dtype=complex) desc2.diagonalize_dc(H_tmp, v_tmp, self.w_T) r = Redistributor(grid.comm, desc2, desc) self.v_St = desc.zeros(dtype=complex) r.redistribute(v_tmp, self.v_St) self.v_St = self.v_St.conj().T if self.write_v and self.td: # Cannot use par_save without td self.par_save('v_TS.ulm', 'v_TS', self.v_St.T) return def get_bse_matrix(self, q_c=[0.0, 0.0, 0.0], direction=0, ac=1.0, readfile=None, optical=True, write_eig=None): """Calculate and diagonalize BSE matrix""" self.q_c = q_c self.direction = direction if readfile is None: self.calculate(optical=optical, ac=ac) if hasattr(self, 'w_T'): return self.diagonalize() elif readfile == 'H_SS': print('Reading Hamiltonian from file', file=self.fd) self.par_load('H_SS.ulm', 'H_SS') self.diagonalize() elif readfile == 'v_TS': print('Reading eigenstates from file', file=self.fd) self.par_load('v_TS.ulm', 'v_TS') else: raise ValueError('%s array not recognized' % readfile) # TODO: Move write_eig here return def get_vchi(self, w_w=None, eta=0.1, q_c=[0.0, 0.0, 0.0], direction=0, ac=1.0, readfile=None, optical=True, write_eig=None): """Returns v * \chi where v is the bare Coulomb interaction""" self.get_bse_matrix(q_c=q_c, direction=direction, ac=ac, readfile=readfile, optical=optical, write_eig=write_eig) w_T = self.w_T rhoG0_S = self.rhoG0_S df_S = self.df_S print('Calculating response function at %s frequency points' % len(w_w), file=self.fd) vchi_w = np.zeros(len(w_w), dtype=complex) if not self.td: C_T = np.zeros(self.nS - len(self.excludef_S), complex) if world.rank == 0: A_T = np.dot(rhoG0_S, self.v_ST) B_T = np.dot(rhoG0_S * df_S, self.v_ST) tmp = np.dot(self.v_ST.conj().T, self.v_ST) overlap_tt = np.linalg.inv(tmp) C_T = np.dot(B_T.conj(), overlap_tt.T) * A_T world.broadcast(C_T, 0) else: A_t = np.dot(rhoG0_S, self.v_St) B_t = np.dot(rhoG0_S * df_S, self.v_St) if world.size == 1: C_T = B_t.conj() * A_t else: Nv = self.nv * (self.spinors + 1) Nc = self.nc * (self.spinors + 1) Ns = self.spins nS = self.nS ns = -(-self.kd.nbzkpts // world.size) * Nv * Nc * Ns grid = BlacsGrid(world, world.size, 1) desc = grid.new_descriptor(nS, 1, ns, 1) C_t = desc.empty(dtype=complex) C_t[:, 0] = B_t.conj() * A_t C_T = desc.collect_on_master(C_t)[:, 0] if world.rank != 0: C_T = np.empty(nS, dtype=complex) world.broadcast(C_T, 0) eta /= Hartree for iw, w in enumerate(w_w / Hartree): tmp_T = 1. / (w - w_T + 1j * eta) vchi_w[iw] += np.dot(tmp_T, C_T) vchi_w *= 4 * np.pi / self.vol if not np.allclose(self.q_c, 0.0): cell_cv = self.calc.wfs.gd.cell_cv B_cv = 2 * np.pi * np.linalg.inv(cell_cv).T q_v = np.dot(q_c, B_cv) vchi_w /= np.dot(q_v, q_v) """Check f-sum rule.""" nv = self.calc.wfs.setups.nvalence dw_w = (w_w[1:] - w_w[:-1]) / Hartree wchi_w = (w_w[1:] * vchi_w[1:] + w_w[:-1] * vchi_w[:-1]) / Hartree / 2 N = -np.dot(dw_w, wchi_w.imag) * self.vol / (2 * np.pi**2) print(file=self.fd) print('Checking f-sum rule:', file=self.fd) print(' Valence = %s, N = %f' % (nv, N), file=self.fd) print(file=self.fd) if write_eig is not None: if world.rank == 0: f = open(write_eig, 'w') print('# %s eigenvalues in eV' % self.mode, file=f) for iw, w in enumerate(self.w_T * Hartree): print('%8d %12.6f %12.16f' % (iw, w.real, C_T[iw].real), file=f) f.close() return vchi_w * ac def get_dielectric_function(self, w_w=None, eta=0.1, q_c=[0.0, 0.0, 0.0], direction=0, filename='df_bse.csv', readfile=None, write_eig='eig.dat'): """Returns and writes real and imaginary part of the dielectric function. w_w: list of frequencies (eV) Dielectric function is calculated at these frequencies eta: float Lorentzian broadening of the spectrum (eV) q_c: list of three floats Wavevector in reduced units on which the response is calculated direction: int if q_c = [0, 0, 0] this gives the direction in cartesian coordinates - 0=x, 1=y, 2=z filename: str data file on which frequencies, real and imaginary part of dielectric function is written readfile: str If H_SS is given, the method will load the BSE Hamiltonian from H_SS.ulm. If v_TS is given, the method will load the eigenstates from v_TS.ulm write_eig: str File on which the BSE eigenvalues are written """ epsilon_w = -self.get_vchi(w_w=w_w, eta=eta, q_c=q_c, direction=direction, readfile=readfile, optical=True, write_eig=write_eig) epsilon_w += 1.0 if world.rank == 0 and filename is not None: f = open(filename, 'w') for iw, w in enumerate(w_w): print('%.9f, %.9f, %.9f' % (w, epsilon_w[iw].real, epsilon_w[iw].imag), file=f) f.close() world.barrier() print('Calculation completed at:', ctime(), file=self.fd) print(file=self.fd) return w_w, epsilon_w def get_eels_spectrum(self, w_w=None, eta=0.1, q_c=[0.0, 0.0, 0.0], direction=0, filename='df_bse.csv', readfile=None, write_eig='eig.dat'): """Returns and writes real and imaginary part of the dielectric function. w_w: list of frequencies (eV) Dielectric function is calculated at these frequencies eta: float Lorentzian broadening of the spectrum (eV) q_c: list of three floats Wavevector in reduced units on which the response is calculated direction: int if q_c = [0, 0, 0] this gives the direction in cartesian coordinates - 0=x, 1=y, 2=z filename: str data file on which frequencies, real and imaginary part of dielectric function is written readfile: str If H_SS is given, the method will load the BSE Hamiltonian from H_SS.ulm. If v_TS is given, the method will load the eigenstates from v_TS.ulm write_eig: str File on which the BSE eigenvalues are written """ eels_w = -self.get_vchi(w_w=w_w, eta=eta, q_c=q_c, direction=direction, readfile=readfile, optical=False, write_eig=write_eig).imag if world.rank == 0 and filename is not None: f = open(filename, 'w') for iw, w in enumerate(w_w): print('%.9f, %.9f' % (w, eels_w[iw]), file=f) f.close() world.barrier() print('Calculation completed at:', ctime(), file=self.fd) print(file=self.fd) return w_w, eels_w def get_polarizability(self, w_w=None, eta=0.1, q_c=[0.0, 0.0, 0.0], direction=0, filename='pol_bse.csv', readfile=None, pbc=None, write_eig='eig.dat'): """Calculate the polarizability alpha. In 3D the imaginary part of the polarizability is related to the dielectric function by Im(eps_M) = 4 pi * Im(alpha). In systems with reduced dimensionality the converged value of alpha is independent of the cell volume. This is not the case for eps_M, which is ill defined. A truncated Coulomb kernel will always give eps_M = 1.0, whereas the polarizability maintains its structure. pbs should be a list of booleans giving the periodic directions. By default, generate a file 'pol_bse.csv'. The three colomns are: frequency (eV), Real(alpha), Imag(alpha). The dimension of alpha is \AA to the power of non-periodic directions. """ cell_cv = self.calc.wfs.gd.cell_cv if not pbc: pbc_c = self.calc.atoms.pbc else: pbc_c = np.array(pbc) if pbc_c.all(): V = 1.0 else: V = np.abs(np.linalg.det(cell_cv[~pbc_c][:, ~pbc_c])) if self.truncation is None: optical = True else: optical = False vchi_w = self.get_vchi(w_w=w_w, eta=eta, q_c=q_c, direction=direction, readfile=readfile, optical=optical, write_eig=write_eig) alpha_w = -V * vchi_w / (4 * np.pi) alpha_w *= Bohr**(sum(~pbc_c)) if world.rank == 0 and filename is not None: fd = open(filename, 'w') for iw, w in enumerate(w_w): print('%.9f, %.9f, %.9f' % (w, alpha_w[iw].real, alpha_w[iw].imag), file=fd) fd.close() print('Calculation completed at:', ctime(), file=self.fd) print(file=self.fd) return w_w, alpha_w def get_2d_absorption(self, w_w=None, eta=0.1, q_c=[0.0, 0.0, 0.0], direction=0, filename='abs_bse.csv', readfile=None, pbc=None, write_eig='eig.dat'): """Calculate the dimensionless absorption for 2d materials. It is essentially related to the 2D polarizability \alpha_2d as ABS = 4 * np.pi * \omega * \alpha_2d / c where c is the velocity of light """ from ase.units import alpha c = 1.0 / alpha cell_cv = self.calc.wfs.gd.cell_cv if not pbc: pbc_c = self.calc.atoms.pbc else: pbc_c = np.array(pbc) assert np.sum(pbc_c) == 2 V = np.abs(np.linalg.det(cell_cv[~pbc_c][:, ~pbc_c])) vchi_w = self.get_vchi(w_w=w_w, eta=eta, q_c=q_c, direction=direction, readfile=readfile, optical=True, write_eig=write_eig) abs_w = -V * vchi_w.imag * w_w / Hartree / c if world.rank == 0 and filename is not None: fd = open(filename, 'w') for iw, w in enumerate(w_w): print('%.9f, %.9f' % (w, abs_w[iw]), file=fd) fd.close() print('Calculation completed at:', ctime(), file=self.fd) print(file=self.fd) return w_w, abs_w def par_save(self, filename, name, A_sS): import ase.io.ulm as ulm if world.size == 1: A_XS = A_sS else: A_XS = self.collect_A_SS(A_sS) if world.rank == 0: w = ulm.open(filename, 'w') if name == 'v_TS': w.write(w_T=self.w_T) # w.write(nS=self.nS) w.write(rhoG0_S=self.rhoG0_S) w.write(df_S=self.df_S) w.write(A_XS=A_XS) w.close() world.barrier() def par_load(self, filename, name): import ase.io.ulm as ulm if world.rank == 0: r = ulm.open(filename, 'r') if name == 'v_TS': self.w_T = r.w_T self.rhoG0_S = r.rhoG0_S self.df_S = r.df_S A_XS = r.A_XS r.close() else: if name == 'v_TS': self.w_T = np.zeros((self.nS), dtype=float) self.rhoG0_S = np.zeros((self.nS), dtype=complex) self.df_S = np.zeros((self.nS), dtype=float) A_XS = None world.broadcast(self.rhoG0_S, 0) world.broadcast(self.df_S, 0) if name == 'H_SS': self.H_sS = self.distribute_A_SS(A_XS) if name == 'v_TS': world.broadcast(self.w_T, 0) self.v_St = self.distribute_A_SS(A_XS, transpose=True) def collect_A_SS(self, A_sS): if world.rank == 0: A_SS = np.zeros((self.nS, self.nS), dtype=complex) A_SS[:len(A_sS)] = A_sS Ntot = len(A_sS) for rank in range(1, world.size): nkr, nk, ns = self.parallelisation_sizes(rank) buf = np.empty((ns, self.nS), dtype=complex) world.receive(buf, rank, tag=123) A_SS[Ntot:Ntot + ns] = buf Ntot += ns else: world.send(A_sS, 0, tag=123) world.barrier() if world.rank == 0: return A_SS def distribute_A_SS(self, A_SS, transpose=False): if world.rank == 0: for rank in range(0, world.size): nkr, nk, ns = self.parallelisation_sizes(rank) if rank == 0: A_sS = A_SS[0:ns] Ntot = ns else: world.send(A_SS[Ntot:Ntot + ns], rank, tag=123) Ntot += ns else: nkr, nk, ns = self.parallelisation_sizes() A_sS = np.empty((ns, self.nS), dtype=complex) world.receive(A_sS, 0, tag=123) world.barrier() if transpose: A_sS = A_sS.T return A_sS def parallelisation_sizes(self, rank=None): if rank is None: rank = world.rank nK = self.kd.nbzkpts myKsize = -(-nK // world.size) myKrange = range(rank * myKsize, min((rank + 1) * myKsize, nK)) myKsize = len(myKrange) mySsize = myKsize * self.nv * self.nc * self.spins mySsize *= (1 + self.spinors)**2 return myKrange, myKsize, mySsize def get_bse_wf(self): pass # asd = 1.0 def print_initialization(self, td, eshift, gw_skn): p = functools.partial(print, file=self.fd) p('----------------------------------------------------------') p('%s Hamiltonian' % self.mode) p('----------------------------------------------------------') p('Started at: ', ctime()) p() p('Atoms :', self.calc.atoms.get_chemical_formula(mode='hill')) p('Ground state XC functional :', self.calc.hamiltonian.xc.name) p('Valence electrons :', self.calc.wfs.setups.nvalence) p('Spinor calculations :', self.spinors) p('Number of bands :', self.calc.wfs.bd.nbands) p('Number of spins :', self.calc.wfs.nspins) p('Number of k-points :', self.kd.nbzkpts) p('Number of irreducible k-points :', self.kd.nibzkpts) p('Number of q-points :', self.qd.nbzkpts) p('Number of irreducible q-points :', self.qd.nibzkpts) p() for q in self.qd.ibzk_kc: p(' q: [%1.4f %1.4f %1.4f]' % (q[0], q[1], q[2])) p() if gw_skn is not None: p('User specified BSE bands') p('Response PW cutoff :', self.ecut * Hartree, 'eV') p('Screening bands included :', self.nbands) if len(self.val_sn) == 1: p('Valence bands :', self.val_sn[0]) p('Conduction bands :', self.con_sn[0]) else: p('Valence bands :', self.val_sn[0],self.val_sn[1]) p('Conduction bands :', self.con_sn[0],self.con_sn[1]) if eshift is not None: p('Scissors operator :', eshift * Hartree, 'eV') p('Tamm-Dancoff approximation :', td) p('Number of pair orbitals :', self.nS) p() p('Truncation of Coulomb kernel :', self.truncation) if self.integrate_gamma == 0: p('Coulomb integration scheme :', 'Analytical - gamma only') elif self.integrate_gamma == 1: p('Coulomb integration scheme :', 'Numerical - all q-points') else: pass p() p('----------------------------------------------------------') p('----------------------------------------------------------') p() p('Parallelization - Total number of CPUs : % s' % world.size) p(' Screened potential') p(' K-point/band decomposition : % s' % world.size) p(' Hamiltonian') p(' Pair orbital decomposition : % s' % world.size) p()
class UTKPointParallelSetup(TestCase): """ Setup a simple kpoint parallel calculation.""" # Number of bands nbands = 1 # Spin-polarized nspins = 1 # Mean spacing and number of grid points per axis (G x G x G) h = 0.25 / Bohr G = 48 ## Symmetry-reduction of k-points TODO #symmetry = p.usesymm #XXX 'None' is an allowed value!!! # Whether spin/k-points are equally distributed (determines nibzkpts) equipartition = None nibzkpts = None gamma = False # can't be gamma point when nibzkpts > 1 ... dtype = complex #XXX virtual so far.. # ================================= def setUp(self): for virtvar in ['equipartition']: assert getattr(self,virtvar) is not None, 'Virtual "%s"!' % virtvar kpts = {'even' : (12,1,2), \ 'prime': (23,1,1)}[self.equipartition] #primes = [i for i in xrange(50,1,-1) if ~np.any(i%np.arange(2,i)==0)] bzk_kc = kpts2ndarray(kpts) assert p.usesymm == None self.nibzkpts = len(bzk_kc) #parsize, parsize_bands = create_parsize_minbands(self.nbands, world.size) parsize, parsize_bands = 1, 1 #XXX assert self.nbands % np.prod(parsize_bands) == 0 domain_comm, kpt_comm, band_comm = distribute_cpus(parsize, parsize_bands, self.nspins, self.nibzkpts) # Set up band descriptor: self.bd = BandDescriptor(self.nbands, band_comm, p.parallel['stridebands']) # Set up grid descriptor: res, ngpts = shapeopt(300, self.G**3, 3, 0.2) cell_c = self.h * np.array(ngpts) pbc_c = (True, False, True) self.gd = GridDescriptor(ngpts, cell_c, pbc_c, domain_comm, parsize) # Create randomized gas-like atomic configuration self.atoms = create_random_atoms(self.gd) # Create setups Z_a = self.atoms.get_atomic_numbers() self.setups = Setups(Z_a, p.setups, p.basis, p.lmax, xc) self.natoms = len(self.setups) # Set up kpoint descriptor: self.kd = KPointDescriptor(bzk_kc, self.nspins) self.kd.set_symmetry(self.atoms, self.setups, p.usesymm) self.kd.set_communicator(kpt_comm) def tearDown(self): del self.bd, self.gd, self.kd del self.setups, self.atoms def get_parsizes(self): #XXX NO LONGER IN UT_HSOPS?!? # Careful, overwriting imported GPAW params may cause amnesia in Python. from gpaw import parsize, parsize_bands # Choose the largest possible parallelization over kpoint/spins test_parsize_ks_pairs = gcd(self.nspins*self.nibzkpts, world.size) remsize = world.size//test_parsize_ks_pairs # If parsize_bands is not set, choose the largest possible test_parsize_bands = parsize_bands or gcd(self.nbands, remsize) # If parsize_bands is not set, choose as few domains as possible test_parsize = parsize or (remsize//test_parsize_bands) return test_parsize, test_parsize_bands # ================================= def verify_comm_sizes(self): #TODO needs work if world.size == 1: return comm_sizes = tuple([comm.size for comm in [world, self.bd.comm, \ self.gd.comm, self.kd.comm]]) self._parinfo = '%d world, %d band, %d domain, %d kpt' % comm_sizes #self.assertEqual((self.nspins*self.nibzkpts) % self.kd.comm.size, 0) #XXX def verify_slice_consistency(self): for kpt_rank in range(self.kd.comm.size): uslice = self.kd.get_slice(kpt_rank) myus = np.arange(*uslice.indices(self.kd.nks)) for myu,u in enumerate(myus): self.assertEqual(self.kd.who_has(u), (kpt_rank, myu)) def verify_combination_consistency(self): for u in range(self.kd.nks): s, k = self.kd.what_is(u) self.assertEqual(self.kd.where_is(s, k), u) for s in range(self.kd.nspins): for k in range(self.kd.nibzkpts): u = self.kd.where_is(s, k) self.assertEqual(self.kd.what_is(u), (s,k,)) def verify_indexing_consistency(self): for u in range(self.kd.nks): kpt_rank, myu = self.kd.who_has(u) self.assertEqual(self.kd.global_index(myu, kpt_rank), u) for kpt_rank in range(self.kd.comm.size): for myu in range(self.kd.get_count(kpt_rank)): u = self.kd.global_index(myu, kpt_rank) self.assertEqual(self.kd.who_has(u), (kpt_rank, myu)) def verify_ranking_consistency(self): ranks = self.kd.get_ranks() for kpt_rank in range(self.kd.comm.size): my_indices = self.kd.get_indices(kpt_rank) matches = np.argwhere(ranks == kpt_rank).ravel() self.assertTrue((matches == my_indices).all()) for myu in range(self.kd.get_count(kpt_rank)): u = self.kd.global_index(myu, kpt_rank) self.assertEqual(my_indices[myu], u)
class G0W0(PairDensity): def __init__(self, calc, filename='gw', kpts=None, bands=None, nbands=None, ppa=False, wstc=False, ecut=150.0, eta=0.1, E0=1.0 * Hartree, domega0=0.025, omega2=10.0, world=mpi.world): PairDensity.__init__(self, calc, ecut, world=world, txt=filename + '.txt') self.filename = filename ecut /= Hartree self.ppa = ppa self.wstc = wstc self.eta = eta / Hartree self.E0 = E0 / Hartree self.domega0 = domega0 / Hartree self.omega2 = omega2 / Hartree print(' ___ _ _ _ ', file=self.fd) print(' | || | | |', file=self.fd) print(' | | || | | |', file=self.fd) print(' |__ ||_____|', file=self.fd) print(' |___| ', file=self.fd) print(file=self.fd) self.kpts = select_kpts(kpts, self.calc) if bands is None: bands = [0, self.nocc2] self.bands = bands b1, b2 = bands self.shape = shape = (self.calc.wfs.nspins, len(self.kpts), b2 - b1) self.eps_sin = np.empty(shape) # KS-eigenvalues self.f_sin = np.empty(shape) # occupation numbers self.sigma_sin = np.zeros(shape) # self-energies self.dsigma_sin = np.zeros(shape) # derivatives of self-energies self.vxc_sin = None # KS XC-contributions self.exx_sin = None # exact exchange contributions self.Z_sin = None # renormalization factors if nbands is None: nbands = int(self.vol * ecut**1.5 * 2**0.5 / 3 / pi**2) self.nbands = nbands kd = self.calc.wfs.kd self.mysKn1n2 = None # my (s, K, n1, n2) indices self.distribute_k_points_and_bands(b1, b2, kd.ibz2bz_k[self.kpts]) # Find q-vectors and weights in the IBZ: assert -1 not in kd.bz2bz_ks offset_c = 0.5 * ((kd.N_c + 1) % 2) / kd.N_c bzq_qc = monkhorst_pack(kd.N_c) + offset_c self.qd = KPointDescriptor(bzq_qc) self.qd.set_symmetry(self.calc.atoms, self.calc.wfs.setups, usesymm=self.calc.input_parameters.usesymm, N_c=self.calc.wfs.gd.N_c) assert self.calc.wfs.nspins == 1 @timer('G0W0') def calculate(self): kd = self.calc.wfs.kd self.calculate_ks_xc_contribution() self.calculate_exact_exchange() # Get KS eigenvalues and occupation numbers: b1, b2 = self.bands for i, k in enumerate(self.kpts): kpt = self.calc.wfs.kpt_u[k] self.eps_sin[0, i] = kpt.eps_n[b1:b2] self.f_sin[0, i] = kpt.f_n[b1:b2] / kpt.weight # My part of the states we want to calculate QP-energies for: mykpts = [self.get_k_point(s, K, n1, n2) for s, K, n1, n2 in self.mysKn1n2] # Loop over q in the IBZ: for pd0, W0, q_c in self.calculate_screened_potential(): for kpt1 in mykpts: K2 = kd.find_k_plus_q(q_c, [kpt1.K])[0] kpt2 = self.get_k_point(0, K2, 0, self.nbands) k1 = kd.bz2ibz_k[kpt1.K] i = self.kpts.index(k1) self.calculate_q(i, kpt1, kpt2, pd0, W0) self.world.sum(self.sigma_sin) self.world.sum(self.dsigma_sin) self.Z_sin = 1 / (1 - self.dsigma_sin) self.qp_sin = self.eps_sin + self.Z_sin * (self.sigma_sin + self.exx_sin - self.vxc_sin) results = {'f': self.f_sin, 'eps': self.eps_sin * Hartree, 'vxc': self.vxc_sin * Hartree, 'exx': self.exx_sin * Hartree, 'sigma': self.sigma_sin * Hartree, 'Z': self.Z_sin, 'qp': self.qp_sin * Hartree} self.print_results(results) return results def calculate_q(self, i, kpt1, kpt2, pd0, W0): wfs = self.calc.wfs N_c = pd0.gd.N_c i_cG = self.sign * np.dot(self.U_cc, np.unravel_index(pd0.Q_qG[0], N_c)) q_c = wfs.kd.bzk_kc[kpt2.K] - wfs.kd.bzk_kc[kpt1.K] q0 = np.allclose(q_c, 0) shift0_c = q_c - self.sign * np.dot(self.U_cc, pd0.kd.bzk_kc[0]) assert np.allclose(shift0_c.round(), shift0_c) shift0_c = shift0_c.round().astype(int) shift_c = kpt1.shift_c - kpt2.shift_c - shift0_c I_G = np.ravel_multi_index(i_cG + shift_c[:, None], N_c, 'wrap') G_Gv = pd0.G_Qv[pd0.Q_qG[0]] + pd0.K_qv[0] pos_av = np.dot(self.spos_ac, pd0.gd.cell_cv) M_vv = np.dot(pd0.gd.cell_cv.T, np.dot(self.U_cc.T, np.linalg.inv(pd0.gd.cell_cv).T)) Q_aGii = [] for a, Q_Gii in enumerate(self.Q_aGii): x_G = np.exp(1j * np.dot(G_Gv, (pos_av[a] - self.sign * np.dot(M_vv, pos_av[a])))) U_ii = self.calc.wfs.setups[a].R_sii[self.s] Q_Gii = np.dot(np.dot(U_ii, Q_Gii * x_G[:, None, None]), U_ii.T).transpose(1, 0, 2) Q_aGii.append(Q_Gii) if debug: self.check(i_cG, shift0_c, N_c, q_c, Q_aGii) if self.ppa: calculate_sigma = self.calculate_sigma_ppa else: calculate_sigma = self.calculate_sigma for n in range(kpt1.n2 - kpt1.n1): ut1cc_R = kpt1.ut_nR[n].conj() eps1 = kpt1.eps_n[n] C1_aGi = [np.dot(Q_Gii, P1_ni[n].conj()) for Q_Gii, P1_ni in zip(Q_aGii, kpt1.P_ani)] n_mG = self.calculate_pair_densities(ut1cc_R, C1_aGi, kpt2, pd0, I_G) if self.sign == 1: n_mG = n_mG.conj() if q0: n_mG[:, 0] = 0 m = n + kpt1.n1 - kpt2.n1 if 0 <= m < len(n_mG): n_mG[m, 0] = 1.0 f_m = kpt2.f_n deps_m = eps1 - kpt2.eps_n sigma, dsigma = calculate_sigma(n_mG, deps_m, f_m, W0) nn = kpt1.n1 + n - self.bands[0] self.sigma_sin[kpt1.s, i, nn] += sigma self.dsigma_sin[kpt1.s, i, nn] += dsigma def check(self, i_cG, shift0_c, N_c, q_c, Q_aGii): I0_G = np.ravel_multi_index(i_cG - shift0_c[:, None], N_c, 'wrap') qd1 = KPointDescriptor([q_c]) pd1 = PWDescriptor(self.ecut, self.calc.wfs.gd, complex, qd1) G_I = np.empty(N_c.prod(), int) G_I[:] = -1 I1_G = pd1.Q_qG[0] G_I[I1_G] = np.arange(len(I0_G)) G_G = G_I[I0_G] assert len(I0_G) == len(I1_G) assert (G_G >= 0).all() for a, Q_Gii in enumerate(self.initialize_paw_corrections(pd1)): e = abs(Q_aGii[a] - Q_Gii[G_G]).max() assert e < 1e-12 @timer('Sigma') def calculate_sigma(self, n_mG, deps_m, f_m, C_swGG): o_m = abs(deps_m) # Add small number to avoid zeros for degenerate states: sgn_m = np.sign(deps_m + 1e-15) # Pick +i*eta or -i*eta: s_m = (1 + sgn_m * np.sign(0.5 - f_m)).astype(int) // 2 beta = (2**0.5 - 1) * self.domega0 / self.omega2 w_m = (o_m / (self.domega0 + beta * o_m)).astype(int) o1_m = self.omega_w[w_m] o2_m = self.omega_w[w_m + 1] x = 1.0 / (self.qd.nbzkpts * 2 * pi * self.vol) sigma = 0.0 dsigma = 0.0 for o, o1, o2, sgn, s, w, n_G in zip(o_m, o1_m, o2_m, sgn_m, s_m, w_m, n_mG): C1_GG = C_swGG[s][w] C2_GG = C_swGG[s][w + 1] p = x * sgn sigma1 = p * np.dot(np.dot(n_G, C1_GG), n_G.conj()).imag sigma2 = p * np.dot(np.dot(n_G, C2_GG), n_G.conj()).imag sigma += ((o - o1) * sigma2 + (o2 - o) * sigma1) / (o2 - o1) dsigma += sgn * (sigma2 - sigma1) / (o2 - o1) return sigma, dsigma @timer('W') def calculate_screened_potential(self): print('Calulating screened Coulomb 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: 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, **parameters) 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) for iq, q_c in enumerate(self.qd.ibzk_kc): if self.wstc: wstc = WignerSeitzTruncatedCoulomb( self.calc.wfs.gd.cell_cv, self.calc.wfs.kd.N_c, chi0.fd) else: wstc = None pd, chi0_wGG = chi0.calculate(q_c)[:2] self.Q_aGii = chi0.Q_aGii W = self.calculate_w(pd, chi0_wGG, q_c, htp, htm, wstc) 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) @timer('WW') def calculate_w(self, pd, chi0_wGG, q_c, htp, htm, wstc): if self.wstc: iG_G = (wstc.get_potential(pd) / (4 * pi))**0.5 if np.allclose(q_c, 0): chi0_wGG[:, 0] = 0.0 chi0_wGG[:, :, 0] = 0.0 G0inv = 0.0 G20inv = 0.0 else: G0inv = None G20inv = None else: if np.allclose(q_c, 0): dq3 = (2 * pi)**3 / (self.qd.nbzkpts * self.vol) qc = (dq3 / 4 / pi * 3)**(1 / 3) G0inv = 2 * pi * qc**2 / dq3 G20inv = 4 * pi * qc / dq3 G_G = pd.G2_qG[0]**0.5 G_G[0] = 1 iG_G = 1 / G_G else: iG_G = pd.G2_qG[0]**-0.5 G0inv = None G20inv = None delta_GG = np.eye(len(iG_G)) if self.ppa: return self.ppa_w(chi0_wGG, iG_G, delta_GG, G0inv, G20inv, q_c) self.timer.start('Dyson eq.') # Calculate W and store it in chi0_wGG ndarray: for chi0_GG in chi0_wGG: e_GG = (delta_GG - 4 * pi * chi0_GG * iG_G * iG_G[:, np.newaxis]) W_GG = chi0_GG W_GG[:] = 4 * pi * (np.linalg.inv(e_GG) - delta_GG) * iG_G * iG_G[:, np.newaxis] if np.allclose(q_c, 0): W_GG[0, 0] *= G20inv W_GG[1:, 0] *= G0inv W_GG[0, 1:] *= G0inv Wp_wGG = chi0_wGG.copy() Wm_wGG = chi0_wGG with self.timer('Hilbert transform'): htp(Wp_wGG) htm(Wm_wGG) self.timer.stop('Dyson eq.') return [Wp_wGG, Wm_wGG] @timer('Kohn-Sham XC-contribution') def calculate_ks_xc_contribution(self): name = self.filename + '.vxc.npy' fd = opencew(name) if fd is None: print('Reading Kohn-Sham XC contribution from file:', name, file=self.fd) with open(name) as fd: self.vxc_sin = np.load(fd) assert self.vxc_sin.shape == self.shape, self.vxc_sin.shape return print('Calculating Kohn-Sham XC contribution', file=self.fd) vxc_skn = vxc(self.calc, self.calc.hamiltonian.xc) / Hartree n1, n2 = self.bands self.vxc_sin = vxc_skn[:, self.kpts, n1:n2] np.save(fd, self.vxc_sin) @timer('EXX') def calculate_exact_exchange(self): name = self.filename + '.exx.npy' fd = opencew(name) if fd is None: print('Reading EXX contribution from file:', name, file=self.fd) with open(name) as fd: self.exx_sin = np.load(fd) assert self.exx_sin.shape == self.shape, self.exx_sin.shape return print('Calculating EXX contribution', file=self.fd) exx = EXX(self.calc, kpts=self.kpts, bands=self.bands, txt=self.filename + '.exx.txt', timer=self.timer) exx.calculate() self.exx_sin = exx.get_eigenvalue_contributions() / Hartree np.save(fd, self.exx_sin) def print_results(self, results): description = ['f: Occupation numbers', 'eps: KS-eigenvalues [eV]', 'vxc: KS vxc [eV]', 'exx: Exact exchange [eV]', 'sigma: Self-energies [eV]', 'Z: Renormalization factors', 'qp: QP-energies [eV]'] print('\nResults:', file=self.fd) for line in description: print(line, file=self.fd) b1, b2 = self.bands names = [line.split(':', 1)[0] for line in description] ibzk_kc = self.calc.wfs.kd.ibzk_kc for s in range(self.calc.wfs.nspins): for i, ik in enumerate(self.kpts): print('\nk-point ' + '{0} ({1}): ({2:.3f}, {3:.3f}, {4:.3f})'.format( i, ik, *ibzk_kc[ik]), file=self.fd) print('band' + ''.join('{0:>8}'.format(name) for name in names), file=self.fd) for n in range(b2 - b1): print('{0:4}'.format(n + b1) + ''.join('{0:8.3f}'.format(results[name][s, i, n]) for name in names), file=self.fd) self.timer.write(self.fd) @timer('PPA') def ppa_w(self, chi0_wGG, iG_G, delta_GG, G0inv, G20inv, q_c): einv_wGG = [] for chi0_GG in chi0_wGG: e_GG = (delta_GG - 4 * pi * chi0_GG * iG_G * iG_G[:, np.newaxis]) einv_wGG.append(np.linalg.inv(e_GG) - delta_GG) if self.wstc and np.allclose(q_c, 0): einv_wGG[0][0] = 42 einv_wGG[0][:, 0] = 42 omegat_GG = self.E0 * np.sqrt(einv_wGG[1] / (einv_wGG[0] - einv_wGG[1])) R_GG = -0.5 * omegat_GG * einv_wGG[0] W_GG = 4 * pi**2 * R_GG * iG_G * iG_G[:, np.newaxis] if np.allclose(q_c, 0): W_GG[0, 0] *= G20inv W_GG[1:, 0] *= G0inv W_GG[0, 1:] *= G0inv return [W_GG, omegat_GG] @timer('PPA-Sigma') def calculate_sigma_ppa(self, n_mG, deps_m, f_m, W): W_GG, omegat_GG = W deps_mGG = deps_m[:, np.newaxis, np.newaxis] sign_mGG = 2 * f_m[:, np.newaxis, np.newaxis] - 1 x1_mGG = 1 / (deps_mGG + omegat_GG - 1j * self.eta) x2_mGG = 1 / (deps_mGG - omegat_GG + 1j * self.eta) x3_mGG = 1 / (deps_mGG + omegat_GG - 1j * self.eta * sign_mGG) x4_mGG = 1 / (deps_mGG - omegat_GG - 1j * self.eta * sign_mGG) x_mGG = W_GG * (sign_mGG * (x1_mGG - x2_mGG) + x3_mGG + x4_mGG) dx_mGG = W_GG * (sign_mGG * (x1_mGG**2 - x2_mGG**2) + x3_mGG**2 + x4_mGG**2) sigma = 0.0 dsigma = 0.0 for m in range(np.shape(n_mG)[0]): nW_mG = np.dot(n_mG[m], x_mGG[m]) sigma += np.vdot(n_mG[m], nW_mG).real nW_mG = np.dot(n_mG[m], dx_mGG[m]) dsigma -= np.vdot(n_mG[m], nW_mG).real x = 1 / (self.qd.nbzkpts * 2 * pi * self.vol) return x * sigma, x * dsigma
def initialize(self, atoms=None): """Inexpensive initialization.""" if atoms is None: atoms = self.atoms else: # Save the state of the atoms: self.atoms = atoms.copy() par = self.input_parameters world = par.communicator if world is None: world = mpi.world elif hasattr(world, 'new_communicator'): # Check for whether object has correct type already # # Using isinstance() is complicated because of all the # combinations, serial/parallel/debug... pass else: # world should be a list of ranks: world = mpi.world.new_communicator(np.asarray(world)) self.wfs.world = world if 'txt' in self._changed_keywords: self.set_txt(par.txt) self.verbose = par.verbose natoms = len(atoms) cell_cv = atoms.get_cell() / Bohr pbc_c = atoms.get_pbc() Z_a = atoms.get_atomic_numbers() magmom_av = atoms.get_initial_magnetic_moments() self.check_atoms() # Generate new xc functional only when it is reset by set # XXX sounds like this should use the _changed_keywords dictionary. if self.hamiltonian is None or self.hamiltonian.xc is None: if isinstance(par.xc, str): xc = XC(par.xc) else: xc = par.xc else: xc = self.hamiltonian.xc mode = par.mode if mode == 'fd': mode = FD() elif mode == 'pw': mode = pw.PW() elif mode == 'lcao': mode = LCAO() else: assert hasattr(mode, 'name'), str(mode) if xc.orbital_dependent and mode.name == 'lcao': raise NotImplementedError('LCAO mode does not support ' 'orbital-dependent XC functionals.') if par.realspace is None: realspace = (mode.name != 'pw') else: realspace = par.realspace if mode.name == 'pw': assert not realspace if par.filter is None and mode.name != 'pw': gamma = 1.6 if par.gpts is not None: h = ((np.linalg.inv(cell_cv)**2).sum(0)**-0.5 / par.gpts).max() else: h = (par.h or 0.2) / Bohr def filter(rgd, rcut, f_r, l=0): gcut = np.pi / h - 2 / rcut / gamma f_r[:] = rgd.filter(f_r, rcut * gamma, gcut, l) else: filter = par.filter setups = Setups(Z_a, par.setups, par.basis, par.lmax, xc, filter, world) if magmom_av.ndim == 1: collinear = True magmom_av, magmom_a = np.zeros((natoms, 3)), magmom_av magmom_av[:, 2] = magmom_a else: collinear = False magnetic = magmom_av.any() spinpol = par.spinpol if par.hund: if natoms != 1: raise ValueError('hund=True arg only valid for single atoms!') spinpol = True magmom_av[0] = (0, 0, setups[0].get_hunds_rule_moment(par.charge)) if spinpol is None: spinpol = magnetic elif magnetic and not spinpol: raise ValueError('Non-zero initial magnetic moment for a ' + 'spin-paired calculation!') if collinear: nspins = 1 + int(spinpol) ncomp = 1 else: nspins = 1 ncomp = 2 if par.usesymm != 'default': warnings.warn('Use "symmetry" keyword instead of ' + '"usesymm" keyword') par.symmetry = usesymm2symmetry(par.usesymm) symm = par.symmetry if symm == 'off': symm = {'point_group': False, 'time_reversal': False} bzkpts_kc = kpts2ndarray(par.kpts, self.atoms) kd = KPointDescriptor(bzkpts_kc, nspins, collinear) m_av = magmom_av.round(decimals=3) # round off id_a = zip(setups.id_a, *m_av.T) symmetry = Symmetry(id_a, cell_cv, atoms.pbc, **symm) kd.set_symmetry(atoms, symmetry, comm=world) setups.set_symmetry(symmetry) if par.gpts is not None: N_c = np.array(par.gpts) else: h = par.h if h is not None: h /= Bohr N_c = get_number_of_grid_points(cell_cv, h, mode, realspace, kd.symmetry) symmetry.check_grid(N_c) width = par.width if width is None: if pbc_c.any(): width = 0.1 # eV else: width = 0.0 else: assert par.occupations is None if hasattr(self, 'time') or par.dtype == complex: dtype = complex else: if kd.gamma: dtype = float else: dtype = complex nao = setups.nao nvalence = setups.nvalence - par.charge M_v = magmom_av.sum(0) M = np.dot(M_v, M_v)**0.5 nbands = par.nbands orbital_free = any(setup.orbital_free for setup in setups) if orbital_free: nbands = 1 if isinstance(nbands, basestring): if nbands[-1] == '%': basebands = int(nvalence + M + 0.5) // 2 nbands = int((float(nbands[:-1]) / 100) * basebands) else: raise ValueError('Integer Expected: Only use a string ' 'if giving a percentage of occupied bands') if nbands is None: nbands = 0 for setup in setups: nbands_from_atom = setup.get_default_nbands() # Any obscure setup errors? if nbands_from_atom < -(-setup.Nv // 2): raise ValueError('Bad setup: This setup requests %d' ' bands but has %d electrons.' % (nbands_from_atom, setup.Nv)) nbands += nbands_from_atom nbands = min(nao, nbands) elif nbands > nao and mode.name == 'lcao': raise ValueError('Too many bands for LCAO calculation: ' '%d bands and only %d atomic orbitals!' % (nbands, nao)) if nvalence < 0: raise ValueError( 'Charge %f is not possible - not enough valence electrons' % par.charge) if nbands <= 0: nbands = int(nvalence + M + 0.5) // 2 + (-nbands) if nvalence > 2 * nbands and not orbital_free: raise ValueError('Too few bands! Electrons: %f, bands: %d' % (nvalence, nbands)) nbands *= ncomp if par.width is not None: self.text('**NOTE**: please start using ' 'occupations=FermiDirac(width).') if par.fixmom: self.text('**NOTE**: please start using ' 'occupations=FermiDirac(width, fixmagmom=True).') if self.occupations is None: if par.occupations is None: # Create object for occupation numbers: if orbital_free: width = 0.0 # even for PBC self.occupations = occupations.TFOccupations( width, par.fixmom) else: self.occupations = occupations.FermiDirac( width, par.fixmom) else: self.occupations = par.occupations # If occupation numbers are changed, and we have wave functions, # recalculate the occupation numbers if self.wfs is not None and not isinstance(self.wfs, EmptyWaveFunctions): self.occupations.calculate(self.wfs) self.occupations.magmom = M_v[2] cc = par.convergence if mode.name == 'lcao': niter_fixdensity = 0 else: niter_fixdensity = None if self.scf is None: force_crit = cc['forces'] if force_crit is not None: force_crit /= Hartree / Bohr self.scf = SCFLoop(cc['eigenstates'] / Hartree**2 * nvalence, cc['energy'] / Hartree * max(nvalence, 1), cc['density'] * nvalence, par.maxiter, par.fixdensity, niter_fixdensity, force_crit) parsize_kpt = par.parallel['kpt'] parsize_domain = par.parallel['domain'] parsize_bands = par.parallel['band'] if not realspace: pbc_c = np.ones(3, bool) if not self.wfs: if parsize_domain == 'domain only': # XXX this was silly! parsize_domain = world.size parallelization = mpi.Parallelization(world, nspins * kd.nibzkpts) ndomains = None if parsize_domain is not None: ndomains = np.prod(parsize_domain) if mode.name == 'pw': if ndomains > 1: raise ValueError('Planewave mode does not support ' 'domain decomposition.') ndomains = 1 parallelization.set(kpt=parsize_kpt, domain=ndomains, band=parsize_bands) comms = parallelization.build_communicators() domain_comm = comms['d'] kpt_comm = comms['k'] band_comm = comms['b'] kptband_comm = comms['D'] domainband_comm = comms['K'] self.comms = comms kd.set_communicator(kpt_comm) parstride_bands = par.parallel['stridebands'] # Unfortunately we need to remember that we adjusted the # number of bands so we can print a warning if it differs # from the number specified by the user. (The number can # be inferred from the input parameters, but it's tricky # because we allow negative numbers) self.nbands_parallelization_adjustment = -nbands % band_comm.size nbands += self.nbands_parallelization_adjustment # I would like to give the following error message, but apparently # there are cases, e.g. gpaw/test/gw_ppa.py, which involve # nbands > nao and are supposed to work that way. #if nbands > nao: # raise ValueError('Number of bands %d adjusted for band ' # 'parallelization %d exceeds number of atomic ' # 'orbitals %d. This problem can be fixed ' # 'by reducing the number of bands a bit.' # % (nbands, band_comm.size, nao)) bd = BandDescriptor(nbands, band_comm, parstride_bands) if (self.density is not None and self.density.gd.comm.size != domain_comm.size): # Domain decomposition has changed, so we need to # reinitialize density and hamiltonian: if par.fixdensity: raise RuntimeError( 'Density reinitialization conflict ' + 'with "fixdensity" - specify domain decomposition.') self.density = None self.hamiltonian = None # Construct grid descriptor for coarse grids for wave functions: gd = self.grid_descriptor_class(N_c, cell_cv, pbc_c, domain_comm, parsize_domain) # do k-point analysis here? XXX args = (gd, nvalence, setups, bd, dtype, world, kd, kptband_comm, self.timer) if par.parallel['sl_auto']: # Choose scalapack parallelization automatically for key, val in par.parallel.items(): if (key.startswith('sl_') and key != 'sl_auto' and val is not None): raise ValueError("Cannot use 'sl_auto' together " "with '%s'" % key) max_scalapack_cpus = bd.comm.size * gd.comm.size nprow = max_scalapack_cpus npcol = 1 # Get a sort of reasonable number of columns/rows while npcol < nprow and nprow % 2 == 0: npcol *= 2 nprow //= 2 assert npcol * nprow == max_scalapack_cpus # ScaLAPACK creates trouble if there aren't at least a few # whole blocks; choose block size so there will always be # several blocks. This will crash for small test systems, # but so will ScaLAPACK in any case blocksize = min(-(-nbands // 4), 64) sl_default = (nprow, npcol, blocksize) else: sl_default = par.parallel['sl_default'] if mode.name == 'lcao': # Layouts used for general diagonalizer sl_lcao = par.parallel['sl_lcao'] if sl_lcao is None: sl_lcao = sl_default lcaoksl = get_KohnSham_layouts(sl_lcao, 'lcao', gd, bd, domainband_comm, dtype, nao=nao, timer=self.timer) self.wfs = mode(collinear, lcaoksl, *args) elif mode.name == 'fd' or mode.name == 'pw': # buffer_size keyword only relevant for fdpw buffer_size = par.parallel['buffer_size'] # Layouts used for diagonalizer sl_diagonalize = par.parallel['sl_diagonalize'] if sl_diagonalize is None: sl_diagonalize = sl_default diagksl = get_KohnSham_layouts( sl_diagonalize, 'fd', # XXX # choice of key 'fd' not so nice gd, bd, domainband_comm, dtype, buffer_size=buffer_size, timer=self.timer) # Layouts used for orthonormalizer sl_inverse_cholesky = par.parallel['sl_inverse_cholesky'] if sl_inverse_cholesky is None: sl_inverse_cholesky = sl_default if sl_inverse_cholesky != sl_diagonalize: message = 'sl_inverse_cholesky != sl_diagonalize ' \ 'is not implemented.' raise NotImplementedError(message) orthoksl = get_KohnSham_layouts(sl_inverse_cholesky, 'fd', gd, bd, domainband_comm, dtype, buffer_size=buffer_size, timer=self.timer) # Use (at most) all available LCAO for initialization lcaonbands = min(nbands, nao) try: lcaobd = BandDescriptor(lcaonbands, band_comm, parstride_bands) except RuntimeError: initksl = None else: # Layouts used for general diagonalizer # (LCAO initialization) sl_lcao = par.parallel['sl_lcao'] if sl_lcao is None: sl_lcao = sl_default initksl = get_KohnSham_layouts(sl_lcao, 'lcao', gd, lcaobd, domainband_comm, dtype, nao=nao, timer=self.timer) if hasattr(self, 'time'): assert mode.name == 'fd' from gpaw.tddft import TimeDependentWaveFunctions self.wfs = TimeDependentWaveFunctions( par.stencils[0], diagksl, orthoksl, initksl, gd, nvalence, setups, bd, world, kd, kptband_comm, self.timer) elif mode.name == 'fd': self.wfs = mode(par.stencils[0], diagksl, orthoksl, initksl, *args) else: assert mode.name == 'pw' self.wfs = mode(diagksl, orthoksl, initksl, *args) else: self.wfs = mode(self, *args) else: self.wfs.set_setups(setups) if not self.wfs.eigensolver: # Number of bands to converge: nbands_converge = cc['bands'] if nbands_converge == 'all': nbands_converge = nbands elif nbands_converge != 'occupied': assert isinstance(nbands_converge, int) if nbands_converge < 0: nbands_converge += nbands eigensolver = get_eigensolver(par.eigensolver, mode, par.convergence) eigensolver.nbands_converge = nbands_converge # XXX Eigensolver class doesn't define an nbands_converge property if isinstance(xc, SIC): eigensolver.blocksize = 1 self.wfs.set_eigensolver(eigensolver) if self.density is None: gd = self.wfs.gd if par.stencils[1] != 9: # Construct grid descriptor for fine grids for densities # and potentials: finegd = gd.refine() else: # Special case (use only coarse grid): finegd = gd if realspace: self.density = RealSpaceDensity( gd, finegd, nspins, par.charge + setups.core_charge, collinear, par.stencils[1]) else: self.density = pw.ReciprocalSpaceDensity( gd, finegd, nspins, par.charge + setups.core_charge, collinear) self.density.initialize(setups, self.timer, magmom_av, par.hund) self.density.set_mixer(par.mixer) if self.hamiltonian is None: gd, finegd = self.density.gd, self.density.finegd if realspace: self.hamiltonian = RealSpaceHamiltonian( gd, finegd, nspins, setups, self.timer, xc, world, self.wfs.kptband_comm, par.external, collinear, par.poissonsolver, par.stencils[1]) else: self.hamiltonian = pw.ReciprocalSpaceHamiltonian( gd, finegd, self.density.pd2, self.density.pd3, nspins, setups, self.timer, xc, world, self.wfs.kptband_comm, par.external, collinear) xc.initialize(self.density, self.hamiltonian, self.wfs, self.occupations) self.text() self.print_memory_estimate(self.txt, maxdepth=memory_estimate_depth) self.txt.flush() self.timer.print_info(self) if dry_run: self.dry_run() if realspace and \ self.hamiltonian.poisson.get_description() == 'FDTD+TDDFT': self.hamiltonian.poisson.set_density(self.density) self.hamiltonian.poisson.print_messages(self.text) self.txt.flush() self.initialized = True self._changed_keywords.clear()
class GWQEHCorrection(PairDensity): def __init__(self, calc, gwfile, filename=None, kpts=[0], bands=None, structure=None, d=None, layer=0, dW_qw=None, qqeh=None, wqeh=None, txt=sys.stdout, world=mpi.world, domega0=0.025, omega2=10.0, eta=0.1, include_q0=True, metal=False): """ Class for calculating quasiparticle energies of van der Waals heterostructures using the GW approximation for the self-energy. The quasiparticle energy correction due to increased screening from surrounding layers is obtained from the QEH model. Parameters: calc: str or PAW object GPAW calculator object or filename of saved calculator object. gwfile: str name of gw results file from the monolayer calculation filename: str filename for gwqeh output kpts: list List of indices of sthe IBZ k-points to calculate the quasi particle energies for. Set to [0] by default since the QP correction is generally the same for all k. bands: tuple Range of band indices, like (n1, n2+1), to calculate the quasi particle energies for. Note that the second band index is not included. Should be the same as used for the GW calculation. structure: list of str Heterostructure set up. Each entry should consist of number of layers + chemical formula. For example: ['3H-MoS2', graphene', '10H-WS2'] gives 3 layers of H-MoS2, 1 layer of graphene and 10 layers of H-WS2. The name of the layers should correspond to building block files: "<name>-chi.pckl" in the local repository. d: array of floats Interlayer distances for neighboring layers in Ang. Length of array = number of layers - 1 layer: int index of layer to calculate QP correction for. dW_qw: 2D array of floats dimension q X w Change in screened interaction. Should be set to None to calculate dW directly from buildingblocks. qqeh: array of floats q-grid used for dW_qw (only needed if dW is given by hand). wqeh: array of floats w-grid used for dW_qw. So far this have to be the same as for the GWQEH calculation. (only needed if dW is given by hand). domega0: float Minimum frequency step (in eV) used in the generation of the non- linear frequency grid. omega2: float Control parameter for the non-linear frequency grid, equal to the frequency where the grid spacing has doubled in size. eta: float Broadening parameter. include_q0: bool include q=0 in W or not. if True an integral arround q=0 is performed, if False the q=0 contribution is set to zero. metal: bool If True, the point at q=0 is omitted when averaging the screened potential close to q=0. """ self.gwfile = gwfile self.inputcalc = calc # Set low ecut in order to use PairDensity object since only # G=0 is needed. self.ecut = 1. PairDensity.__init__(self, calc, ecut=self.ecut, world=world, txt=filename + '.txt') self.filename = filename self.ecut /= Hartree self.eta = eta / Hartree self.domega0 = domega0 / Hartree self.omega2 = omega2 / Hartree self.kpts = list(select_kpts(kpts, self.calc)) if bands is None: bands = [0, self.nocc2] self.bands = bands b1, b2 = bands self.shape = shape = (self.calc.wfs.nspins, len(self.kpts), b2 - b1) self.eps_sin = np.empty(shape) # KS-eigenvalues self.f_sin = np.empty(shape) # occupation numbers self.sigma_sin = np.zeros(shape) # self-energies self.dsigma_sin = np.zeros(shape) # derivatives of self-energies self.Z_sin = None # renormalization factors self.qp_sin = None self.Qp_sin = None self.ecutnb = 150 / Hartree vol = abs(np.linalg.det(self.calc.wfs.gd.cell_cv)) self.vol = vol self.nbands = min(self.calc.get_number_of_bands(), int(vol * (self.ecutnb)**1.5 * 2**0.5 / 3 / pi**2)) self.nspins = self.calc.wfs.nspins kd = self.calc.wfs.kd self.mysKn1n2 = None # my (s, K, n1, n2) indices self.distribute_k_points_and_bands(b1, b2, kd.ibz2bz_k[self.kpts]) # Find q-vectors and weights in the IBZ: assert -1 not in kd.bz2bz_ks offset_c = 0.5 * ((kd.N_c + 1) % 2) / kd.N_c bzq_qc = monkhorst_pack(kd.N_c) + offset_c self.qd = KPointDescriptor(bzq_qc) self.qd.set_symmetry(self.calc.atoms, kd.symmetry) # frequency grid omax = self.find_maximum_frequency() self.omega_w = frequency_grid(self.domega0, self.omega2, omax) self.nw = len(self.omega_w) self.wsize = 2 * self.nw # Calculate screened potential of Heterostructure if dW_qw is None: try: self.qqeh, self.wqeh, dW_qw = pickle.load( open(filename + '_dW_qw.pckl', 'rb')) except: dW_qw = self.calculate_W_QEH(structure, d, layer) else: self.qqeh = qqeh self.wqeh = None # wqeh self.dW_qw = self.get_W_on_grid(dW_qw, include_q0=include_q0, metal=metal) assert self.nw == self.dW_qw.shape[1], \ ('Frequency grids doesnt match!') self.htp = HilbertTransform(self.omega_w, self.eta, gw=True) self.htm = HilbertTransform(self.omega_w, -self.eta, gw=True) self.complete = False self.nq = 0 if self.load_state_file(): if self.complete: print('Self-energy loaded from file', file=self.fd) def calculate_QEH(self): print('Calculating QEH self-energy contribution', file=self.fd) kd = self.calc.wfs.kd # Reset calculation self.sigma_sin = np.zeros(self.shape) # self-energies self.dsigma_sin = np.zeros(self.shape) # derivatives of self-energies # Get KS eigenvalues and occupation numbers: b1, b2 = self.bands nibzk = self.calc.wfs.kd.nibzkpts for i, k in enumerate(self.kpts): for s in range(self.nspins): u = s * nibzk + k kpt = self.calc.wfs.kpt_u[u] self.eps_sin[s, i] = kpt.eps_n[b1:b2] self.f_sin[s, i] = kpt.f_n[b1:b2] / kpt.weight # My part of the states we want to calculate QP-energies for: mykpts = [ self.get_k_point(s, K, n1, n2) for s, K, n1, n2 in self.mysKn1n2 ] kplusqdone_u = [set() for kpt in mykpts] Nq = len((self.qd.ibzk_kc)) for iq, q_c in enumerate(self.qd.ibzk_kc): self.nq = iq nq = iq self.save_state_file() qcstr = '(' + ', '.join(['%.3f' % x for x in q_c]) + ')' print('Calculating contribution from IBZ q-point #%d/%d q_c=%s' % (nq, Nq, qcstr), file=self.fd) rcell_cv = 2 * pi * np.linalg.inv(self.calc.wfs.gd.cell_cv).T q_abs = np.linalg.norm(np.dot(q_c, rcell_cv)) # Screened potential dW_w = self.dW_qw[nq] dW_w = dW_w[:, np.newaxis, np.newaxis] L = abs(self.calc.wfs.gd.cell_cv[2, 2]) dW_w *= L nw = self.nw Wpm_w = np.zeros([2 * nw, 1, 1], dtype=complex) Wpm_w[:nw] = dW_w Wpm_w[nw:] = Wpm_w[0:nw] with self.timer('Hilbert transform'): self.htp(Wpm_w[:nw]) self.htm(Wpm_w[nw:]) qd = KPointDescriptor([q_c]) pd0 = PWDescriptor(self.ecut, self.calc.wfs.gd, complex, qd) # modify pd0 by hand - only G=0 component is needed pd0.G_Qv = np.array([1e-17, 1e-17, 1e-17])[np.newaxis, :] pd0.Q_qG = [np.array([0], dtype='int32')] pd0.ngmax = 1 G_Gv = pd0.get_reciprocal_vectors() self.Q_aGii = self.initialize_paw_corrections(pd0) # 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] Q2s = set() for s, Q2 in enumerate(self.qd.bz2bz_ks[Q1]): if Q2 >= 0 and Q2 not in Q2s: Q2s.add(Q2) for Q2 in Q2s: s = self.qd.sym_k[Q2] self.s = s 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(U_cc, q_c) - Q_c assert np.allclose(d_c.round(), d_c) for u1, kpt1 in enumerate(mykpts): K2 = kd.find_k_plus_q(Q_c, [kpt1.K])[0] kpt2 = self.get_k_point(kpt1.s, K2, 0, self.nbands, block=True) k1 = kd.bz2ibz_k[kpt1.K] i = self.kpts.index(k1) N_c = pd0.gd.N_c i_cG = self.sign * np.dot( U_cc, np.unravel_index(pd0.Q_qG[0], N_c)) k1_c = kd.bzk_kc[kpt1.K] k2_c = kd.bzk_kc[K2] # This is the q that connects K1 and K2 in the 1st BZ q1_c = kd.bzk_kc[K2] - kd.bzk_kc[kpt1.K] # G-vector that connects the full Q_c with q1_c shift1_c = q1_c - self.sign * np.dot(U_cc, q_c) assert np.allclose(shift1_c.round(), shift1_c) shift1_c = shift1_c.round().astype(int) shift_c = kpt1.shift_c - kpt2.shift_c - shift1_c I_G = np.ravel_multi_index(i_cG + shift_c[:, None], N_c, 'wrap') pos_av = np.dot(self.spos_ac, pd0.gd.cell_cv) M_vv = np.dot( pd0.gd.cell_cv.T, np.dot(U_cc.T, np.linalg.inv(pd0.gd.cell_cv).T)) Q_aGii = [] for a, Q_Gii in enumerate(self.Q_aGii): x_G = np.exp( 1j * np.dot(G_Gv, (pos_av[a] - np.dot(M_vv, pos_av[a])))) U_ii = self.calc.wfs.setups[a].R_sii[self.s] Q_Gii = np.dot( np.dot(U_ii, Q_Gii * x_G[:, None, None]), U_ii.T).transpose(1, 0, 2) if self.sign == -1: Q_Gii = Q_Gii.conj() Q_aGii.append(Q_Gii) for n in range(kpt1.n2 - kpt1.n1): ut1cc_R = kpt1.ut_nR[n].conj() eps1 = kpt1.eps_n[n] C1_aGi = [ np.dot(Qa_Gii, P1_ni[n].conj()) for Qa_Gii, P1_ni in zip(Q_aGii, kpt1.P_ani) ] n_mG = self.calculate_pair_densities( ut1cc_R, C1_aGi, kpt2, pd0, I_G) if self.sign == 1: n_mG = n_mG.conj() f_m = kpt2.f_n deps_m = eps1 - kpt2.eps_n sigma, dsigma = self.calculate_sigma( n_mG, deps_m, f_m, Wpm_w) nn = kpt1.n1 + n - self.bands[0] self.sigma_sin[kpt1.s, i, nn] += sigma self.dsigma_sin[kpt1.s, i, nn] += dsigma self.world.sum(self.sigma_sin) self.world.sum(self.dsigma_sin) self.complete = True self.save_state_file() return self.sigma_sin, self.dsigma_sin def calculate_qp_correction(self): if self.filename: pckl = self.filename + '_qeh.pckl' else: pckl = 'qeh.pckl' if self.complete: print('Self-energy loaded from file', file=self.fd) else: self.calculate_QEH() # Need GW result for renormalization factor b1, b2 = self.bands gwdata = pickle.load(open(self.gwfile, 'rb')) self.dsigmagw_sin = gwdata['dsigma'] self.qpgw_sin = gwdata['qp'] / Hartree nk = self.qpgw_sin.shape[1] if not self.sigma_sin.shape[1] == nk: self.sigma_sin = np.repeat(self.sigma_sin[:, :1, :], nk, axis=1) self.dsigma_sin = np.repeat(self.dsigma_sin[:, :1, :], nk, axis=1) self.Z_sin = 1. / (1 - self.dsigma_sin - self.dsigmagw_sin) self.qp_sin = self.Z_sin * self.sigma_sin return self.qp_sin * Hartree def calculate_qp_energies(self): # calculate qp_sin = self.calculate_qp_correction() / Hartree self.Qp_sin = self.qpgw_sin + qp_sin self.save_state_file() return self.Qp_sin * Hartree @timer('Sigma') def calculate_sigma(self, n_mG, deps_m, f_m, W_wGG): """Calculates a contribution to the self-energy and its derivative for a given (k, k-q)-pair from its corresponding pair-density and energy.""" o_m = abs(deps_m) # Add small number to avoid zeros for degenerate states: sgn_m = np.sign(deps_m + 1e-15) # Pick +i*eta or -i*eta: s_m = (1 + sgn_m * np.sign(0.5 - f_m)).astype(int) // 2 world = self.world comm = self.blockcomm nw = len(self.omega_w) nG = n_mG.shape[1] mynG = (nG + comm.size - 1) // comm.size Ga = min(comm.rank * mynG, nG) Gb = min(Ga + mynG, nG) beta = (2**0.5 - 1) * self.domega0 / self.omega2 w_m = (o_m / (self.domega0 + beta * o_m)).astype(int) o1_m = self.omega_w[w_m] o2_m = self.omega_w[w_m + 1] x = 1.0 / (self.qd.nbzkpts * 2 * pi * self.vol) sigma = 0.0 dsigma = 0.0 # Performing frequency integration for o, o1, o2, sgn, s, w, n_G in zip(o_m, o1_m, o2_m, sgn_m, s_m, w_m, n_mG): C1_GG = W_wGG[s * nw + w] C2_GG = W_wGG[s * nw + w + 1] p = x * sgn myn_G = n_G[Ga:Gb] sigma1 = p * np.dot(np.dot(myn_G, C1_GG), n_G.conj()).imag sigma2 = p * np.dot(np.dot(myn_G, C2_GG), n_G.conj()).imag sigma += ((o - o1) * sigma2 + (o2 - o) * sigma1) / (o2 - o1) dsigma += sgn * (sigma2 - sigma1) / (o2 - o1) return sigma, dsigma def save_state_file(self, q=0): data = { 'kpts': self.kpts, 'bands': self.bands, 'nbands': self.nbands, 'last_q': self.nq, 'complete': self.complete, 'sigma_sin': self.sigma_sin, 'dsigma_sin': self.dsigma_sin, 'qp_sin': self.qp_sin, 'Qp_sin': self.Qp_sin } if self.world.rank == 0: with open(self.filename + '_qeh.pckl', 'wb') as fd: pickle.dump(data, fd) def load_state_file(self): try: data = pickle.load(open(self.filename + '_qeh.pckl', 'rb')) except IOError: return False else: if (data['kpts'] == self.kpts and data['bands'] == self.bands and data['nbands'] == self.nbands): self.nq = data['last_q'] self.complete = data['complete'] self.complete = data['complete'] self.sigma_sin = data['sigma_sin'] self.dsigma_sin = data['dsigma_sin'] return True else: return False def get_W_on_grid(self, dW_qw, include_q0=True, metal=False): """This function transforms the screened potential W(q,w) to the (q,w)-grid of the GW calculation. Also, W is integrated over a region around q=0 if include_q0 is set to True.""" q_cs = self.qd.ibzk_kc rcell_cv = 2 * pi * np.linalg.inv(self.calc.wfs.gd.cell_cv).T q_vs = np.dot(q_cs, rcell_cv) q_grid = (q_vs**2).sum(axis=1)**0.5 self.q_grid = q_grid w_grid = self.omega_w wqeh = self.wqeh # w_grid.copy() # self.qeh qqeh = self.qqeh sortqeh = np.argsort(qqeh) qqeh = qqeh[sortqeh] dW_qw = dW_qw[sortqeh] sort = np.argsort(q_grid) isort = np.argsort(sort) if metal and np.isclose(qqeh[0], 0): """We don't have the right q=0 limit for metals and semi-metals. -> Point should be omitted from interpolation""" qqeh = qqeh[1:] dW_qw = dW_qw[1:] sort = sort[1:] from scipy.interpolate import RectBivariateSpline yr = RectBivariateSpline(qqeh, wqeh, dW_qw.real, s=0) yi = RectBivariateSpline(qqeh, wqeh, dW_qw.imag, s=0) dWgw_qw = yr(q_grid[sort], w_grid) + 1j * yi(q_grid[sort], w_grid) dW_qw = yr(qqeh, w_grid) + 1j * yi(qqeh, w_grid) if metal: # Interpolation is done -> put back zeros at q=0 dWgw_qw = np.insert(dWgw_qw, 0, 0, axis=0) qqeh = np.insert(qqeh, 0, 0) dW_qw = np.insert(dW_qw, 0, 0, axis=0) q_cut = q_grid[sort][0] / 2. else: q_cut = q_grid[sort][1] / 2. q0 = np.array([q for q in qqeh if q <= q_cut]) if len(q0) > 1: # Integrate arround q=0 vol = np.pi * (q0[-1] + q0[1] / 2.)**2 if np.isclose(q0[0], 0): weight0 = np.pi * (q0[1] / 2.)**2 / vol c = (1 - weight0) / np.sum(q0) weights = c * q0 weights[0] = weight0 else: c = 1 / np.sum(q0) weights = c * q0 dWgw_qw[0] = ( np.repeat(weights[:, np.newaxis], len(w_grid), axis=1) * dW_qw[:len(q0)]).sum(axis=0) if not include_q0: # Omit q=0 contrinution completely. dWgw_qw[0] = 0.0 dWgw_qw = dWgw_qw[isort] # Put dW back on native grid. return dWgw_qw def calculate_W_QEH(self, structure, d, layer=0): from gpaw.response.qeh import Heterostructure, expand_layers, \ check_building_blocks structure = expand_layers(structure) self.w_grid = self.omega_w wmax = self.w_grid[-1] # qmax = (self.q_grid).max() # Single layer s = (np.insert(d, 0, d[0]) + np.append(d, d[-1])) / 2. d0 = s[layer] HS0 = Heterostructure( structure=[structure[layer]], d=[], d0=d0, wmax=wmax * Hartree, # qmax=qmax / Bohr ) W0_qw = HS0.get_screened_potential() # Full heterostructure HS = Heterostructure( structure=structure, d=d, wmax=wmax * Hartree, # qmax=qmax / Bohr ) W_qw = HS.get_screened_potential(layer=layer) # Difference in screened potential: dW_qw = W_qw - W0_qw self.wqeh = HS.frequencies self.qqeh = HS.q_abs if self.world.rank == 0: pickle.dump((self.qqeh, self.wqeh, dW_qw), open(self.filename + '_dW_qw.pckl', 'wb')) return dW_qw def find_maximum_frequency(self): self.epsmin = 10000.0 self.epsmax = -10000.0 for kpt in self.calc.wfs.kpt_u: self.epsmin = min(self.epsmin, kpt.eps_n[0]) self.epsmax = max(self.epsmax, kpt.eps_n[self.nbands - 1]) print('Minimum eigenvalue: %10.3f eV' % (self.epsmin * Hartree), file=self.fd) print('Maximum eigenvalue: %10.3f eV' % (self.epsmax * Hartree), file=self.fd) return self.epsmax - self.epsmin
class UTGaussianWavefunctionSetup(UTDomainParallelSetup): __doc__ = UTDomainParallelSetup.__doc__ + """ The pseudo wavefunctions are moving gaussians centered around each atom.""" allocated = False dtype = None # Default arguments for scaled Gaussian wave _sigma0 = 2.0 #0.75 _k0_c = 2*np.pi*np.array([1/5., 1/3., 0.]) def setUp(self): UTDomainParallelSetup.setUp(self) for virtvar in ['dtype']: assert getattr(self,virtvar) is not None, 'Virtual "%s"!' % virtvar # Create randomized atoms self.atoms = create_random_atoms(self.gd, 5) # also tested: 10xNH3/BDA # XXX DEBUG START if False: from ase import view view(self.atoms*(1+2*self.gd.pbc_c)) # XXX DEBUG END # Do we agree on the atomic positions? pos_ac = self.atoms.get_positions() pos_rac = np.empty((world.size,)+pos_ac.shape, pos_ac.dtype) world.all_gather(pos_ac, pos_rac) if (pos_rac-pos_rac[world.rank,...][np.newaxis,...]).any(): raise RuntimeError('Discrepancy in atomic positions detected.') # Create setups for atoms self.Z_a = self.atoms.get_atomic_numbers() self.setups = Setups(self.Z_a, p.setups, p.basis, p.lmax, xc) # K-point descriptor bzk_kc = np.array([[0, 0, 0]], dtype=float) self.kd = KPointDescriptor(bzk_kc, 1) self.kd.set_symmetry(self.atoms, self.setups) self.kd.set_communicator(self.kpt_comm) # Create gamma-point dummy wavefunctions self.wfs = FDWFS(self.gd, self.bd, self.kd, self.setups, self.block_comm, self.dtype) spos_ac = self.atoms.get_scaled_positions() % 1.0 self.wfs.set_positions(spos_ac) self.pt = self.wfs.pt # XXX shortcut ## Also create pseudo partial waveves #from gpaw.lfc import LFC #self.phit = LFC(self.gd, [setup.phit_j for setup in self.setups], \ # self.kpt_comm, dtype=self.dtype) #self.phit.set_positions(spos_ac) self.r_cG = None self.buf_G = None self.psit_nG = None self.allocate() def tearDown(self): UTDomainParallelSetup.tearDown(self) del self.r_cG, self.buf_G, self.psit_nG del self.pt, self.setups, self.atoms self.allocated = False def allocate(self): self.r_cG = self.gd.get_grid_point_coordinates() cell_cv = self.atoms.get_cell() / Bohr assert np.abs(cell_cv-self.gd.cell_cv).max() < 1e-9 center_c = 0.5*cell_cv.diagonal() self.buf_G = self.gd.empty(dtype=self.dtype) self.psit_nG = self.gd.empty(self.bd.mynbands, dtype=self.dtype) for myn,psit_G in enumerate(self.psit_nG): n = self.bd.global_index(myn) psit_G[:] = self.get_scaled_gaussian_wave(center_c, scale=10+2j*n) k_c = 2*np.pi*np.array([1/2., -1/7., 0.]) for pos_c in self.atoms.get_positions() / Bohr: sigma = self._sigma0/(1+np.sum(pos_c**2))**0.5 psit_G += self.get_scaled_gaussian_wave(pos_c, sigma, k_c, n+5j) self.allocated = True def get_scaled_gaussian_wave(self, pos_c, sigma=None, k_c=None, scale=None): if sigma is None: sigma = self._sigma0 if k_c is None: k_c = self._k0_c if scale is None: A = None else: # 4*pi*int(exp(-r^2/(2*w^2))^2*r^2, r=0...infinity)= w^3*pi^(3/2) # = scale/A^2 -> A = scale*(sqrt(Pi)*w)^(-3/2) hence int -> scale^2 A = scale/(sigma*(np.pi)**0.5)**1.5 return gaussian_wave(self.r_cG, pos_c, sigma, k_c, A, self.dtype, self.buf_G) def check_and_plot(self, P_ani, P0_ani, digits, keywords=''): # Collapse into viewable matrices P_In = self.wfs.collect_projections(P_ani) P0_In = self.wfs.collect_projections(P0_ani) # Construct fingerprint of input matrices for comparison fingerprint = np.array([md5_array(P_In, numeric=True), md5_array(P0_In, numeric=True)]) # Compare fingerprints across all processors fingerprints = np.empty((world.size, 2), np.int64) world.all_gather(fingerprint, fingerprints) if fingerprints.ptp(0).any(): raise RuntimeError('Distributed matrices are not identical!') # If assertion fails, catch temporarily while plotting, then re-raise try: self.assertAlmostEqual(np.abs(P_In-P0_In).max(), 0, digits) except AssertionError: if world.rank == 0 and mpl is not None: from matplotlib.figure import Figure fig = Figure() ax = fig.add_axes([0.0, 0.1, 1.0, 0.83]) ax.set_title(self.__class__.__name__) im = ax.imshow(np.abs(P_In-P0_In), interpolation='nearest') fig.colorbar(im) fig.text(0.5, 0.05, 'Keywords: ' + keywords, \ horizontalalignment='center', verticalalignment='top') from matplotlib.backends.backend_agg import FigureCanvasAgg img = 'ut_invops_%s_%s.png' % (self.__class__.__name__, \ '_'.join(keywords.split(','))) FigureCanvasAgg(fig).print_figure(img.lower(), dpi=90) raise # ================================= def test_projection_linearity(self): kpt = self.wfs.kpt_u[0] Q_ani = self.pt.dict(self.bd.mynbands) self.pt.integrate(self.psit_nG, Q_ani, q=kpt.q) for Q_ni in Q_ani.values(): self.assertTrue(Q_ni.dtype == self.dtype) P0_ani = dict([(a,Q_ni.copy()) for a,Q_ni in Q_ani.items()]) self.pt.add(self.psit_nG, Q_ani, q=kpt.q) self.pt.integrate(self.psit_nG, P0_ani, q=kpt.q) #rank_a = self.gd.get_ranks_from_positions(spos_ac) #my_atom_indices = np.argwhere(self.gd.comm.rank == rank_a).ravel() # ~ a ~ a' #TODO XXX should fix PairOverlap-ish stuff for < p | phi > overlaps # i i' #spos_ac = self.pt.spos_ac # NewLFC doesn't have this spos_ac = self.atoms.get_scaled_positions() % 1.0 gpo = GridPairOverlap(self.gd, self.setups) B_aa = gpo.calculate_overlaps(spos_ac, self.pt) # Compare fingerprints across all processors fingerprint = np.array([md5_array(B_aa, numeric=True)]) fingerprints = np.empty(world.size, np.int64) world.all_gather(fingerprint, fingerprints) if fingerprints.ptp(0).any(): raise RuntimeError('Distributed matrices are not identical!') P_ani = dict([(a,Q_ni.copy()) for a,Q_ni in Q_ani.items()]) for a1 in range(len(self.atoms)): if a1 in P_ani.keys(): P_ni = P_ani[a1] else: # Atom a1 is not in domain so allocate a temporary buffer P_ni = np.zeros((self.bd.mynbands,self.setups[a1].ni,), dtype=self.dtype) for a2, Q_ni in Q_ani.items(): # B_aa are the projector overlaps across atomic pairs B_ii = gpo.extract_atomic_pair_matrix(B_aa, a1, a2) P_ni += np.dot(Q_ni, B_ii.T) #sum over a2 and last i in B_ii self.gd.comm.sum(P_ni) self.check_and_plot(P_ani, P0_ani, 8, 'projection,linearity') def test_extrapolate_overlap(self): kpt = self.wfs.kpt_u[0] ppo = ProjectorPairOverlap(self.wfs, self.atoms) # Compare fingerprints across all processors fingerprint = np.array([md5_array(ppo.B_aa, numeric=True)]) fingerprints = np.empty(world.size, np.int64) world.all_gather(fingerprint, fingerprints) if fingerprints.ptp(0).any(): raise RuntimeError('Distributed matrices are not identical!') work_nG = np.empty_like(self.psit_nG) P_ani = ppo.apply(self.psit_nG, work_nG, self.wfs, kpt, \ calculate_P_ani=True, extrapolate_P_ani=True) P0_ani = self.pt.dict(self.bd.mynbands) self.pt.integrate(work_nG, P0_ani, kpt.q) del work_nG self.check_and_plot(P_ani, P0_ani, 11, 'extrapolate,overlap') def test_extrapolate_inverse(self): kpt = self.wfs.kpt_u[0] ppo = ProjectorPairOverlap(self.wfs, self.atoms) # Compare fingerprints across all processors fingerprint = np.array([md5_array(ppo.B_aa, numeric=True)]) fingerprints = np.empty(world.size, np.int64) world.all_gather(fingerprint, fingerprints) if fingerprints.ptp(0).any(): raise RuntimeError('Distributed matrices are not identical!') work_nG = np.empty_like(self.psit_nG) P_ani = ppo.apply_inverse(self.psit_nG, work_nG, self.wfs, kpt, \ calculate_P_ani=True, extrapolate_P_ani=True) P0_ani = self.pt.dict(self.bd.mynbands) self.pt.integrate(work_nG, P0_ani, kpt.q) del work_nG self.check_and_plot(P_ani, P0_ani, 11, 'extrapolate,inverse') def test_overlap_inverse_after(self): kpt = self.wfs.kpt_u[0] kpt.P_ani = self.pt.dict(self.bd.mynbands) ppo = ProjectorPairOverlap(self.wfs, self.atoms) # Compare fingerprints across all processors fingerprint = np.array([md5_array(ppo.B_aa, numeric=True)]) fingerprints = np.empty(world.size, np.int64) world.all_gather(fingerprint, fingerprints) if fingerprints.ptp(0).any(): raise RuntimeError('Distributed matrices are not identical!') work_nG = np.empty_like(self.psit_nG) self.pt.integrate(self.psit_nG, kpt.P_ani, kpt.q) P0_ani = dict([(a,P_ni.copy()) for a,P_ni in kpt.P_ani.items()]) ppo.apply(self.psit_nG, work_nG, self.wfs, kpt, calculate_P_ani=False) res_nG = np.empty_like(self.psit_nG) ppo.apply_inverse(work_nG, res_nG, self.wfs, kpt, calculate_P_ani=True) del work_nG P_ani = self.pt.dict(self.bd.mynbands) self.pt.integrate(res_nG, P_ani, kpt.q) abserr = np.empty(1, dtype=float) for n in range(self.nbands): band_rank, myn = self.bd.who_has(n) if band_rank == self.bd.comm.rank: abserr[:] = np.abs(self.psit_nG[myn] - res_nG[myn]).max() self.gd.comm.max(abserr) self.bd.comm.broadcast(abserr, band_rank) self.assertAlmostEqual(abserr.item(), 0, 10) self.check_and_plot(P_ani, P0_ani, 10, 'overlap,inverse,after') def test_overlap_inverse_before(self): kpt = self.wfs.kpt_u[0] kpt.P_ani = self.pt.dict(self.bd.mynbands) ppo = ProjectorPairOverlap(self.wfs, self.atoms) # Compare fingerprints across all processors fingerprint = np.array([md5_array(ppo.B_aa, numeric=True)]) fingerprints = np.empty(world.size, np.int64) world.all_gather(fingerprint, fingerprints) if fingerprints.ptp(0).any(): raise RuntimeError('Distributed matrices are not identical!') work_nG = np.empty_like(self.psit_nG) self.pt.integrate(self.psit_nG, kpt.P_ani, kpt.q) P0_ani = dict([(a,P_ni.copy()) for a,P_ni in kpt.P_ani.items()]) ppo.apply_inverse(self.psit_nG, work_nG, self.wfs, kpt, calculate_P_ani=False) res_nG = np.empty_like(self.psit_nG) ppo.apply(work_nG, res_nG, self.wfs, kpt, calculate_P_ani=True) del work_nG P_ani = self.pt.dict(self.bd.mynbands) self.pt.integrate(res_nG, P_ani, kpt.q) abserr = np.empty(1, dtype=float) for n in range(self.nbands): band_rank, myn = self.bd.who_has(n) if band_rank == self.bd.comm.rank: abserr[:] = np.abs(self.psit_nG[myn] - res_nG[myn]).max() self.gd.comm.max(abserr) self.bd.comm.broadcast(abserr, band_rank) self.assertAlmostEqual(abserr.item(), 0, 10) self.check_and_plot(P_ani, P0_ani, 10, 'overlap,inverse,before')
class UTKPointParallelSetup(TestCase): """ Setup a simple kpoint parallel calculation.""" # Number of bands nbands = 1 # Spin-polarized nspins = 1 # Mean spacing and number of grid points per axis (G x G x G) h = 0.25 / Bohr G = 48 ## Symmetry-reduction of k-points TODO #symmetry = p.usesymm #XXX 'None' is an allowed value!!! # Whether spin/k-points are equally distributed (determines nibzkpts) equipartition = None nibzkpts = None gamma = False # can't be gamma point when nibzkpts > 1 ... dtype = complex #XXX virtual so far.. # ================================= def setUp(self): for virtvar in ['equipartition']: assert getattr(self,virtvar) is not None, 'Virtual "%s"!' % virtvar kpts = {'even' : (12,1,2), \ 'prime': (23,1,1)}[self.equipartition] #primes = [i for i in xrange(50,1,-1) if ~np.any(i%np.arange(2,i)==0)] bzk_kc = kpts2ndarray(kpts) assert p.usesymm == None self.nibzkpts = len(bzk_kc) #parsize_domain, parsize_bands = create_parsize_minbands(self.nbands, world.size) parsize_domain, parsize_bands = 1, 1 #XXX assert self.nbands % np.prod(parsize_bands) == 0 domain_comm, kpt_comm, band_comm = distribute_cpus(parsize_domain, parsize_bands, self.nspins, self.nibzkpts) # Set up band descriptor: self.bd = BandDescriptor(self.nbands, band_comm, p.parallel['stridebands']) # Set up grid descriptor: res, ngpts = shapeopt(300, self.G**3, 3, 0.2) cell_c = self.h * np.array(ngpts) pbc_c = (True, False, True) self.gd = GridDescriptor(ngpts, cell_c, pbc_c, domain_comm, parsize_domain) # Create randomized gas-like atomic configuration self.atoms = create_random_atoms(self.gd) # Create setups Z_a = self.atoms.get_atomic_numbers() self.setups = Setups(Z_a, p.setups, p.basis, p.lmax, xc) self.natoms = len(self.setups) # Set up kpoint descriptor: self.kd = KPointDescriptor(bzk_kc, self.nspins) self.kd.set_symmetry(self.atoms, self.setups, usesymm=p.usesymm) self.kd.set_communicator(kpt_comm) def tearDown(self): del self.bd, self.gd, self.kd del self.setups, self.atoms def get_parsizes(self): #XXX NO LONGER IN UT_HSOPS?!? # Careful, overwriting imported GPAW params may cause amnesia in Python. from gpaw import parsize_domain, parsize_bands # Choose the largest possible parallelization over kpoint/spins test_parsize_ks_pairs = gcd(self.nspins*self.nibzkpts, world.size) remsize = world.size//test_parsize_ks_pairs # If parsize_bands is not set, choose the largest possible test_parsize_bands = parsize_bands or gcd(self.nbands, remsize) # If parsize_bands is not set, choose as few domains as possible test_parsize_domain = parsize_domain or (remsize//test_parsize_bands) return test_parsize_domain, test_parsize_bands # ================================= def verify_comm_sizes(self): #TODO needs work if world.size == 1: return comm_sizes = tuple([comm.size for comm in [world, self.bd.comm, \ self.gd.comm, self.kd.comm]]) self._parinfo = '%d world, %d band, %d domain, %d kpt' % comm_sizes #self.assertEqual((self.nspins*self.nibzkpts) % self.kd.comm.size, 0) #XXX def verify_slice_consistency(self): for kpt_rank in range(self.kd.comm.size): uslice = self.kd.get_slice(kpt_rank) myus = np.arange(*uslice.indices(self.kd.nks)) for myu,u in enumerate(myus): self.assertEqual(self.kd.who_has(u), (kpt_rank, myu)) def verify_combination_consistency(self): for u in range(self.kd.nks): s, k = self.kd.what_is(u) self.assertEqual(self.kd.where_is(s, k), u) for s in range(self.kd.nspins): for k in range(self.kd.nibzkpts): u = self.kd.where_is(s, k) self.assertEqual(self.kd.what_is(u), (s,k,)) def verify_indexing_consistency(self): for u in range(self.kd.nks): kpt_rank, myu = self.kd.who_has(u) self.assertEqual(self.kd.global_index(myu, kpt_rank), u) for kpt_rank in range(self.kd.comm.size): for myu in range(self.kd.get_count(kpt_rank)): u = self.kd.global_index(myu, kpt_rank) self.assertEqual(self.kd.who_has(u), (kpt_rank, myu)) def verify_ranking_consistency(self): ranks = self.kd.get_ranks() for kpt_rank in range(self.kd.comm.size): my_indices = self.kd.get_indices(kpt_rank) matches = np.argwhere(ranks == kpt_rank).ravel() self.assertTrue((matches == my_indices).all()) for myu in range(self.kd.get_count(kpt_rank)): u = self.kd.global_index(myu, kpt_rank) self.assertEqual(my_indices[myu], u)
def initialize(self, atoms=None): """Inexpensive initialization.""" if atoms is None: atoms = self.atoms else: # Save the state of the atoms: self.atoms = atoms.copy() par = self.input_parameters world = par.communicator if world is None: world = mpi.world elif hasattr(world, 'new_communicator'): # Check for whether object has correct type already # # Using isinstance() is complicated because of all the # combinations, serial/parallel/debug... pass else: # world should be a list of ranks: world = mpi.world.new_communicator(np.asarray(world)) self.wfs.world = world self.set_text(par.txt, par.verbose) natoms = len(atoms) cell_cv = atoms.get_cell() / Bohr pbc_c = atoms.get_pbc() Z_a = atoms.get_atomic_numbers() magmom_av = atoms.get_initial_magnetic_moments() # Generate new xc functional only when it is reset by set if self.hamiltonian is None or self.hamiltonian.xc is None: if isinstance(par.xc, str): xc = XC(par.xc) else: xc = par.xc else: xc = self.hamiltonian.xc mode = par.mode if xc.orbital_dependent and mode == 'lcao': raise NotImplementedError('LCAO mode does not support ' 'orbital-dependent XC functionals.') if mode == 'pw': mode = PW() if mode == 'fd' and par.usefractrans: raise NotImplementedError('FD mode does not support ' 'fractional translations.') if mode == 'lcao' and par.usefractrans: raise Warning('Fractional translations have not been tested ' 'with LCAO mode. Use with care!') if par.realspace is None: realspace = not isinstance(mode, PW) else: realspace = par.realspace if isinstance(mode, PW): assert not realspace if par.gpts is not None: N_c = np.array(par.gpts) else: h = par.h if h is not None: h /= Bohr N_c = get_number_of_grid_points(cell_cv, h, mode, realspace) if par.filter is None and not isinstance(mode, PW): gamma = 1.6 hmax = ((np.linalg.inv(cell_cv)**2).sum(0)**-0.5 / N_c).max() def filter(rgd, rcut, f_r, l=0): gcut = np.pi / hmax - 2 / rcut / gamma f_r[:] = rgd.filter(f_r, rcut * gamma, gcut, l) else: filter = par.filter setups = Setups(Z_a, par.setups, par.basis, par.lmax, xc, filter, world) if magmom_av.ndim == 1: collinear = True magmom_av, magmom_a = np.zeros((natoms, 3)), magmom_av magmom_av[:, 2] = magmom_a else: collinear = False magnetic = magmom_av.any() spinpol = par.spinpol if par.hund: if natoms != 1: raise ValueError('hund=True arg only valid for single atoms!') spinpol = True magmom_av[0] = (0, 0, setups[0].get_hunds_rule_moment(par.charge)) if spinpol is None: spinpol = magnetic elif magnetic and not spinpol: raise ValueError('Non-zero initial magnetic moment for a ' + 'spin-paired calculation!') if collinear: nspins = 1 + int(spinpol) ncomp = 1 else: nspins = 1 ncomp = 2 # K-point descriptor bzkpts_kc = kpts2ndarray(par.kpts, self.atoms) kd = KPointDescriptor(bzkpts_kc, nspins, collinear, par.usefractrans) width = par.width if width is None: if pbc_c.any(): width = 0.1 # eV else: width = 0.0 else: assert par.occupations is None if hasattr(self, 'time') or par.dtype == complex: dtype = complex else: if kd.gamma: dtype = float else: dtype = complex ## rbw: If usefractrans=True, kd.set_symmetry might overwrite N_c. ## This is necessary, because N_c must be dividable by 1/(fractional translation), ## f.e. fractional translations of a grid point must land on a grid point. N_c = kd.set_symmetry(atoms, setups, magmom_av, par.usesymm, N_c, world) nao = setups.nao nvalence = setups.nvalence - par.charge M_v = magmom_av.sum(0) M = np.dot(M_v, M_v)**0.5 nbands = par.nbands if nbands is None: nbands = 0 for setup in setups: nbands_from_atom = setup.get_default_nbands() # Any obscure setup errors? if nbands_from_atom < -(-setup.Nv // 2): raise ValueError('Bad setup: This setup requests %d' ' bands but has %d electrons.' % (nbands_from_atom, setup.Nv)) nbands += nbands_from_atom nbands = min(nao, nbands) elif nbands > nao and mode == 'lcao': raise ValueError('Too many bands for LCAO calculation: ' '%d bands and only %d atomic orbitals!' % (nbands, nao)) if nvalence < 0: raise ValueError( 'Charge %f is not possible - not enough valence electrons' % par.charge) if nbands <= 0: nbands = int(nvalence + M + 0.5) // 2 + (-nbands) if nvalence > 2 * nbands: raise ValueError('Too few bands! Electrons: %f, bands: %d' % (nvalence, nbands)) nbands *= ncomp if par.width is not None: self.text('**NOTE**: please start using ' 'occupations=FermiDirac(width).') if par.fixmom: self.text('**NOTE**: please start using ' 'occupations=FermiDirac(width, fixmagmom=True).') if self.occupations is None: if par.occupations is None: # Create object for occupation numbers: self.occupations = occupations.FermiDirac(width, par.fixmom) else: self.occupations = par.occupations self.occupations.magmom = M_v[2] cc = par.convergence if mode == 'lcao': niter_fixdensity = 0 else: niter_fixdensity = None if self.scf is None: self.scf = SCFLoop( cc['eigenstates'] / Hartree**2 * nvalence, cc['energy'] / Hartree * max(nvalence, 1), cc['density'] * nvalence, par.maxiter, par.fixdensity, niter_fixdensity) parsize_kpt = par.parallel['kpt'] parsize_domain = par.parallel['domain'] parsize_bands = par.parallel['band'] if not realspace: pbc_c = np.ones(3, bool) if not self.wfs: if parsize_domain == 'domain only': # XXX this was silly! parsize_domain = world.size parallelization = mpi.Parallelization(world, nspins * kd.nibzkpts) ndomains = None if parsize_domain is not None: ndomains = np.prod(parsize_domain) if isinstance(mode, PW): if ndomains > 1: raise ValueError('Planewave mode does not support ' 'domain decomposition.') ndomains = 1 parallelization.set(kpt=parsize_kpt, domain=ndomains, band=parsize_bands) domain_comm, kpt_comm, band_comm = \ parallelization.build_communicators() #domain_comm, kpt_comm, band_comm = mpi.distribute_cpus( # parsize_domain, parsize_bands, # nspins, kd.nibzkpts, world, par.idiotproof, mode) kd.set_communicator(kpt_comm) parstride_bands = par.parallel['stridebands'] # Unfortunately we need to remember that we adjusted the # number of bands so we can print a warning if it differs # from the number specified by the user. (The number can # be inferred from the input parameters, but it's tricky # because we allow negative numbers) self.nbands_parallelization_adjustment = -nbands % band_comm.size nbands += self.nbands_parallelization_adjustment # I would like to give the following error message, but apparently # there are cases, e.g. gpaw/test/gw_ppa.py, which involve # nbands > nao and are supposed to work that way. #if nbands > nao: # raise ValueError('Number of bands %d adjusted for band ' # 'parallelization %d exceeds number of atomic ' # 'orbitals %d. This problem can be fixed ' # 'by reducing the number of bands a bit.' # % (nbands, band_comm.size, nao)) bd = BandDescriptor(nbands, band_comm, parstride_bands) if (self.density is not None and self.density.gd.comm.size != domain_comm.size): # Domain decomposition has changed, so we need to # reinitialize density and hamiltonian: if par.fixdensity: raise RuntimeError('Density reinitialization conflict ' + 'with "fixdensity" - specify domain decomposition.') self.density = None self.hamiltonian = None # Construct grid descriptor for coarse grids for wave functions: gd = self.grid_descriptor_class(N_c, cell_cv, pbc_c, domain_comm, parsize_domain) # do k-point analysis here? XXX args = (gd, nvalence, setups, bd, dtype, world, kd, self.timer) if par.parallel['sl_auto']: # Choose scalapack parallelization automatically for key, val in par.parallel.items(): if (key.startswith('sl_') and key != 'sl_auto' and val is not None): raise ValueError("Cannot use 'sl_auto' together " "with '%s'" % key) max_scalapack_cpus = bd.comm.size * gd.comm.size nprow = max_scalapack_cpus npcol = 1 # Get a sort of reasonable number of columns/rows while npcol < nprow and nprow % 2 == 0: npcol *= 2 nprow //= 2 assert npcol * nprow == max_scalapack_cpus # ScaLAPACK creates trouble if there aren't at least a few # whole blocks; choose block size so there will always be # several blocks. This will crash for small test systems, # but so will ScaLAPACK in any case blocksize = min(-(-nbands // 4), 64) sl_default = (nprow, npcol, blocksize) else: sl_default = par.parallel['sl_default'] if mode == 'lcao': # Layouts used for general diagonalizer sl_lcao = par.parallel['sl_lcao'] if sl_lcao is None: sl_lcao = sl_default lcaoksl = get_KohnSham_layouts(sl_lcao, 'lcao', gd, bd, dtype, nao=nao, timer=self.timer) if collinear: self.wfs = LCAOWaveFunctions(lcaoksl, *args) else: from gpaw.xc.noncollinear import \ NonCollinearLCAOWaveFunctions self.wfs = NonCollinearLCAOWaveFunctions(lcaoksl, *args) elif mode == 'fd' or isinstance(mode, PW): # buffer_size keyword only relevant for fdpw buffer_size = par.parallel['buffer_size'] # Layouts used for diagonalizer sl_diagonalize = par.parallel['sl_diagonalize'] if sl_diagonalize is None: sl_diagonalize = sl_default diagksl = get_KohnSham_layouts(sl_diagonalize, 'fd', gd, bd, dtype, buffer_size=buffer_size, timer=self.timer) # Layouts used for orthonormalizer sl_inverse_cholesky = par.parallel['sl_inverse_cholesky'] if sl_inverse_cholesky is None: sl_inverse_cholesky = sl_default if sl_inverse_cholesky != sl_diagonalize: message = 'sl_inverse_cholesky != sl_diagonalize ' \ 'is not implemented.' raise NotImplementedError(message) orthoksl = get_KohnSham_layouts(sl_inverse_cholesky, 'fd', gd, bd, dtype, buffer_size=buffer_size, timer=self.timer) # Use (at most) all available LCAO for initialization lcaonbands = min(nbands, nao) try: lcaobd = BandDescriptor(lcaonbands, band_comm, parstride_bands) except RuntimeError: initksl = None else: # Layouts used for general diagonalizer # (LCAO initialization) sl_lcao = par.parallel['sl_lcao'] if sl_lcao is None: sl_lcao = sl_default initksl = get_KohnSham_layouts(sl_lcao, 'lcao', gd, lcaobd, dtype, nao=nao, timer=self.timer) if hasattr(self, 'time'): assert mode == 'fd' from gpaw.tddft import TimeDependentWaveFunctions self.wfs = TimeDependentWaveFunctions(par.stencils[0], diagksl, orthoksl, initksl, gd, nvalence, setups, bd, world, kd, self.timer) elif mode == 'fd': self.wfs = FDWaveFunctions(par.stencils[0], diagksl, orthoksl, initksl, *args) else: # Planewave basis: self.wfs = mode(diagksl, orthoksl, initksl, *args) else: self.wfs = mode(self, *args) else: self.wfs.set_setups(setups) if not self.wfs.eigensolver: # Number of bands to converge: nbands_converge = cc['bands'] if nbands_converge == 'all': nbands_converge = nbands elif nbands_converge != 'occupied': assert isinstance(nbands_converge, int) if nbands_converge < 0: nbands_converge += nbands eigensolver = get_eigensolver(par.eigensolver, mode, par.convergence) eigensolver.nbands_converge = nbands_converge # XXX Eigensolver class doesn't define an nbands_converge property if isinstance(xc, SIC): eigensolver.blocksize = 1 self.wfs.set_eigensolver(eigensolver) if self.density is None: gd = self.wfs.gd if par.stencils[1] != 9: # Construct grid descriptor for fine grids for densities # and potentials: finegd = gd.refine() else: # Special case (use only coarse grid): finegd = gd if realspace: self.density = RealSpaceDensity( gd, finegd, nspins, par.charge + setups.core_charge, collinear, par.stencils[1]) else: self.density = ReciprocalSpaceDensity( gd, finegd, nspins, par.charge + setups.core_charge, collinear) self.density.initialize(setups, self.timer, magmom_av, par.hund) self.density.set_mixer(par.mixer) if self.hamiltonian is None: gd, finegd = self.density.gd, self.density.finegd if realspace: self.hamiltonian = RealSpaceHamiltonian( gd, finegd, nspins, setups, self.timer, xc, par.external, collinear, par.poissonsolver, par.stencils[1], world) else: self.hamiltonian = ReciprocalSpaceHamiltonian( gd, finegd, self.density.pd2, self.density.pd3, nspins, setups, self.timer, xc, par.external, collinear, world) xc.initialize(self.density, self.hamiltonian, self.wfs, self.occupations) self.text() self.print_memory_estimate(self.txt, maxdepth=memory_estimate_depth) self.txt.flush() self.timer.print_info(self) if dry_run: self.dry_run() self.initialized = True
class UTDomainParallelSetup(TestCase): """ Setup a simple domain parallel calculation.""" # Number of bands nbands = 12 # Spin-polarized nspins = 1 # Mean spacing and number of grid points per axis (G x G x G) h = 0.25 / Bohr G = 48 # Type of boundary conditions employed (determines nibzkpts and dtype) boundaries = None nibzkpts = None dtype = None timer = nulltimer def setUp(self): for virtvar in ['boundaries']: assert getattr(self, virtvar) is not None, 'Virtual "%s"!' % virtvar # Basic unit cell information: res, N_c = shapeopt(100, self.G**3, 3, 0.2) #N_c = 4*np.round(np.array(N_c)/4) # makes domain decomposition easier cell_cv = self.h * np.diag(N_c) pbc_c = {'zero' : (False,False,False), \ 'periodic': (True,True,True), \ 'mixed' : (True, False, True)}[self.boundaries] # Create randomized gas-like atomic configuration on interim grid tmpgd = GridDescriptor(N_c, cell_cv, pbc_c) self.atoms = create_random_atoms(tmpgd) # Create setups Z_a = self.atoms.get_atomic_numbers() assert 1 == self.nspins self.setups = Setups(Z_a, p.setups, p.basis, p.lmax, xc) self.natoms = len(self.setups) # Decide how many kpoints to sample from the 1st Brillouin Zone kpts_c = np.ceil( (10 / Bohr) / np.sum(cell_cv**2, axis=1)**0.5).astype(int) kpts_c = tuple(kpts_c * pbc_c + 1 - pbc_c) self.bzk_kc = kpts2ndarray(kpts_c) # Set up k-point descriptor self.kd = KPointDescriptor(self.bzk_kc, self.nspins) self.kd.set_symmetry(self.atoms, self.setups, p.usesymm) # Set the dtype if self.kd.gamma: self.dtype = float else: self.dtype = complex # Create communicators parsize, parsize_bands = self.get_parsizes() assert self.nbands % np.prod(parsize_bands) == 0 domain_comm, kpt_comm, band_comm = distribute_cpus( parsize, parsize_bands, self.nspins, self.kd.nibzkpts) self.kd.set_communicator(kpt_comm) # Set up band descriptor: self.bd = BandDescriptor(self.nbands, band_comm) # Set up grid descriptor: self.gd = GridDescriptor(N_c, cell_cv, pbc_c, domain_comm, parsize) # Set up kpoint/spin descriptor (to be removed): self.kd_old = KPointDescriptorOld(self.nspins, self.kd.nibzkpts, kpt_comm, self.kd.gamma, self.dtype) def tearDown(self): del self.atoms, self.bd, self.gd, self.kd, self.kd_old def get_parsizes(self): # Careful, overwriting imported GPAW params may cause amnesia in Python. from gpaw import parsize, parsize_bands # D: number of domains # B: number of band groups if parsize is None: D = min(world.size, 2) else: D = parsize assert world.size % D == 0 if parsize_bands is None: B = world.size // D else: B = parsize_bands return D, B # ================================= def verify_comm_sizes(self): if world.size == 1: return comm_sizes = tuple([comm.size for comm in [world, self.bd.comm, \ self.gd.comm, self.kd_old.comm]]) self._parinfo = '%d world, %d band, %d domain, %d kpt' % comm_sizes self.assertEqual(self.nbands % self.bd.comm.size, 0) self.assertEqual( (self.nspins * self.kd.nibzkpts) % self.kd_old.comm.size, 0)
class UTDomainParallelSetup(TestCase): """ Setup a simple domain parallel calculation.""" # Number of bands nbands = 12 # Spin-polarized nspins = 1 # Mean spacing and number of grid points per axis (G x G x G) h = 0.25 / Bohr G = 48 # Type of boundary conditions employed (determines nibzkpts and dtype) boundaries = None nibzkpts = None dtype = None timer = nulltimer def setUp(self): for virtvar in ['boundaries']: assert getattr(self,virtvar) is not None, 'Virtual "%s"!' % virtvar # Basic unit cell information: res, N_c = shapeopt(100, self.G**3, 3, 0.2) #N_c = 4*np.round(np.array(N_c)/4) # makes domain decomposition easier cell_cv = self.h * np.diag(N_c) pbc_c = {'zero' : (False,False,False), \ 'periodic': (True,True,True), \ 'mixed' : (True, False, True)}[self.boundaries] # Create randomized gas-like atomic configuration on interim grid tmpgd = GridDescriptor(N_c, cell_cv, pbc_c) self.atoms = create_random_atoms(tmpgd) # Create setups Z_a = self.atoms.get_atomic_numbers() assert 1 == self.nspins self.setups = Setups(Z_a, p.setups, p.basis, p.lmax, xc) self.natoms = len(self.setups) # Decide how many kpoints to sample from the 1st Brillouin Zone kpts_c = np.ceil((10/Bohr)/np.sum(cell_cv**2,axis=1)**0.5).astype(int) kpts_c = tuple(kpts_c * pbc_c + 1 - pbc_c) self.bzk_kc = kpts2ndarray(kpts_c) # Set up k-point descriptor self.kd = KPointDescriptor(self.bzk_kc, self.nspins) self.kd.set_symmetry(self.atoms, self.setups, p.usesymm) # Set the dtype if self.kd.gamma: self.dtype = float else: self.dtype = complex # Create communicators parsize, parsize_bands = self.get_parsizes() assert self.nbands % np.prod(parsize_bands) == 0 domain_comm, kpt_comm, band_comm = distribute_cpus(parsize, parsize_bands, self.nspins, self.kd.nibzkpts) self.kd.set_communicator(kpt_comm) # Set up band descriptor: self.bd = BandDescriptor(self.nbands, band_comm) # Set up grid descriptor: self.gd = GridDescriptor(N_c, cell_cv, pbc_c, domain_comm, parsize) # Set up kpoint/spin descriptor (to be removed): self.kd_old = KPointDescriptorOld(self.nspins, self.kd.nibzkpts, kpt_comm, self.kd.gamma, self.dtype) def tearDown(self): del self.atoms, self.bd, self.gd, self.kd, self.kd_old def get_parsizes(self): # Careful, overwriting imported GPAW params may cause amnesia in Python. from gpaw import parsize, parsize_bands # D: number of domains # B: number of band groups if parsize is None: D = min(world.size, 2) else: D = parsize assert world.size % D == 0 if parsize_bands is None: B = world.size // D else: B = parsize_bands return D, B # ================================= def verify_comm_sizes(self): if world.size == 1: return comm_sizes = tuple([comm.size for comm in [world, self.bd.comm, \ self.gd.comm, self.kd_old.comm]]) self._parinfo = '%d world, %d band, %d domain, %d kpt' % comm_sizes self.assertEqual(self.nbands % self.bd.comm.size, 0) self.assertEqual((self.nspins * self.kd.nibzkpts) % self.kd_old.comm.size, 0)
class HybridXC(XCFunctional): orbital_dependent = True def __init__(self, name, hybrid=None, xc=None, finegrid=False, alpha=None): """Mix standard functionals with exact exchange. name: str Name of hybrid functional. hybrid: float Fraction of exact exchange. xc: str or XCFunctional object Standard DFT functional with scaled down exchange. finegrid: boolean Use fine grid for energy functional evaluations? """ if name == 'EXX': assert hybrid is None and xc is None hybrid = 1.0 xc = XC(XCNull()) elif name == 'PBE0': assert hybrid is None and xc is None hybrid = 0.25 xc = XC('HYB_GGA_XC_PBEH') elif name == 'B3LYP': assert hybrid is None and xc is None hybrid = 0.2 xc = XC('HYB_GGA_XC_B3LYP') if isinstance(xc, str): xc = XC(xc) self.hybrid = hybrid self.xc = xc self.type = xc.type self.alpha = alpha self.exx = 0.0 XCFunctional.__init__(self, name) def get_setup_name(self): return 'PBE' def calculate_radial(self, rgd, n_sLg, Y_L, v_sg, dndr_sLg=None, rnablaY_Lv=None, tau_sg=None, dedtau_sg=None): return self.xc.calculate_radial(rgd, n_sLg, Y_L, v_sg, dndr_sLg, rnablaY_Lv) def initialize(self, density, hamiltonian, wfs, occupations): self.xc.initialize(density, hamiltonian, wfs, occupations) self.nspins = wfs.nspins self.setups = wfs.setups self.density = density self.kpt_u = wfs.kpt_u self.gd = density.gd self.kd = wfs.kd self.bd = wfs.bd N_c = self.gd.N_c N = self.gd.N_c.prod() vol = self.gd.dv * N if self.alpha is None: self.alpha = 6 * vol**(2 / 3.0) / pi**2 self.gamma = (vol / (2 * pi)**2 * sqrt(pi / self.alpha) * self.kd.nbzkpts) ecut = 0.5 * pi**2 / (self.gd.h_cv**2).sum(1).max() if self.kd.N_c is None: self.bzk_kc = np.zeros((1, 3)) dfghdfgh else: n = self.kd.N_c * 2 - 1 bzk_kc = np.indices(n).transpose((1, 2, 3, 0)) bzk_kc.shape = (-1, 3) bzk_kc -= self.kd.N_c - 1 self.bzk_kc = bzk_kc.astype(float) / self.kd.N_c self.pwd = PWDescriptor(ecut, self.gd, self.bzk_kc) n = 0 for k_c, Gpk2_G in zip(self.bzk_kc[:], self.pwd.G2_qG): if (k_c > -0.5).all() and (k_c <= 0.5).all(): #XXX??? if k_c.any(): self.gamma -= np.dot(np.exp(-self.alpha * Gpk2_G), Gpk2_G**-1) else: self.gamma -= np.dot(np.exp(-self.alpha * Gpk2_G[1:]), Gpk2_G[1:]**-1) n += 1 assert n == self.kd.N_c.prod() self.ghat = LFC(self.gd, [setup.ghat_l for setup in density.setups], dtype=complex ) self.ghat.set_k_points(self.bzk_kc) self.fullkd = KPointDescriptor(self.kd.bzk_kc, nspins=1) class S: id_a = [] def set_symmetry(self, s): pass self.fullkd.set_symmetry(Atoms(pbc=True), S(), False) self.fullkd.set_communicator(world) self.pt = LFC(self.gd, [setup.pt_j for setup in density.setups], dtype=complex) self.pt.set_k_points(self.fullkd.ibzk_kc) self.interpolator = density.interpolator def set_positions(self, spos_ac): self.ghat.set_positions(spos_ac) self.pt.set_positions(spos_ac) def calculate(self, gd, n_sg, v_sg=None, e_g=None): # Normal XC contribution: exc = self.xc.calculate(gd, n_sg, v_sg, e_g) # Add EXX contribution: return exc + self.exx def calculate_exx(self): """Non-selfconsistent calculation.""" kd = self.kd K = self.fullkd.nibzkpts assert self.nspins == 1 Q = K // world.size assert Q * world.size == K parallel = (world.size > self.nspins) self.exx = 0.0 self.exx_skn = np.zeros((self.nspins, K, self.bd.nbands)) kpt_u = [] for k in range(world.rank * Q, (world.rank + 1) * Q): k_c = self.fullkd.ibzk_kc[k] for k1, k1_c in enumerate(kd.bzk_kc): if abs(k1_c - k_c).max() < 1e-10: break # Index of symmetry related point in the irreducible BZ ik = kd.kibz_k[k1] kpt = self.kpt_u[ik] # KPoint from ground-state calculation phase_cd = np.exp(2j * pi * self.gd.sdisp_cd * k_c[:, np.newaxis]) kpt2 = KPoint0(kpt.weight, kpt.s, k, None, phase_cd) kpt2.psit_nG = np.empty_like(kpt.psit_nG) kpt2.f_n = kpt.f_n / kpt.weight / K * 2 for n, psit_G in enumerate(kpt2.psit_nG): psit_G[:] = kd.transform_wave_function(kpt.psit_nG[n], k1) kpt2.P_ani = self.pt.dict(len(kpt.psit_nG)) self.pt.integrate(kpt2.psit_nG, kpt2.P_ani, k) kpt_u.append(kpt2) for s in range(self.nspins): kpt1_q = [KPoint(self.fullkd, kpt) for kpt in kpt_u if kpt.s == s] kpt2_q = kpt1_q[:] if len(kpt1_q) == 0: # No s-spins on this CPU: continue # Send rank: srank = self.fullkd.get_rank_and_index(s, (kpt1_q[0].k - 1) % K)[0] # Receive rank: rrank = self.fullkd.get_rank_and_index(s, (kpt1_q[-1].k + 1) % K)[0] # Shift k-points K // 2 times: for i in range(K // 2 + 1): if i < K // 2: if parallel: kpt = kpt2_q[-1].next() kpt.start_receiving(rrank) kpt2_q[0].start_sending(srank) else: kpt = kpt2_q[0] for kpt1, kpt2 in zip(kpt1_q, kpt2_q): if 2 * i == K: self.apply(kpt1, kpt2, invert=(kpt1.k > kpt2.k)) else: self.apply(kpt1, kpt2) self.apply(kpt1, kpt2, invert=True) if i < K // 2: if parallel: kpt.wait() kpt2_q[0].wait() kpt2_q.pop(0) kpt2_q.append(kpt) self.exx = world.sum(self.exx) world.sum(self.exx_skn) self.exx += self.calculate_paw_correction() def apply(self, kpt1, kpt2, invert=False): #print world.rank,kpt1.k,kpt2.k,invert k1_c = self.fullkd.ibzk_kc[kpt1.k] k2_c = self.fullkd.ibzk_kc[kpt2.k] if invert: k2_c = -k2_c k12_c = k1_c - k2_c N_c = self.gd.N_c eikr_R = np.exp(2j * pi * np.dot(np.indices(N_c).T, k12_c / N_c).T) for q, k_c in enumerate(self.bzk_kc): if abs(k_c + k12_c).max() < 1e-9: q0 = q break for q, k_c in enumerate(self.bzk_kc): if abs(k_c - k12_c).max() < 1e-9: q00 = q break Gpk2_G = self.pwd.G2_qG[q0] if Gpk2_G[0] == 0: Gpk2_G = Gpk2_G.copy() Gpk2_G[0] = 1.0 / self.gamma N = N_c.prod() vol = self.gd.dv * N nspins = self.nspins same = (kpt1.k == kpt2.k) for n1, psit1_R in enumerate(kpt1.psit_nG): f1 = kpt1.f_n[n1] for n2, psit2_R in enumerate(kpt2.psit_nG): if same and n2 > n1: continue f2 = kpt2.f_n[n2] nt_R = self.calculate_pair_density(n1, n2, kpt1, kpt2, q0, invert) nt_G = self.pwd.fft(nt_R * eikr_R) / N vt_G = nt_G.copy() vt_G *= -pi * vol / Gpk2_G e = np.vdot(nt_G, vt_G).real * nspins * self.hybrid if same and n1 == n2: e /= 2 self.exx += e * f1 * f2 self.ekin -= 2 * e * f1 * f2 self.exx_skn[kpt1.s, kpt1.k, n1] += f2 * e self.exx_skn[kpt2.s, kpt2.k, n2] += f1 * e calculate_potential = not True if calculate_potential: vt_R = self.pwd.ifft(vt_G).conj() * eikr_R * N / vol if kpt1 is kpt2 and not invert and n1 == n2: kpt1.vt_nG[n1] = 0.5 * f1 * vt_R if invert: kpt1.Htpsit_nG[n1] += \ f2 * nspins * psit2_R.conj() * vt_R else: kpt1.Htpsit_nG[n1] += f2 * nspins * psit2_R * vt_R if kpt1 is not kpt2: if invert: kpt2.Htpsit_nG[n2] += (f1 * nspins * psit1_R.conj() * vt_R) else: kpt2.Htpsit_nG[n2] += (f1 * nspins * psit1_R * vt_R.conj()) def calculate_paw_correction(self): exx = 0 deg = 2 // self.nspins # spin degeneracy for a, D_sp in self.density.D_asp.items(): setup = self.setups[a] for D_p in D_sp: D_ii = unpack2(D_p) ni = len(D_ii) for i1 in range(ni): for i2 in range(ni): A = 0.0 for i3 in range(ni): p13 = packed_index(i1, i3, ni) for i4 in range(ni): p24 = packed_index(i2, i4, ni) A += setup.M_pp[p13, p24] * D_ii[i3, i4] p12 = packed_index(i1, i2, ni) exx -= self.hybrid / deg * D_ii[i1, i2] * A if setup.X_p is not None: exx -= self.hybrid * np.dot(D_p, setup.X_p) exx += self.hybrid * setup.ExxC return exx def calculate_pair_density(self, n1, n2, kpt1, kpt2, q, invert): if invert: nt_G = kpt1.psit_nG[n1].conj() * kpt2.psit_nG[n2].conj() else: nt_G = kpt1.psit_nG[n1].conj() * kpt2.psit_nG[n2] Q_aL = {} for a, P1_ni in kpt1.P_ani.items(): P1_i = P1_ni[n1] P2_i = kpt2.P_ani[a][n2] if invert: D_ii = np.outer(P1_i.conj(), P2_i.conj()) else: D_ii = np.outer(P1_i.conj(), P2_i) D_p = pack(D_ii) Q_aL[a] = np.dot(D_p, self.setups[a].Delta_pL) self.ghat.add(nt_G, Q_aL, q) return nt_G