class LinearResponse: """ """ def __init__(self, calc, energy_cut=10.0, timing=False, txt=None): """ Calculate linear optical response (LR-TD-DFTB). For details, see Niehaus et.al. Phys. Rev. B 63, 085108 (2001) parameters: =========== calc: calculator object energy_cut: max energy (in eV) for particle-hole excitations Used to select all particle-hole excitations in the construction of the matrix, that have excitation energy less than this value. This implies, that if we are interested in optical response up to some energy, energy_cut should be slightly larger. timing: output timing summary after calculation out: output object (file name or object) """ self.calc = calc self.st = calc.st self.el = calc.el self.es = calc.st.es self.energy_cut = energy_cut / Hartree #self.noc=self.st.get_hoc()+1 #number of occupied states (not index) # do not use HOC self.nel = self.el.get_number_of_electrons() self.norb = self.el.get_nr_orbitals() self.e = self.st.get_eigenvalues()[0, :] self.f = self.st.get_occupations()[0, :] if np.any(abs(self.st.wf.flatten().imag) > 1E-10): raise ValueError('Wave functions should not be complex.') self.wf = self.st.wf[0].real self.S = self.st.S[0].real self.N = len(self.el) self.SCC = self.calc.get('SCC') atoms = calc.get_atoms() if atoms.pbc.any(): raise AssertionError( 'No linear response for extended, periodic systems!') #if abs(np.mod(self.nel,2))>1E-2: #raise RuntimeError('Linear response only for closed shell systems! (even number of electrons)') #if abs(self.nel-2*self.noc)>1E-2: #print 'Number of electrons:',self.nel #print '2*Number of occupied states:',2*self.noc #raise RuntimeError('Number of electrons!=2*number of occupied orbitals. Decrease electronic temperature?') if txt is None: self.txt = sys.stdout else: self.txt = open(txt, 'a') self.timer = Timer('Linear Response', txt=self.txt) self.timing = timing self.done = False self.allowed_cut = 1E-2 #if osc.strength is smaller, transition is not allowed self._initialize() def _initialize(self): """ Perform some initialization calculations. """ self.timer.start('init') self.Swf = np.dot(self.S, self.wf.transpose()) #try to avoind the transpose self.timer.stop('init') def get_linear_response(self): """ Get linear response spectrum in eV. """ return self.omega * Hartree, self.F def mulliken_transfer(self, k, l): """ Return Mulliken transfer charges between states k and l. """ q = [] for i, o1, no in self.el.get_property_lists(['i', 'o1', 'no']): #qi=sum( [self.wf[a,k]*self.Swf[a,l]+self.wf[a,l]*self.Swf[a,k] for a in range(o1,o1+no)] ) qi = sum([ self.wf[k, a] * self.Swf[a, l] + self.wf[l, a] * self.Swf[a, k] for a in range(o1, o1 + no) ]) q.append(qi / 2) return np.array(q) def run(self): """ Run the calculation. """ if self.done == True: raise AssertionError('Run LR calculation only once.') print('\nLR for %s (charge %.2f). ' % (self.el.get_name(), self.calc.get_charge()), end=' ', file=self.txt) # # select electron-hole excitations (i occupied, j not occupied) # de = excitation energy ej-ei (ej>ei) # df = occupation difference fi-fj (ej>ei so that fi>fj) # de = [] df = [] particle_holes = [] self.timer.start('setup ph pairs') for i in range(self.norb): for j in range(i + 1, self.norb): energy = self.e[j] - self.e[i] occup = ( self.f[i] - self.f[j] ) / 2 #normalize the double occupations (...is this rigorously right?) if energy < self.energy_cut and occup > 1E-6: assert energy > 0 and occup > 0 particle_holes.append([i, j]) de.append(energy) df.append(occup) self.timer.stop('setup ph pairs') de = np.array(de) df = np.array(df) # # setup the matrix (gamma-approximation) and diagonalize # self.timer.start('setup matrix') dim = len(de) print('Dimension %i. ' % dim, end=' ', file=self.txt) if not 0 < dim < 100000: raise RuntimeError('Coupling matrix too large or small (%i)' % dim) r = self.el.get_positions() transfer_q = np.array( [self.mulliken_transfer(ph[0], ph[1]) for ph in particle_holes]) rv = np.array([dot(tq, r) for tq in transfer_q]) matrix = np.zeros((dim, dim)) if self.SCC: gamma = self.es.get_gamma().copy() gamma_tq = np.zeros((dim, self.N)) for k in range(dim): gamma_tq[k, :] = dot(gamma, transfer_q[k, :]) for k1, ph1 in enumerate(particle_holes): matrix[k1, k1] = de[k1]**2 for k2, ph2 in enumerate(particle_holes): coupling = dot(transfer_q[k1, :], gamma_tq[k2, :]) matrix[k1, k2] += 2 * sqrt( df[k1] * de[k1] * de[k2] * df[k2]) * coupling else: for k1, ph1 in enumerate(particle_holes): matrix[k1, k1] = de[k1]**2 self.timer.stop('setup matrix') print('coupling matrix constructed. ', end=' ', file=self.txt) self.txt.flush() self.timer.start('diagonalize') omega2, eigv = eigh(matrix) self.timer.stop('diagonalize') print('Matrix diagonalized.', end=' ', file=self.txt) self.txt.flush() # assert np.all(omega2>1E-16) print(omega2) omega = sqrt(omega2) # calculate oscillator strengths F = [] collectivity = [] self.timer.start('oscillator strengths') for ex in range(dim): v = [] for i in range(3): v.append( sum(rv[:, i] * sqrt(df[:] * de[:]) * eigv[:, ex]) / sqrt(omega[ex]) * 2) F.append(omega[ex] * dot(v, v) * 2.0 / 3) collectivity.append(1 / sum(eigv[:, ex]**4)) self.omega = omega self.F = F self.eigv = eigv self.collectivity = collectivity self.dim = dim self.particle_holes = particle_holes self.timer.stop('oscillator strengths') if self.timing: self.timer.summary() self.done = True self.emax = max(omega) self.particle_holes = particle_holes def info(self): """ Some info about excitations (energy, main p-h excitations,...) """ print('\n#e(eV), f, collectivity, transitions ...') for ex in range(self.dim): if self.F[ex] < self.allowed_cut: continue print( '%.5f %.5f %8.1f' % (self.omega[ex] * Hartree, self.F[ex], self.collectivity[ex]), end=' ') order = np.argsort(abs(self.eigv[:, ex]))[::-1] for ph in order[:4]: i, j = self.particle_holes[ph] print('%3i-%-3i:%-10.3f' % (i, j, self.eigv[ph, ex]**2), end=' ') print() def get_excitation(self, i, allowed=True): """ Return energy (eV) and oscillation strength for i'th allowed excitation index. i=0 means first excitation """ if allowed == False: return self.omega[i] * Hartree, self.F[i] else: p = -1 for k in range(self.dim): if self.F[k] >= self.allowed_cut: p += 1 if p == i: return self.omega[k] * Hartree, self.F[k] def write_spectrum(self, filename=None): """ Write the linear response spectrum into file. """ if filename == None: filename = 'linear_spectrum.out' o = open(filename, 'w') print('#e(eV), f', file=o) for ex in range(self.dim): print( '%10.5f %10.5f %10.5f' % (self.omega[ex] * Hartree, self.F[ex], self.collectivity[ex]), file=o) o.close() def read_spectrum(self, filename): """ Read the linear response from given file. Format: energy & oscillator strength. """ o = open(filename, 'r') data = mix.read(filename) self.omega, self.F, self.collectivity = data[:, 0], data[:, 1], data[:, 2] def plot_spectrum(self, filename, width=0.2, xlim=None): """ Make pretty plot of the linear response. Parameters: =========== filename: output file name (&format, supported by matplotlib) width: width of Lorenzian broadening xlim: energy range for plotting tuple (emin,emax) """ import pylab as pl if not self.done: self.run() e, f = mix.broaden(self.omega * Hartree, self.F, width=width, N=1000, extend=True) f = f / max(abs(f)) pl.plot(e, f, lw=2) ## MS: incompatibility issue with matplotlib>=3.1 # xs, ys = pl.poly_between(e, 0, f) # pl.fill(xs,ys,fc='b',ec='b',alpha=0.5) pl.fill(np.append(e, 0), np.append(f, 0), fc='b', ec='b', alpha=0.5) pl.ylim(0, 1.2) if xlim == None: pl.xlim(0, self.emax * Hartree * 1.2) else: pl.xlim(xlim) pl.xlabel('energy (eV)') pl.ylabel('linear optical response') pl.title('Optical response') pl.savefig(filename) #pl.show() pl.close()
class SlaterKosterTable: def __init__(self, ela, elb, txt=None, timing=False): """ Construct Slater-Koster table for given elements. parameters: ----------- ela: element objects (KSAllElectron or Element) elb: element objects (KSAllElectron or Element) txt: output file object or file name timing: output of timing summary after calculation """ self.ela = ela self.elb = elb self.timing = timing if txt == None: self.txt = sys.stdout else: if type(txt) == type(''): self.txt = open(txt, 'a') else: self.txt = txt self.comment = self.ela.get_comment() if ela.get_symbol() != elb.get_symbol(): self.nel = 2 self.pairs = [(ela, elb), (elb, ela)] self.elements = [ela, elb] self.comment += '\n' + self.elb.get_comment() else: self.nel = 1 self.pairs = [(ela, elb)] self.elements = [ela] self.timer = Timer('SlaterKosterTable', txt=self.txt, enabled=timing) print('\n\n\n\n', file=self.txt) print('************************************************', file=self.txt) print('Slater-Koster table construction for %2s and %2s' % (ela.get_symbol(), elb.get_symbol()), file=self.txt) print('************************************************', file=self.txt) def __del__(self): self.timer.summary() def get_table(self): """ Return tables. """ return self.Rgrid, self.tables def smooth_tails(self): """ Smooth the behaviour of tables near cutoff. """ for p in range(self.nel): for i in range(20): self.tables[p][:, i] = tail_smoothening(self.Rgrid, self.tables[p][:, i]) def write(self, filename=None): """ Use symbol1_symbol2.par as default. """ self.smooth_tails() if filename == None: fn = '%s_%s.par' % (self.ela.get_symbol(), self.elb.get_symbol()) else: fn = filename f = open(fn, 'w') print('slako_comment=', file=f) print(self.get_comment(), '\n\n', file=f) for p, (e1, e2) in enumerate(self.pairs): print('%s_%s_table=' % (e1.get_symbol(), e2.get_symbol()), file=f) for i, R in enumerate(self.Rgrid): print('%.6e' % R, end=' ', file=f) for t in range(20): x = self.tables[p][i, t] if abs(x) < 1E-90: print('0.', end=' ', file=f) else: print('%.6e' % x, end=' ', file=f) print(file=f) print('\n\n', file=f) f.close() def plot(self, filename=None): """ Plot the Slater-Koster table with matplotlib. parameters: =========== filename: for graphics file """ try: import pylab as pl except: raise AssertionError('pylab could not be imported') fig = pl.figure() fig.subplots_adjust(hspace=0.0001, wspace=0.0001) mx = max(1, self.tables[0].max()) if self.nel == 2: mx = max(mx, self.tables[1].max()) for i in range(10): name = integrals[i] ax = pl.subplot(5, 2, i + 1) for p, (e1, e2) in enumerate(self.pairs): s1, s2 = e1.get_symbol(), e2.get_symbol() if p == 0: s = '-' lw = 1 alpha = 1.0 else: s = '--' lw = 4 alpha = 0.2 if np.all(abs(self.tables[p][:, i]) < 1E-10): ax.text(0.03, 0.02 + p * 0.15, 'No %s integrals for <%s|%s>' % (name, s1, s2), transform=ax.transAxes, size=10) if not ax.is_last_row(): pl.xticks([], []) if not ax.is_first_col(): pl.yticks([], []) else: pl.plot(self.Rgrid, self.tables[p][:, i], c='r', ls=s, lw=lw, alpha=alpha) pl.plot(self.Rgrid, self.tables[p][:, i + 10], c='b', ls=s, lw=lw, alpha=alpha) pl.axhline(0, c='k', ls='--') pl.title(name, position=(0.9, 0.8)) if ax.is_last_row(): pl.xlabel('r (Bohr)') else: pl.xticks([], []) if not ax.is_first_col(): pl.yticks([], []) pl.ylim(-mx, mx) pl.xlim(0) pl.figtext(0.3, 0.95, 'H', color='r', size=20) pl.figtext(0.34, 0.95, 'S', color='b', size=20) pl.figtext(0.38, 0.95, ' Slater-Koster tables', size=20) e1, e2 = self.ela.get_symbol(), self.elb.get_symbol() pl.figtext(0.3, 0.92, '(thin solid: <%s|%s>, wide dashed: <%s|%s>)' % (e1, e2, e2, e1), size=10) file = '%s_%s_slako.pdf' % (e1, e2) if filename != None: file = filename pl.savefig(file) def get_comment(self): """ Get comments concerning parametrization. """ return self.comment def set_comment(self, comment): """ Add optional one-liner comment for documenting the parametrization. """ self.comment += '\n' + comment def get_range(self, fractional_limit): """ Define ranges for the atoms: largest r such that Rnl(r)<limit. """ self.timer.start('define ranges') wf_range = 0.0 for el in self.elements: r = max([ el.get_wf_range(nl, fractional_limit) for nl in el.get_valence_orbitals() ]) print('wf range for %s=%10.5f' % (el.get_symbol(), r), file=self.txt) wf_range = max(r, wf_range) if wf_range > 20: raise AssertionError( 'Wave function range >20 Bohr. Decrease wflimit?') return wf_range self.timer.stop('define ranges') def run(self, R1, R2, N, ntheta=150, nr=50, wflimit=1E-7): """ Calculate the Slater-Koster table. parameters: ------------ R1, R2, N: make table from R1 to R2 with N points ntheta: number of angular divisions in polar grid. (more dense towards bonding region) nr: number of radial divisions in polar grid. (more dense towards origins) with p=q=2 (powers in polar grid) ntheta~3*nr is optimum (with fixed grid size) with ntheta=150, nr=50 you get~1E-4 accuracy for H-elements (beyond that, gain is slow with increasing grid size) wflimit: use max range for wfs such that at R(rmax)<wflimit*max(R(r)) """ if R1 < 1E-3: raise AssertionError('For stability; use R1>~1E-3') self.timer.start('calculate tables') self.wf_range = self.get_range(wflimit) Rgrid = np.linspace(R1, R2, N) self.N = N self.Rgrid = Rgrid self.dH = 0.0 self.Hmax = 0.0 if self.nel == 1: self.tables = [np.zeros((N, 20))] else: self.tables = [np.zeros((N, 20)), np.zeros((N, 20))] print('Start making table...', file=self.txt) for Ri, R in enumerate(Rgrid): if R > 2 * self.wf_range: break grid, areas = self.make_grid(R, nt=ntheta, nr=nr) if Ri == N - 1 or N // 10 == 0 or np.mod(Ri, N // 10) == 0: print('R=%8.2f, %i grid points ...' % (R, len(grid)), file=self.txt) for p, (e1, e2) in enumerate(self.pairs): selected = select_integrals(e1, e2) if Ri == 0: print('R=%8.2f %s-%s, %i grid points, ' % (R, e1.get_symbol(), e2.get_symbol(), len(grid)), end=' ', file=self.txt) print('integrals:', end=' ', file=self.txt) for s in selected: print(s[0], end=' ', file=self.txt) print(file=self.txt) S, H, H2 = self.calculate_mels(selected, e1, e2, R, grid, areas) self.Hmax = max(self.Hmax, max(abs(H))) self.dH = max(self.dH, max(abs(H - H2))) self.tables[p][Ri, :10] = H self.tables[p][Ri, 10:] = S print('Maximum value for H=%.2g' % self.Hmax, file=self.txt) print('Maximum error for H=%.2g' % self.dH, file=self.txt) print(' Relative error=%.2g %%' % (self.dH / self.Hmax * 100), file=self.txt) self.timer.stop('calculate tables') self.comment += '\n' + asctime() self.txt.flush() def calculate_mels(self, selected, e1, e2, R, grid, area): """ Perform integration for selected H and S integrals. parameters: ----------- selected: list of [('dds','3d','4d'),(...)] e1: <bra| element e2: |ket> element R: e1 is at origin, e2 at z=R grid: list of grid points on (d,z)-plane area: d-z areas of the grid points. return: ------- List of H,S and H2 for selected integrals. H2 is calculated using different technique and can be used for error estimation. S: simply R1*R2*angle-part H: operate (derivate) R2 <R1|t+Veff1+Veff2-Conf1-Conf2|R2> H2: operate with full h2 and hence use eigenvalue of |R2> with full Veff2 <R1|(t1+Veff1)+Veff2-Conf1-Conf2|R2> = <R1|h1+Veff2-Conf1-Conf2|R2> (operate with h1 on left) = <R1|e1+Veff2-Conf1-Conf2|R2> = e1*S + <R1|Veff2-Conf1-Conf2|R2> -> H and H2 can be compared and error estimated """ self.timer.start('calculate_mels') Sl, Hl, H2l = np.zeros(10), np.zeros(10), np.zeros(10) # common for all integrals (not wf-dependent parts) self.timer.start('prelude') N = len(grid) gphi, radii, v1, v2 = zeros((N, 10)), zeros((N, 2)), zeros(N), zeros(N) for i, (d, z) in enumerate(grid): r1, r2 = sqrt(d**2 + z**2), sqrt(d**2 + (R - z)**2) t1, t2 = arccos(z / r1), arccos((z - R) / r2) radii[i, :] = [r1, r2] gphi[i, :] = g(t1, t2) v1[i] = e1.effective_potential(r1) - e1.confinement_potential(r1) v2[i] = e2.effective_potential(r2) - e2.confinement_potential(r2) self.timer.stop('prelude') # calculate all selected integrals for integral, nl1, nl2 in selected: index = integrals.index(integral) S, H, H2 = 0.0, 0.0, 0.0 l2 = angular_momentum[nl2[1]] for i, dA in enumerate(area): r1, r2 = radii[i, :] d, z = grid[i] aux = gphi[i, index] * dA * d Rnl1, Rnl2, ddunl2 = e1.Rnl(r1, nl1), e2.Rnl(r2, nl2), e2.unl(r2, nl2, der=2) S += Rnl1 * Rnl2 * aux H += Rnl1 * (-0.5 * ddunl2 / r2 + (v1[i] + v2[i] + l2 * (l2 + 1) / (2 * r2**2)) * Rnl2) * aux H2 += Rnl1 * Rnl2 * aux * (v2[i] - e1.confinement_potential(r1)) H2 += e1.get_epsilon(nl1) * S Sl[index] = S Hl[index] = H H2l[index] = H2 self.timer.stop('calculate_mels') return Sl, Hl, H2l def make_grid(self, Rz, nt, nr, p=2, q=2, view=False): """ Construct a double-polar grid. Parameters: ----------- Rz: element 1 is at origin, element 2 at z=Rz nt: number of theta grid points nr: number of radial grid points p: power describing the angular distribution of grid points (larger puts more weight towards theta=0) q: power describing the radial disribution of grid points (larger puts more weight towards centers) view: view the distribution of grid points with pylab. Plane at R/2 divides two polar grids. ^ (z-axis) |--------_____ phi_j | / ----__ * | / \ / * | / \ / X * X=coordinates of the center of area element(z,d), | / \ \-----* phi_(j+1) area=(r_(i+1)**2-r_i**2)*(phi_(j+1)-phi_j)/2 | / \ r_i r_(i+1) | / \ | / | *2------------------------| polar centered on atom 2 | \ | | \ / 1 | \ / / \ |-------------------------- z=h -line ordering of sector slice / \ | / \ points: / \ | / \ / \ | / | / 0 4 *1------------------------|---> polar centered on atom 1 2 / | \ | (r_perpendicular (xy-plane) = 'd-axis') \ / | \ / \ / | \ / 3 | \ / | \ / | \ / | \ ___ --- |--------- """ self.timer.start('make grid') rmin, rmax = (1E-7, self.wf_range) max_range = self.wf_range h = Rz / 2 T = np.linspace(0, 1, nt)**p * np.pi R = rmin + np.linspace(0, 1, nr)**q * (rmax - rmin) grid = [] area = [] # first calculate grid for polar centered on atom 1: # the z=h-like starts cutting full elements starting from point (1) for j in range(nt - 1): for i in range(nr - 1): # corners of area element d1, z1 = R[i + 1] * sin(T[j]), R[i + 1] * cos(T[j]) d2, z2 = R[i] * sin(T[j]), R[i] * cos(T[j]) d3, z3 = R[i] * sin(T[j + 1]), R[i] * cos(T[j + 1]) d4, z4 = R[i + 1] * sin(T[j + 1]), R[i + 1] * cos(T[j + 1]) A0 = (R[i + 1]**2 - R[i]**2) * (T[j + 1] - T[j]) / 2 if z1 <= h: # area fully inside region r0 = 0.5 * (R[i] + R[i + 1]) t0 = 0.5 * (T[j] + T[j + 1]) A = A0 elif z1 > h and z2 <= h and z4 <= h: # corner 1 outside region Th = np.arccos(h / R[i + 1]) r0 = 0.5 * (R[i] + R[i + 1]) t0 = 0.5 * (Th + T[j + 1]) A = A0 A -= 0.5 * R[i + 1]**2 * (Th - T[j]) - 0.5 * h**2 * ( tan(Th) - tan(T[j])) elif z1 > h and z2 > h and z3 <= h and z4 <= h: # corners 1 and 2 outside region Th1 = np.arccos(h / R[i]) Th2 = np.arccos(h / R[i + 1]) r0 = 0.5 * (R[i] + R[i + 1]) t0 = 0.5 * (Th2 + T[j + 1]) A = A0 A -= A0 * (Th1 - T[j]) / (T[j + 1] - T[j]) A -= 0.5 * R[i + 1]**2 * (Th2 - Th1) - 0.5 * h**2 * ( tan(Th2) - tan(Th1)) elif z1 > h and z2 > h and z4 > h and z3 <= h: # only corner 3 inside region Th = np.arccos(h / R[i]) r0 = 0.5 * (R[i] + h / cos(T[j + 1])) t0 = 0.5 * (Th + T[j + 1]) A = 0.5 * h**2 * (tan( T[j + 1]) - tan(Th)) - 0.5 * R[i]**2 * (T[j + 1] - Th) elif z1 > h and z4 > h and z2 <= h and z3 <= h: # corners 1 and 4 outside region r0 = 0.5 * (R[i] + h / cos(T[j + 1])) t0 = 0.5 * (T[j] + T[j + 1]) A = 0.5 * h**2 * (tan(T[j + 1]) - tan( T[j])) - 0.5 * R[i]**2 * (T[j + 1] - T[j]) elif z3 > h: A = -1 else: raise RuntimeError('Illegal coordinates.') d, z = (r0 * sin(t0), r0 * cos(t0)) if A > 0 and sqrt(d**2 + z**2) < max_range and sqrt( d**2 + (Rz - z)**2) < max_range: grid.append([d, z]) area.append(A) self.timer.start('symmetrize') # calculate the polar centered on atom 2 by mirroring the other grid grid = np.array(grid) area = np.array(area) grid2 = grid.copy() grid2[:, 1] = -grid[:, 1] shift = np.zeros_like(grid) shift[:, 1] = 2 * h grid = np.concatenate((grid, grid2 + shift)) area = np.concatenate((area, area)) self.timer.stop('symmetrize') if view: import pylab as pl pl.plot([h, h, h]) pl.scatter(grid[:, 0], grid[:, 1], s=10 * area / max(area)) pl.show() self.timer.stop('make grid') return grid, area
class LinearResponse: """ """ def __init__(self, calc, energy_cut=10.0, timing=False, txt=None): """ Calculate linear optical response (LR-TD-DFTB). For details, see Niehaus et.al. Phys. Rev. B 63, 085108 (2001) parameters: =========== calc: calculator object energy_cut: max energy (in eV) for particle-hole excitations Used to select all particle-hole excitations in the construction of the matrix, that have excitation energy less than this value. This implies, that if we are interested in optical response up to some energy, energy_cut should be slightly larger. timing: output timing summary after calculation out: output object (file name or object) """ self.calc = calc self.st = calc.st self.el = calc.el self.es = calc.st.es self.energy_cut = energy_cut / Hartree # self.noc=self.st.get_hoc()+1 #number of occupied states (not index) # do not use HOC self.nel = self.el.get_number_of_electrons() self.norb = self.el.get_nr_orbitals() self.e = self.st.get_eigenvalues()[0, :] self.f = self.st.get_occupations()[0, :] if np.any(abs(self.st.wf.flatten().imag) > 1e-10): raise ValueError("Wave functions should not be complex.") self.wf = self.st.wf[0].real self.S = self.st.S[0].real self.N = len(self.el) self.SCC = self.calc.get("SCC") atoms = calc.get_atoms() if atoms.pbc.any(): raise AssertionError("No linear response for extended, periodic systems!") # if abs(np.mod(self.nel,2))>1E-2: # raise RuntimeError('Linear response only for closed shell systems! (even number of electrons)') # if abs(self.nel-2*self.noc)>1E-2: # print 'Number of electrons:',self.nel # print '2*Number of occupied states:',2*self.noc # raise RuntimeError('Number of electrons!=2*number of occupied orbitals. Decrease electronic temperature?') if txt is None: self.txt = sys.stdout else: self.txt = open(txt, "a") self.timer = Timer("Linear Response", txt=self.txt) self.timing = timing self.done = False self.allowed_cut = 1e-2 # if osc.strength is smaller, transition is not allowed self._initialize() def _initialize(self): """ Perform some initialization calculations. """ self.timer.start("init") self.Swf = np.dot(self.S, self.wf.transpose()) # try to avoind the transpose self.timer.stop("init") def get_linear_response(self): """ Get linear response spectrum in eV. """ return self.omega * Hartree, self.F def mulliken_transfer(self, k, l): """ Return Mulliken transfer charges between states k and l. """ q = [] for i, o1, no in self.el.get_property_lists(["i", "o1", "no"]): # qi=sum( [self.wf[a,k]*self.Swf[a,l]+self.wf[a,l]*self.Swf[a,k] for a in range(o1,o1+no)] ) qi = sum([self.wf[k, a] * self.Swf[a, l] + self.wf[l, a] * self.Swf[a, k] for a in range(o1, o1 + no)]) q.append(qi / 2) return np.array(q) def run(self): """ Run the calculation. """ if self.done == True: raise AssertionError("Run LR calculation only once.") print >> self.txt, "\nLR for %s (charge %.2f). " % (self.el.get_name(), self.calc.get_charge()), # # select electron-hole excitations (i occupied, j not occupied) # de = excitation energy ej-ei (ej>ei) # df = occupation difference fi-fj (ej>ei so that fi>fj) # de = [] df = [] particle_holes = [] self.timer.start("setup ph pairs") for i in range(self.norb): for j in range(i + 1, self.norb): energy = self.e[j] - self.e[i] occup = (self.f[i] - self.f[j]) / 2 # normalize the double occupations (...is this rigorously right?) if energy < self.energy_cut and occup > 1e-6: assert energy > 0 and occup > 0 particle_holes.append([i, j]) de.append(energy) df.append(occup) self.timer.stop("setup ph pairs") de = np.array(de) df = np.array(df) # # setup the matrix (gamma-approximation) and diagonalize # self.timer.start("setup matrix") dim = len(de) print >> self.txt, "Dimension %i. " % dim, if not 0 < dim < 100000: raise RuntimeError("Coupling matrix too large or small (%i)" % dim) r = self.el.get_positions() transfer_q = np.array([self.mulliken_transfer(ph[0], ph[1]) for ph in particle_holes]) rv = np.array([dot(tq, r) for tq in transfer_q]) matrix = np.zeros((dim, dim)) if self.SCC: gamma = self.es.get_gamma().copy() gamma_tq = np.zeros((dim, self.N)) for k in range(dim): gamma_tq[k, :] = dot(gamma, transfer_q[k, :]) for k1, ph1 in enumerate(particle_holes): matrix[k1, k1] = de[k1] ** 2 for k2, ph2 in enumerate(particle_holes): coupling = dot(transfer_q[k1, :], gamma_tq[k2, :]) matrix[k1, k2] += 2 * sqrt(df[k1] * de[k1] * de[k2] * df[k2]) * coupling else: for k1, ph1 in enumerate(particle_holes): matrix[k1, k1] = de[k1] ** 2 self.timer.stop("setup matrix") print >> self.txt, "coupling matrix constructed. ", self.txt.flush() self.timer.start("diagonalize") omega2, eigv = eigh(matrix) self.timer.stop("diagonalize") print >> self.txt, "Matrix diagonalized.", self.txt.flush() # assert np.all(omega2>1E-16) print omega2 omega = sqrt(omega2) # calculate oscillator strengths F = [] collectivity = [] self.timer.start("oscillator strengths") for ex in range(dim): v = [] for i in range(3): v.append(sum(rv[:, i] * sqrt(df[:] * de[:]) * eigv[:, ex]) / sqrt(omega[ex]) * 2) F.append(omega[ex] * dot(v, v) * 2.0 / 3) collectivity.append(1 / sum(eigv[:, ex] ** 4)) self.omega = omega self.F = F self.eigv = eigv self.collectivity = collectivity self.dim = dim self.particle_holes = particle_holes self.timer.stop("oscillator strengths") if self.timing: self.timer.summary() self.done = True self.emax = max(omega) self.particle_holes = particle_holes def info(self): """ Some info about excitations (energy, main p-h excitations,...) """ print "\n#e(eV), f, collectivity, transitions ..." for ex in range(self.dim): if self.F[ex] < self.allowed_cut: continue print "%.5f %.5f %8.1f" % (self.omega[ex] * Hartree, self.F[ex], self.collectivity[ex]), order = np.argsort(abs(self.eigv[:, ex]))[::-1] for ph in order[:4]: i, j = self.particle_holes[ph] print "%3i-%-3i:%-10.3f" % (i, j, self.eigv[ph, ex] ** 2), print def get_excitation(self, i, allowed=True): """ Return energy (eV) and oscillation strength for i'th allowed excitation index. i=0 means first excitation """ if allowed == False: return self.omega[i] * Hartree, self.F[i] else: p = -1 for k in range(self.dim): if self.F[k] >= self.allowed_cut: p += 1 if p == i: return self.omega[k] * Hartree, self.F[k] def write_spectrum(self, filename=None): """ Write the linear response spectrum into file. """ if filename == None: filename = "linear_spectrum.out" o = open(filename, "w") print >> o, "#e(eV), f" for ex in range(self.dim): print >> o, "%10.5f %10.5f %10.5f" % (self.omega[ex] * Hartree, self.F[ex], self.collectivity[ex]) o.close() def read_spectrum(self, filename): """ Read the linear response from given file. Format: energy & oscillator strength. """ o = open(filename, "r") data = mix.read(filename) self.omega, self.F, self.collectivity = data[:, 0], data[:, 1], data[:, 2] def plot_spectrum(self, filename, width=0.2, xlim=None): """ Make pretty plot of the linear response. Parameters: =========== filename: output file name (&format, supported by matplotlib) width: width of Lorenzian broadening xlim: energy range for plotting tuple (emin,emax) """ import pylab as pl if not self.done: self.run() e, f = mix.broaden(self.omega * Hartree, self.F, width=width, N=1000, extend=True) f = f / max(abs(f)) pl.plot(e, f, lw=2) xs, ys = pl.poly_between(e, 0, f) pl.fill(xs, ys, fc="b", ec="b", alpha=0.5) pl.ylim(0, 1.2) if xlim == None: pl.xlim(0, self.emax * Hartree * 1.2) else: pl.xlim(xlim) pl.xlabel("energy (eV)") pl.ylabel("linear optical response") pl.title("Optical response") pl.savefig(filename) # pl.show() pl.close()
class KSAllElectron: def __init__(self,symbol, configuration={}, valence=[], confinement=None, xc='PW92', convergence={'density':1E-7,'energies':1E-7}, scalarrel=False, rmax=100.0, nodegpts=500, mix=0.2, itmax=200, timing=False, verbose=False, txt=None, restart=None, write=None): """ Make Kohn-Sham all-electron calculation for given atom. Examples: --------- atom=KSAllElectron('C') atom=KSAllElectron('C',confinement={'mode':'quadratic','r0':1.234}) atom.run() Parameters: ----------- symbol: chemical symbol configuration: e.g. {'2s':2,'2p':2}. Overrides (for orbitals given in dict) default configuration from box.data. valence: valence orbitals, e.g. ['2s','2p']. Overrides default valence from box.data. confinement: additional confining potential (see ConfinementPotential class) etol: sp energy tolerance for eigensolver (Hartree) convergence: convergence criterion dictionary * density: max change for integrated |n_old-n_new| * energies: max change in single-particle energy (Hartree) scalarrel: Use scalar relativistic corrections rmax: radial cutoff nodegpts: total number of grid points is nodegpts times the max number of antinodes for all orbitals mix: effective potential mixing constant itmax: maximum number of iterations for self-consistency. timing: output of timing summary verbose: increase verbosity during iterations txt: output file name for log data write: filename: save rgrid, effective potential and density to a file for further calculations. restart: filename: make an initial guess for effective potential and density from another calculation. """ self.symbol=symbol self.valence=valence self.confinement=confinement self.xc=xc self.convergence=convergence self.scalarrel = scalarrel self.set_output(txt) self.itmax=itmax self.verbose=verbose self.nodegpts=nodegpts self.mix=mix self.timing=timing self.timer=Timer('KSAllElectron',txt=self.txt,enabled=self.timing) self.timer.start('init') self.restart = restart self.write = write # element data self.data=copy( data[self.symbol] ) self.Z=self.data['Z'] if self.valence == []: self.valence = copy( data[self.symbol]['valence_orbitals'] ) # ... more specific self.occu = copy( data[self.symbol]['configuration'] ) nel_neutral = self.Z assert sum(self.occu.values()) == nel_neutral self.occu.update( configuration ) self.nel=sum(self.occu.values()) self.charge=nel_neutral-self.nel if self.confinement==None: self.confinement_potential=ConfinementPotential('none') else: self.confinement_potential=ConfinementPotential(**self.confinement) self.conf=None self.nucl=None self.exc=None if self.xc=='PW92': self.xcf=XC_PW92() else: ## MS: add support for functionals from libxc from .pylibxc_interface import libXCFunctional self.xcf = libXCFunctional(self.xc) # technical stuff self.maxl=9 self.maxn=9 self.plotr={} self.unlg={} self.Rnlg={} self.unl_fct={} self.Rnl_fct={} self.veff_fct=None self.total_energy=0.0 maxnodes=max( [n-l-1 for n,l,nl in self.list_states()] ) self.rmin, self.rmax, self.N=( 1E-2/self.Z, rmax, (maxnodes+1)*self.nodegpts ) if self.scalarrel: print('Using scalar relativistic corrections.', file=self.txt) print('max %i nodes, %i grid points' %(maxnodes,self.N), file=self.txt) self.xgrid=np.linspace(0,np.log(self.rmax/self.rmin),self.N) self.rgrid=self.rmin*np.exp(self.xgrid) self.grid=RadialGrid(self.rgrid) self.timer.stop('init') print(self.get_comment(), file=self.txt) self.solved=False def __getstate__(self): """ Return dictionary of all pickable items. """ d=self.__dict__.copy() for key in self.__dict__: if isinstance(d[key], collections.Callable): d.pop(key) d.pop('out') return d def set_output(self,txt): """ Set output channel and give greetings. """ if txt == '-': self.txt = open(os.devnull,'w') elif txt==None: self.txt=sys.stdout else: self.txt=open(txt,'a') print('*******************************************', file=self.txt) print('Kohn-Sham all-electron calculation for %2s ' %self.symbol, file=self.txt) print('*******************************************', file=self.txt) def calculate_energies(self,echo=False): """ Calculate energy contributions. """ self.timer.start('energies') self.bs_energy=0.0 for n,l,nl in self.list_states(): self.bs_energy+=self.occu[nl]*self.enl[nl] ## MS: re-write exc as a function of rho on grid self.xcf.set_grid(self.grid) self.exc=self.xcf.exc(self.dens) self.Hartree_energy=self.grid.integrate(self.Hartree*self.dens,use_dV=True)/2 self.vxc_energy=self.grid.integrate(self.vxc*self.dens,use_dV=True) self.exc_energy=self.grid.integrate(self.exc*self.dens,use_dV=True) self.confinement_energy=self.grid.integrate(self.conf*self.dens,use_dV=True) self.total_energy=self.bs_energy-self.Hartree_energy-self.vxc_energy+self.exc_energy if echo: print('\n\nEnergetics:', file=self.txt) print('-------------', file=self.txt) print('\nsingle-particle energies', file=self.txt) print('------------------------', file=self.txt) for n,l,nl in self.list_states(): print('%s, energy %.15f' %(nl,self.enl[nl]), file=self.txt) print('\nvalence orbital energies', file=self.txt) print('--------------------------', file=self.txt) for nl in data[self.symbol]['valence_orbitals']: print('%s, energy %.15f' %(nl,self.enl[nl]), file=self.txt) print('\n', file=self.txt) print('total energies:', file=self.txt) print('---------------', file=self.txt) print('sum of eigenvalues: %.15f' %self.bs_energy, file=self.txt) print('Hartree energy: %.15f' %self.Hartree_energy, file=self.txt) print('vxc correction: %.15f' %self.vxc_energy, file=self.txt) print('exchange + corr energy: %.15f' %self.exc_energy, file=self.txt) print('----------------------------', file=self.txt) print('total energy: %.15f\n\n' %self.total_energy, file=self.txt) self.timer.stop('energies') def calculate_density(self): """ Calculate the radial electron density.; sum_nl |Rnl(r)|**2/(4*pi) """ self.timer.start('density') dens=np.zeros_like(self.rgrid) for n,l,nl in self.list_states(): dens+=self.occu[nl]*self.unlg[nl]**2 nel=self.grid.integrate(dens) if abs(nel-self.nel)>1E-10: raise RuntimeError('Integrated density %.3g, number of electrons %.3g' %(nel,self.nel) ) dens=dens/(4*np.pi*self.rgrid**2) self.timer.stop('density') return dens def calculate_Hartree_potential(self): """ Calculate Hartree potential. Everything is very sensitive to the way this is calculated. If you can think of how to improve this, please tell me! """ self.timer.start('Hartree') dV=self.grid.get_dvolumes() r, r0=self.rgrid, self.grid.get_r0grid() N=self.N n0=0.5*(self.dens[1:]+self.dens[:-1]) n0*=self.nel/sum(n0*dV) lo, hi, Hartree=np.zeros(N), np.zeros(N), np.zeros(N) lo[0]=0.0 for i in range(1,N): lo[i] = lo[i-1] + dV[i-1]*n0[i-1] hi[-1]=0.0 for i in range(N-2,-1,-1): hi[i] = hi[i+1] + n0[i]*dV[i]/r0[i] for i in range(N): Hartree[i] = lo[i]/r[i] + hi[i] self.Hartree=Hartree self.timer.stop('Hartree') def V_nuclear(self,r): return -self.Z/r def calculate_veff(self): """ Calculate effective potential. """ self.timer.start('veff') ## MS: re-write xcf.vxc as function of density on grid self.xcf.set_grid(self.grid) self.vxc=self.xcf.vxc(self.dens) self.timer.stop('veff') return self.nucl + self.Hartree + self.vxc + self.conf def guess_density(self): """ Guess initial density. """ r2=0.02*self.Z # radius at which density has dropped to half; improve this! dens=np.exp( -self.rgrid/(r2/np.log(2)) ) dens=dens/self.grid.integrate(dens,use_dV=True)*self.nel #pl.plot(self.rgrid,dens) return dens def get_veff_and_dens(self): """ Construct effective potential and electron density. If restart file is given, try to read from there, otherwise make a guess. """ done = False if self.restart is not None: # use density and effective potential from another calculation try: from scipy.interpolate import splrep, splev f = open(self.restart, 'rb') rgrid = pickle.load(f) veff = pickle.load(f) dens = pickle.load(f) v = splrep(rgrid, veff) d = splrep(rgrid, dens) self.veff = array([splev(r,v) for r in self.rgrid]) self.dens = array([splev(r,d) for r in self.rgrid]) f.close() done = True except: print("Could not open restart file, " \ "starting from scratch.", file=self.txt) if not done: self.veff=self.nucl+self.conf self.dens=self.guess_density() def run(self): """ Solve the self-consistent potential. """ self.timer.start('solve ground state') print('\nStart iteration...', file=self.txt) self.enl={} self.d_enl={} for n,l,nl in self.list_states(): self.enl[nl]=0.0 self.d_enl[nl]=0.0 N=self.grid.get_N() # make confinement and nuclear potentials; intitial guess for veff self.conf=array([self.confinement_potential(r) for r in self.rgrid]) self.nucl=array([self.V_nuclear(r) for r in self.rgrid]) self.get_veff_and_dens() self.calculate_Hartree_potential() #self.Hartree=np.zeros((N,)) for it in range(self.itmax): self.veff=self.mix*self.calculate_veff()+(1-self.mix)*self.veff if self.scalarrel: veff = SplineFunction(self.rgrid, self.veff) self.dveff = array([veff(r, der=1) for r in self.rgrid]) d_enl_max, itmax=self.solve_eigenstates(it) dens0=self.dens.copy() self.dens=self.calculate_density() diff=self.grid.integrate(np.abs(self.dens-dens0),use_dV=True) if diff<self.convergence['density'] and d_enl_max<self.convergence['energies'] and it > 5: break self.calculate_Hartree_potential() if np.mod(it,10)==0: print('iter %3i, dn=%.1e>%.1e, max %i sp-iter' %(it,diff,self.convergence['density'],itmax), file=self.txt) if it==self.itmax-1: if self.timing: self.timer.summary() raise RuntimeError('Density not converged in %i iterations' %(it+1)) self.txt.flush() self.calculate_energies(echo=True) print('converged in %i iterations' %it, file=self.txt) print('%9.4f electrons, should be %9.4f' %(self.grid.integrate(self.dens,use_dV=True),self.nel), file=self.txt) for n,l,nl in self.list_states(): self.Rnl_fct[nl]=Function('spline',self.rgrid,self.Rnlg[nl]) self.unl_fct[nl]=Function('spline',self.rgrid,self.unlg[nl]) self.timer.stop('solve ground state') self.timer.summary() self.txt.flush() self.solved=True if self.write != None: f=open(self.write,'wb') pickle.dump(self.rgrid, f) pickle.dump(self.veff, f) pickle.dump(self.dens, f) f.close() def solve_eigenstates(self,iteration,itmax=100): """ Solve the eigenstates for given effective potential. u''(r) - 2*(v_eff(r)+l*(l+1)/(2r**2)-e)*u(r)=0 ( u''(r) + c0(r)*u(r) = 0 ) r=r0*exp(x) --> (to get equally spaced integration mesh) u''(x) - u'(x) + c0(x(r))*u(r) = 0 """ self.timer.start('eigenstates') rgrid=self.rgrid xgrid=self.xgrid dx=xgrid[1]-xgrid[0] N=self.N c2=np.ones(N) c1=-np.ones(N) d_enl_max=0.0 itmax=0 for n,l,nl in self.list_states(): nodes_nl=n-l-1 if iteration==0: eps=-1.0*self.Z**2/n**2 else: eps=self.enl[nl] if iteration<=3: delta=0.5*self.Z**2/n**2 #previous!!!!!!!!!! else: delta=self.d_enl[nl] direction='none' epsmax=self.veff[-1]-l*(l+1)/(2*self.rgrid[-1]**2) it=0 u=np.zeros(N) hist=[] while True: eps0=eps c0, c1, c2 = self.construct_coefficients(l, eps) # boundary conditions for integration from analytic behaviour (unscaled) # u(r)~r**(l+1) r->0 # u(r)~exp( -sqrt(c0(r)) ) (set u[-1]=1 and use expansion to avoid overflows) u[0:2]=rgrid[0:2]**(l+1) if not(c0[-2]<0 and c0[-1]<0): pl.plot(c0) pl.show() assert c0[-2]<0 and c0[-1]<0 u, nodes, A, ctp=shoot(u,dx,c2,c1,c0,N) it+=1 norm=self.grid.integrate(u**2) u=u/sqrt(norm) if nodes>nodes_nl: # decrease energy if direction=='up': delta/=2 eps-=delta direction='down' elif nodes<nodes_nl: # increase energy if direction=='down': delta/=2 eps+=delta direction='up' elif nodes==nodes_nl: shift=-0.5*A/(rgrid[ctp]*norm) if abs(shift)<1E-8: #convergence break if shift>0: direction='up' elif shift<0: direction='down' eps+=shift if eps>epsmax: eps=0.5*(epsmax+eps0) hist.append(eps) if it>100: print('Epsilon history for %s' %nl, file=self.txt) for h in hist: print(h) print('nl=%s, eps=%f' %(nl,eps), file=self.txt) print('max epsilon',epsmax, file=self.txt) raise RuntimeError('Eigensolver out of iterations. Atom not stable?') itmax=max(it,itmax) self.unlg[nl]=u self.Rnlg[nl]=self.unlg[nl]/self.rgrid self.d_enl[nl]=abs(eps-self.enl[nl]) d_enl_max=max(d_enl_max,self.d_enl[nl]) self.enl[nl]=eps if self.verbose: print('-- state %s, %i eigensolver iterations, e=%9.5f, de=%9.5f' %(nl,it,self.enl[nl],self.d_enl[nl]), file=self.txt) assert nodes==nodes_nl assert u[1]>0.0 self.timer.stop('eigenstates') return d_enl_max, itmax def construct_coefficients(self, l, eps): c = 137.036 c2 = np.ones(self.N) if self.scalarrel == False: c0 = -2*( 0.5*l*(l+1)+self.rgrid**2*(self.veff-eps) ) c1 = -np.ones(self.N) else: # from Paolo Giannozzi: Notes on pseudopotential generation ScR_mass = array([1 + 0.5*(eps-V)/c**2 for V in self.veff]) c0 = -l*(l+1) - 2*ScR_mass*self.rgrid**2*(self.veff-eps) - self.dveff*self.rgrid/(2*ScR_mass*c**2) c1 = self.rgrid*self.dveff/(2*ScR_mass*c**2) - 1 return c0, c1, c2 def plot_Rnl(self,filename=None): """ Plot radial wave functions with matplotlib. filename: output file name + extension (extension used in matplotlib) """ if pl==None: raise AssertionError('pylab could not be imported') rmax = data[self.symbol]['R_cov']/0.529177*3 ri = np.where( self.rgrid<rmax )[0][-1] states=len(self.list_states()) p = np.ceil(np.sqrt(states)) #p**2>=states subplots fig=pl.figure() i=1 # as a function of grid points for n,l,nl in self.list_states(): ax=pl.subplot(2*p,p,i) pl.plot(self.Rnlg[nl]) # pl.yticks([],[]) pl.yticks(size=5) pl.xticks(size=5) # annotate c = 'r' if (nl in self.valence) else 'k' pl.text(0.5,0.4,r'$R_{%s}(gridpts)$' %nl, \ transform=ax.transAxes,size=15,color=c) if ax.is_first_col(): pl.ylabel(r'$R_{nl}(r)$',size=8) i+=1 # as a function of radius i = p**2+1 for n,l,nl in self.list_states(): ax=pl.subplot(2*p,p,i) pl.plot(self.rgrid[:ri],self.Rnlg[nl][:ri]) # pl.yticks([],[]) pl.yticks(size=5) pl.xticks(size=5) if ax.is_last_row(): pl.xlabel('r (Bohr)',size=8) # annotate c = 'r' if (nl in self.valence) else 'k' pl.text(0.5,0.4,r'$R_{%s}(r)$' %nl, \ transform=ax.transAxes,size=15,color=c) if ax.is_first_col(): pl.ylabel(r'$R_{nl}(r)$',size=8) i+=1 filen = '%s_KSAllElectron.pdf' %self.symbol #pl.rc('figure.subplot',wspace=0.0,hspace=0.0) fig.subplots_adjust(hspace=0.2,wspace=0.1) s = '' if (self.confinement is None) else '(confined)' pl.figtext(0.4,0.95,r'$R_{nl}(r)$ for %s-%s %s' %(self.symbol,self.symbol,s)) if filename is not None: filen = filename pl.savefig(filen) def get_wf_range(self,nl,fractional_limit=1E-7): """ Return the maximum r for which |R(r)|<fractional_limit*max(|R(r)|) """ wfmax=max(abs(self.Rnlg[nl])) for r,wf in zip(self.rgrid[-1::-1],self.Rnlg[nl][-1::-1]): if abs(wf)>fractional_limit*wfmax: return r def list_states(self): """ List all potential states {(n,l,'nl')}. """ states=[] for l in range(self.maxl+1): for n in range(1,self.maxn+1): nl=orbit_transform((n,l),string=True) if nl in self.occu: states.append((n,l,nl)) return states def get_energy(self): return self.total_energy def get_epsilon(self,nl): """ get_eigenvalue('2p') or get_eigenvalue((2,1)) """ nls=orbit_transform(nl,string=True) if not self.solved: raise AssertionError('run calculations first.') return self.enl[nls] def effective_potential(self,r,der=0): """ Return effective potential at r or its derivatives. """ if self.veff_fct==None: self.veff_fct=Function('spline',self.rgrid,self.veff) return self.veff_fct(r,der=der) def get_radial_density(self): return self.rgrid,self.dens def Rnl(self,r,nl,der=0): """ Rnl(r,'2p') or Rnl(r,(2,1))""" nls=orbit_transform(nl,string=True) return self.Rnl_fct[nls](r,der=der) def unl(self,r,nl,der=0): """ unl(r,'2p')=Rnl(r,'2p')/r or unl(r,(2,1))...""" nls=orbit_transform(nl,string=True) return self.unl_fct[nls](r,der=der) def get_valence_orbitals(self): """ Get list of valence orbitals, e.g. ['2s','2p'] """ return self.valence def get_symbol(self): """ Return atom's chemical symbol. """ return self.symbol def get_comment(self): """ One-line comment, e.g. 'H, charge=0, quadratic, r0=4' """ comment='%s xc=%s charge=%.1f conf:%s' %(self.symbol,self.xc,float(self.charge),self.confinement_potential.get_comment()) return comment def get_valence_energies(self): """ Return list of valence energies, e.g. ['2s','2p'] --> [-39.2134,-36.9412] """ if not self.solved: raise AssertionError('run calculations first.') return [(nl,self.enl[nl]) for nl in self.valence] def write_unl(self,filename,only_valence=True,step=20): """ Append functions unl=Rnl*r, V_effective, V_confinement into file. Only valence functions by default. Parameters: ----------- filename: output file name (e.g. XX.elm) only_valence: output of only valence orbitals step: step size for output grid """ if not self.solved: raise AssertionError('run calculations first.') if only_valence: orbitals=self.valence else: orbitals=[nl for n,l,nl in self.list_states()] o=open(filename,'a') for nl in orbitals: print('\n\nu_%s=' %nl, file=o) for r,u in zip(self.rgrid[::step],self.unlg[nl][::step]): print(r,u, file=o) print('\n\nv_effective=', file=o) for r,ve in zip(self.rgrid[::step],self.veff[::step]): print(r,ve, file=o) print('\n\nconfinement=', file=o) for r,vc in zip(self.rgrid[::step],self.conf[::step]): print(r,vc, file=o) print('\n\n', file=o)
class SlaterKosterTable: def __init__(self, ela, elb, txt=None, timing=False): """ Construct Slater-Koster table for given elements. parameters: ----------- ela: element objects (KSAllElectron or Element) elb: element objects (KSAllElectron or Element) txt: output file object or file name timing: output of timing summary after calculation """ self.ela = ela self.elb = elb self.timing = timing if txt == None: self.txt = sys.stdout else: if type(txt) == type(""): self.txt = open(txt, "a") else: self.txt = txt self.comment = self.ela.get_comment() if ela.get_symbol() != elb.get_symbol(): self.nel = 2 self.pairs = [(ela, elb), (elb, ela)] self.elements = [ela, elb] self.comment += "\n" + self.elb.get_comment() else: self.nel = 1 self.pairs = [(ela, elb)] self.elements = [ela] self.timer = Timer("SlaterKosterTable", txt=self.txt, enabled=timing) print >> self.txt, "\n\n\n\n" print >> self.txt, "************************************************" print >> self.txt, "Slater-Koster table construction for %2s and %2s" % (ela.get_symbol(), elb.get_symbol()) print >> self.txt, "************************************************" def __del__(self): self.timer.summary() def get_table(self): """ Return tables. """ return self.Rgrid, self.tables def smooth_tails(self): """ Smooth the behaviour of tables near cutoff. """ for p in range(self.nel): for i in range(20): self.tables[p][:, i] = tail_smoothening(self.Rgrid, self.tables[p][:, i]) def write(self, filename=None): """ Use symbol1_symbol2.par as default. """ self.smooth_tails() if filename == None: fn = "%s_%s.par" % (self.ela.get_symbol(), self.elb.get_symbol()) else: fn = filename f = open(fn, "w") print >> f, "slako_comment=" print >> f, self.get_comment(), "\n\n" for p, (e1, e2) in enumerate(self.pairs): print >> f, "%s_%s_table=" % (e1.get_symbol(), e2.get_symbol()) for i, R in enumerate(self.Rgrid): print >> f, "%.6e" % R, for t in xrange(20): x = self.tables[p][i, t] if abs(x) < 1e-90: print >> f, "0.", else: print >> f, "%.6e" % x, print >> f print >> f, "\n\n" f.close() def plot(self, filename=None): """ Plot the Slater-Koster table with matplotlib. parameters: =========== filename: for graphics file """ try: import pylab as pl except: raise AssertionError("pylab could not be imported") fig = pl.figure() fig.subplots_adjust(hspace=0.0001, wspace=0.0001) mx = max(1, self.tables[0].max()) if self.nel == 2: mx = max(mx, self.tables[1].max()) for i in xrange(10): name = integrals[i] ax = pl.subplot(5, 2, i + 1) for p, (e1, e2) in enumerate(self.pairs): s1, s2 = e1.get_symbol(), e2.get_symbol() if p == 0: s = "-" lw = 1 alpha = 1.0 else: s = "--" lw = 4 alpha = 0.2 if np.all(abs(self.tables[p][:, i]) < 1e-10): ax.text( 0.03, 0.02 + p * 0.15, "No %s integrals for <%s|%s>" % (name, s1, s2), transform=ax.transAxes, size=10, ) if not ax.is_last_row(): pl.xticks([], []) if not ax.is_first_col(): pl.yticks([], []) else: pl.plot(self.Rgrid, self.tables[p][:, i], c="r", ls=s, lw=lw, alpha=alpha) pl.plot(self.Rgrid, self.tables[p][:, i + 10], c="b", ls=s, lw=lw, alpha=alpha) pl.axhline(0, c="k", ls="--") pl.title(name, position=(0.9, 0.8)) if ax.is_last_row(): pl.xlabel("r (Bohr)") else: pl.xticks([], []) if not ax.is_first_col(): pl.yticks([], []) pl.ylim(-mx, mx) pl.xlim(0) pl.figtext(0.3, 0.95, "H", color="r", size=20) pl.figtext(0.34, 0.95, "S", color="b", size=20) pl.figtext(0.38, 0.95, " Slater-Koster tables", size=20) e1, e2 = self.ela.get_symbol(), self.elb.get_symbol() pl.figtext(0.3, 0.92, "(thin solid: <%s|%s>, wide dashed: <%s|%s>)" % (e1, e2, e2, e1), size=10) file = "%s_%s_slako.pdf" % (e1, e2) if filename != None: file = filename pl.savefig(file) def get_comment(self): """ Get comments concerning parametrization. """ return self.comment def set_comment(self, comment): """ Add optional one-liner comment for documenting the parametrization. """ self.comment += "\n" + comment def get_range(self, fractional_limit): """ Define ranges for the atoms: largest r such that Rnl(r)<limit. """ self.timer.start("define ranges") wf_range = 0.0 for el in self.elements: r = max([el.get_wf_range(nl, fractional_limit) for nl in el.get_valence_orbitals()]) print >> self.txt, "wf range for %s=%10.5f" % (el.get_symbol(), r) wf_range = max(r, wf_range) if wf_range > 20: raise AssertionError("Wave function range >20 Bohr. Decrease wflimit?") return wf_range self.timer.stop("define ranges") def run(self, R1, R2, N, ntheta=150, nr=50, wflimit=1e-7): """ Calculate the Slater-Koster table. parameters: ------------ R1, R2, N: make table from R1 to R2 with N points ntheta: number of angular divisions in polar grid. (more dense towards bonding region) nr: number of radial divisions in polar grid. (more dense towards origins) with p=q=2 (powers in polar grid) ntheta~3*nr is optimum (with fixed grid size) with ntheta=150, nr=50 you get~1E-4 accuracy for H-elements (beyond that, gain is slow with increasing grid size) wflimit: use max range for wfs such that at R(rmax)<wflimit*max(R(r)) """ if R1 < 1e-3: raise AssertionError("For stability; use R1>~1E-3") self.timer.start("calculate tables") self.wf_range = self.get_range(wflimit) Rgrid = np.linspace(R1, R2, N) self.N = N self.Rgrid = Rgrid self.dH = 0.0 self.Hmax = 0.0 if self.nel == 1: self.tables = [np.zeros((N, 20))] else: self.tables = [np.zeros((N, 20)), np.zeros((N, 20))] print >> self.txt, "Start making table..." for Ri, R in enumerate(Rgrid): if R > 2 * self.wf_range: break grid, areas = self.make_grid(R, nt=ntheta, nr=nr) if Ri == N - 1 or N / 10 == 0 or np.mod(Ri, N / 10) == 0: print >> self.txt, "R=%8.2f, %i grid points ..." % (R, len(grid)) for p, (e1, e2) in enumerate(self.pairs): selected = select_integrals(e1, e2) if Ri == 0: print >> self.txt, "R=%8.2f %s-%s, %i grid points, " % ( R, e1.get_symbol(), e2.get_symbol(), len(grid), ), print >> self.txt, "integrals:", for s in selected: print >> self.txt, s[0], print >> self.txt S, H, H2 = self.calculate_mels(selected, e1, e2, R, grid, areas) self.Hmax = max(self.Hmax, max(abs(H))) self.dH = max(self.dH, max(abs(H - H2))) self.tables[p][Ri, :10] = H self.tables[p][Ri, 10:] = S print >> self.txt, "Maximum value for H=%.2g" % self.Hmax print >> self.txt, "Maximum error for H=%.2g" % self.dH print >> self.txt, " Relative error=%.2g %%" % (self.dH / self.Hmax * 100) self.timer.stop("calculate tables") self.comment += "\n" + asctime() self.txt.flush() def calculate_mels(self, selected, e1, e2, R, grid, area): """ Perform integration for selected H and S integrals. parameters: ----------- selected: list of [('dds','3d','4d'),(...)] e1: <bra| element e2: |ket> element R: e1 is at origin, e2 at z=R grid: list of grid points on (d,z)-plane area: d-z areas of the grid points. return: ------- List of H,S and H2 for selected integrals. H2 is calculated using different technique and can be used for error estimation. S: simply R1*R2*angle-part H: operate (derivate) R2 <R1|t+Veff1+Veff2-Conf1-Conf2|R2> H2: operate with full h2 and hence use eigenvalue of |R2> with full Veff2 <R1|(t1+Veff1)+Veff2-Conf1-Conf2|R2> = <R1|h1+Veff2-Conf1-Conf2|R2> (operate with h1 on left) = <R1|e1+Veff2-Conf1-Conf2|R2> = e1*S + <R1|Veff2-Conf1-Conf2|R2> -> H and H2 can be compared and error estimated """ self.timer.start("calculate_mels") Sl, Hl, H2l = np.zeros(10), np.zeros(10), np.zeros(10) # common for all integrals (not wf-dependent parts) self.timer.start("prelude") N = len(grid) gphi, radii, v1, v2 = zeros((N, 10)), zeros((N, 2)), zeros(N), zeros(N) for i, (d, z) in enumerate(grid): r1, r2 = sqrt(d ** 2 + z ** 2), sqrt(d ** 2 + (R - z) ** 2) t1, t2 = arccos(z / r1), arccos((z - R) / r2) radii[i, :] = [r1, r2] gphi[i, :] = g(t1, t2) v1[i] = e1.effective_potential(r1) - e1.confinement_potential(r1) v2[i] = e2.effective_potential(r2) - e2.confinement_potential(r2) self.timer.stop("prelude") # calculate all selected integrals for integral, nl1, nl2 in selected: index = integrals.index(integral) S, H, H2 = 0.0, 0.0, 0.0 l2 = angular_momentum[nl2[1]] for i, dA in enumerate(area): r1, r2 = radii[i, :] d, z = grid[i] aux = gphi[i, index] * dA * d Rnl1, Rnl2, ddunl2 = e1.Rnl(r1, nl1), e2.Rnl(r2, nl2), e2.unl(r2, nl2, der=2) S += Rnl1 * Rnl2 * aux H += Rnl1 * (-0.5 * ddunl2 / r2 + (v1[i] + v2[i] + l2 * (l2 + 1) / (2 * r2 ** 2)) * Rnl2) * aux H2 += Rnl1 * Rnl2 * aux * (v2[i] - e1.confinement_potential(r1)) H2 += e1.get_epsilon(nl1) * S Sl[index] = S Hl[index] = H H2l[index] = H2 self.timer.stop("calculate_mels") return Sl, Hl, H2l def make_grid(self, Rz, nt, nr, p=2, q=2, view=False): """ Construct a double-polar grid. Parameters: ----------- Rz: element 1 is at origin, element 2 at z=Rz nt: number of theta grid points nr: number of radial grid points p: power describing the angular distribution of grid points (larger puts more weight towards theta=0) q: power describing the radial disribution of grid points (larger puts more weight towards centers) view: view the distribution of grid points with pylab. Plane at R/2 divides two polar grids. ^ (z-axis) |--------_____ phi_j | / ----__ * | / \ / * | / \ / X * X=coordinates of the center of area element(z,d), | / \ \-----* phi_(j+1) area=(r_(i+1)**2-r_i**2)*(phi_(j+1)-phi_j)/2 | / \ r_i r_(i+1) | / \ | / | *2------------------------| polar centered on atom 2 | \ | | \ / 1 | \ / / \ |-------------------------- z=h -line ordering of sector slice / \ | / \ points: / \ | / \ / \ | / | / 0 4 *1------------------------|---> polar centered on atom 1 2 / | \ | (r_perpendicular (xy-plane) = 'd-axis') \ / | \ / \ / | \ / 3 | \ / | \ / | \ / | \ ___ --- |--------- """ self.timer.start("make grid") rmin, rmax = (1e-7, self.wf_range) max_range = self.wf_range h = Rz / 2 T = np.linspace(0, 1, nt) ** p * np.pi R = rmin + np.linspace(0, 1, nr) ** q * (rmax - rmin) grid = [] area = [] # first calculate grid for polar centered on atom 1: # the z=h-like starts cutting full elements starting from point (1) for j in xrange(nt - 1): for i in xrange(nr - 1): # corners of area element d1, z1 = R[i + 1] * sin(T[j]), R[i + 1] * cos(T[j]) d2, z2 = R[i] * sin(T[j]), R[i] * cos(T[j]) d3, z3 = R[i] * sin(T[j + 1]), R[i] * cos(T[j + 1]) d4, z4 = R[i + 1] * sin(T[j + 1]), R[i + 1] * cos(T[j + 1]) A0 = (R[i + 1] ** 2 - R[i] ** 2) * (T[j + 1] - T[j]) / 2 if z1 <= h: # area fully inside region r0 = 0.5 * (R[i] + R[i + 1]) t0 = 0.5 * (T[j] + T[j + 1]) A = A0 elif z1 > h and z2 <= h and z4 <= h: # corner 1 outside region Th = np.arccos(h / R[i + 1]) r0 = 0.5 * (R[i] + R[i + 1]) t0 = 0.5 * (Th + T[j + 1]) A = A0 A -= 0.5 * R[i + 1] ** 2 * (Th - T[j]) - 0.5 * h ** 2 * (tan(Th) - tan(T[j])) elif z1 > h and z2 > h and z3 <= h and z4 <= h: # corners 1 and 2 outside region Th1 = np.arccos(h / R[i]) Th2 = np.arccos(h / R[i + 1]) r0 = 0.5 * (R[i] + R[i + 1]) t0 = 0.5 * (Th2 + T[j + 1]) A = A0 A -= A0 * (Th1 - T[j]) / (T[j + 1] - T[j]) A -= 0.5 * R[i + 1] ** 2 * (Th2 - Th1) - 0.5 * h ** 2 * (tan(Th2) - tan(Th1)) elif z1 > h and z2 > h and z4 > h and z3 <= h: # only corner 3 inside region Th = np.arccos(h / R[i]) r0 = 0.5 * (R[i] + h / cos(T[j + 1])) t0 = 0.5 * (Th + T[j + 1]) A = 0.5 * h ** 2 * (tan(T[j + 1]) - tan(Th)) - 0.5 * R[i] ** 2 * (T[j + 1] - Th) elif z1 > h and z4 > h and z2 <= h and z3 <= h: # corners 1 and 4 outside region r0 = 0.5 * (R[i] + h / cos(T[j + 1])) t0 = 0.5 * (T[j] + T[j + 1]) A = 0.5 * h ** 2 * (tan(T[j + 1]) - tan(T[j])) - 0.5 * R[i] ** 2 * (T[j + 1] - T[j]) elif z3 > h: A = -1 else: raise RuntimeError("Illegal coordinates.") d, z = (r0 * sin(t0), r0 * cos(t0)) if A > 0 and sqrt(d ** 2 + z ** 2) < max_range and sqrt(d ** 2 + (Rz - z) ** 2) < max_range: grid.append([d, z]) area.append(A) self.timer.start("symmetrize") # calculate the polar centered on atom 2 by mirroring the other grid grid = np.array(grid) area = np.array(area) grid2 = grid.copy() grid2[:, 1] = -grid[:, 1] shift = np.zeros_like(grid) shift[:, 1] = 2 * h grid = np.concatenate((grid, grid2 + shift)) area = np.concatenate((area, area)) self.timer.stop("symmetrize") if view: import pylab as pl pl.plot([h, h, h]) pl.scatter(grid[:, 0], grid[:, 1], s=10 * area / max(area)) pl.show() self.timer.stop("make grid") return grid, area
class KSAllElectron: def __init__(self,symbol, configuration={}, valence=[], confinement=None, xc='PW92', convergence={'density':1E-7,'energies':1E-7}, scalarrel=False, rmax=100.0, nodegpts=500, mix=0.2, itmax=200, timing=False, verbose=False, txt=None, restart=None, write=None): """ Make Kohn-Sham all-electron calculation for given atom. Examples: --------- atom=KSAllElectron('C') atom=KSAllElectron('C',confinement={'mode':'quadratic','r0':1.234}) atom.run() Parameters: ----------- symbol: chemical symbol configuration: e.g. {'2s':2,'2p':2}. Overrides (for orbitals given in dict) default configuration from box.data. valence: valence orbitals, e.g. ['2s','2p']. Overrides default valence from box.data. confinement: additional confining potential (see ConfinementPotential class) etol: sp energy tolerance for eigensolver (Hartree) convergence: convergence criterion dictionary * density: max change for integrated |n_old-n_new| * energies: max change in single-particle energy (Hartree) scalarrel: Use scalar relativistic corrections rmax: radial cutoff nodegpts: total number of grid points is nodegpts times the max number of antinodes for all orbitals mix: effective potential mixing constant itmax: maximum number of iterations for self-consistency. timing: output of timing summary verbose: increase verbosity during iterations txt: output file name for log data write: filename: save rgrid, effective potential and density to a file for further calculations. restart: filename: make an initial guess for effective potential and density from another calculation. """ self.symbol=symbol self.valence=valence self.confinement=confinement self.xc=xc self.convergence=convergence self.scalarrel = scalarrel self.set_output(txt) self.itmax=itmax self.verbose=verbose self.nodegpts=nodegpts self.mix=mix self.timing=timing self.timer=Timer('KSAllElectron',txt=self.txt,enabled=self.timing) self.timer.start('init') self.restart = restart self.write = write # element data self.data=copy( data[self.symbol] ) self.Z=self.data['Z'] if self.valence == []: self.valence = copy( data[self.symbol]['valence_orbitals'] ) # ... more specific self.occu = copy( data[self.symbol]['configuration'] ) nel_neutral = self.Z assert sum(self.occu.values()) == nel_neutral self.occu.update( configuration ) self.nel=sum(self.occu.values()) self.charge=nel_neutral-self.nel if self.confinement==None: self.confinement_potential=ConfinementPotential('none') else: self.confinement_potential=ConfinementPotential(**self.confinement) self.conf=None self.nucl=None self.exc=None if self.xc=='PW92': self.xcf=XC_PW92() else: raise NotImplementedError('Not implemented xc functional: %s' %xc) # technical stuff self.maxl=9 self.maxn=9 self.plotr={} self.unlg={} self.Rnlg={} self.unl_fct={} self.Rnl_fct={} self.veff_fct=None self.total_energy=0.0 maxnodes=max( [n-l-1 for n,l,nl in self.list_states()] ) self.rmin, self.rmax, self.N=( 1E-2/self.Z, rmax, (maxnodes+1)*self.nodegpts ) if self.scalarrel: print >> self.txt, 'Using scalar relativistic corrections.' print>>self.txt, 'max %i nodes, %i grid points' %(maxnodes,self.N) self.xgrid=np.linspace(0,np.log(self.rmax/self.rmin),self.N) self.rgrid=self.rmin*np.exp(self.xgrid) self.grid=RadialGrid(self.rgrid) self.timer.stop('init') print>>self.txt, self.get_comment() self.solved=False def __getstate__(self): """ Return dictionary of all pickable items. """ d=self.__dict__.copy() for key in self.__dict__: if callable(d[key]): d.pop(key) d.pop('out') return d def set_output(self,txt): """ Set output channel and give greetings. """ if txt == '-': self.txt = open(os.devnull,'w') elif txt==None: self.txt=sys.stdout else: self.txt=open(txt,'a') print>>self.txt, '*******************************************' print>>self.txt, 'Kohn-Sham all-electron calculation for %2s ' %self.symbol print>>self.txt, '*******************************************' def calculate_energies(self,echo=False): """ Calculate energy contributions. """ self.timer.start('energies') self.bs_energy=0.0 for n,l,nl in self.list_states(): self.bs_energy+=self.occu[nl]*self.enl[nl] self.exc=array([self.xcf.exc(self.dens[i]) for i in xrange(self.N)]) self.Hartree_energy=self.grid.integrate(self.Hartree*self.dens,use_dV=True)/2 self.vxc_energy=self.grid.integrate(self.vxc*self.dens,use_dV=True) self.exc_energy=self.grid.integrate(self.exc*self.dens,use_dV=True) self.confinement_energy=self.grid.integrate(self.conf*self.dens,use_dV=True) self.total_energy=self.bs_energy-self.Hartree_energy-self.vxc_energy+self.exc_energy if echo: print>>self.txt, '\n\nEnergetics:' print>>self.txt, '-------------' print>>self.txt, '\nsingle-particle energies' print>>self.txt, '------------------------' for n,l,nl in self.list_states(): print>>self.txt, '%s, energy %.15f' %(nl,self.enl[nl]) print>>self.txt, '\nvalence orbital energies' print>>self.txt, '--------------------------' for nl in data[self.symbol]['valence_orbitals']: print>>self.txt, '%s, energy %.15f' %(nl,self.enl[nl]) print>>self.txt, '\n' print>>self.txt, 'total energies:' print>>self.txt, '---------------' print>>self.txt, 'sum of eigenvalues: %.15f' %self.bs_energy print>>self.txt, 'Hartree energy: %.15f' %self.Hartree_energy print>>self.txt, 'vxc correction: %.15f' %self.vxc_energy print>>self.txt, 'exchange + corr energy: %.15f' %self.exc_energy print>>self.txt, '----------------------------' print>>self.txt, 'total energy: %.15f\n\n' %self.total_energy self.timer.stop('energies') def calculate_density(self): """ Calculate the radial electron density.; sum_nl |Rnl(r)|**2/(4*pi) """ self.timer.start('density') dens=np.zeros_like(self.rgrid) for n,l,nl in self.list_states(): dens+=self.occu[nl]*self.unlg[nl]**2 nel=self.grid.integrate(dens) if abs(nel-self.nel)>1E-10: raise RuntimeError('Integrated density %.3g, number of electrons %.3g' %(nel,self.nel) ) dens=dens/(4*np.pi*self.rgrid**2) self.timer.stop('density') return dens def calculate_Hartree_potential(self): """ Calculate Hartree potential. Everything is very sensitive to the way this is calculated. If you can think of how to improve this, please tell me! """ self.timer.start('Hartree') dV=self.grid.get_dvolumes() r, r0=self.rgrid, self.grid.get_r0grid() N=self.N n0=0.5*(self.dens[1:]+self.dens[:-1]) n0*=self.nel/sum(n0*dV) lo, hi, Hartree=np.zeros(N), np.zeros(N), np.zeros(N) lo[0]=0.0 for i in range(1,N): lo[i] = lo[i-1] + dV[i-1]*n0[i-1] hi[-1]=0.0 for i in range(N-2,-1,-1): hi[i] = hi[i+1] + n0[i]*dV[i]/r0[i] for i in range(N): Hartree[i] = lo[i]/r[i] + hi[i] self.Hartree=Hartree self.timer.stop('Hartree') def V_nuclear(self,r): return -self.Z/r def calculate_veff(self): """ Calculate effective potential. """ self.timer.start('veff') self.vxc=array([self.xcf.vxc(self.dens[i]) for i in xrange(self.N)]) self.timer.stop('veff') return self.nucl + self.Hartree + self.vxc + self.conf def guess_density(self): """ Guess initial density. """ r2=0.02*self.Z # radius at which density has dropped to half; improve this! dens=np.exp( -self.rgrid/(r2/np.log(2)) ) dens=dens/self.grid.integrate(dens,use_dV=True)*self.nel #pl.plot(self.rgrid,dens) return dens def get_veff_and_dens(self): """ Construct effective potential and electron density. If restart file is given, try to read from there, otherwise make a guess. """ done = False if self.restart is not None: # use density and effective potential from another calculation try: from scipy.interpolate import splrep, splev f = open(self.restart) rgrid = pickle.load(f) veff = pickle.load(f) dens = pickle.load(f) v = splrep(rgrid, veff) d = splrep(rgrid, dens) self.veff = array([splev(r,v) for r in self.rgrid]) self.dens = array([splev(r,d) for r in self.rgrid]) f.close() done = True except IOError: print >> self.txt, "Could not open restart file, " \ "starting from scratch." if not done: self.veff=self.nucl+self.conf self.dens=self.guess_density() def run(self): """ Solve the self-consistent potential. """ self.timer.start('solve ground state') print>>self.txt, '\nStart iteration...' self.enl={} self.d_enl={} for n,l,nl in self.list_states(): self.enl[nl]=0.0 self.d_enl[nl]=0.0 N=self.grid.get_N() # make confinement and nuclear potentials; intitial guess for veff self.conf=array([self.confinement_potential(r) for r in self.rgrid]) self.nucl=array([self.V_nuclear(r) for r in self.rgrid]) self.get_veff_and_dens() self.calculate_Hartree_potential() #self.Hartree=np.zeros((N,)) for it in range(self.itmax): self.veff=self.mix*self.calculate_veff()+(1-self.mix)*self.veff if self.scalarrel: veff = SplineFunction(self.rgrid, self.veff) self.dveff = array([veff(r, der=1) for r in self.rgrid]) d_enl_max, itmax=self.solve_eigenstates(it) dens0=self.dens.copy() self.dens=self.calculate_density() diff=self.grid.integrate(np.abs(self.dens-dens0),use_dV=True) if diff<self.convergence['density'] and d_enl_max<self.convergence['energies'] and it > 5: break self.calculate_Hartree_potential() if np.mod(it,10)==0: print>>self.txt, 'iter %3i, dn=%.1e>%.1e, max %i sp-iter' %(it,diff,self.convergence['density'],itmax) if it==self.itmax-1: if self.timing: self.timer.summary() raise RuntimeError('Density not converged in %i iterations' %(it+1)) self.txt.flush() self.calculate_energies(echo=True) print>>self.txt, 'converged in %i iterations' %it print>>self.txt, '%9.4f electrons, should be %9.4f' %(self.grid.integrate(self.dens,use_dV=True),self.nel) for n,l,nl in self.list_states(): self.Rnl_fct[nl]=Function('spline',self.rgrid,self.Rnlg[nl]) self.unl_fct[nl]=Function('spline',self.rgrid,self.unlg[nl]) self.timer.stop('solve ground state') self.timer.summary() self.txt.flush() self.solved=True if self.write != None: f=open(self.write,'w') pickle.dump(self.rgrid, f) pickle.dump(self.veff, f) pickle.dump(self.dens, f) f.close() def solve_eigenstates(self,iteration,itmax=100): """ Solve the eigenstates for given effective potential. u''(r) - 2*(v_eff(r)+l*(l+1)/(2r**2)-e)*u(r)=0 ( u''(r) + c0(r)*u(r) = 0 ) r=r0*exp(x) --> (to get equally spaced integration mesh) u''(x) - u'(x) + c0(x(r))*u(r) = 0 """ self.timer.start('eigenstates') rgrid=self.rgrid xgrid=self.xgrid dx=xgrid[1]-xgrid[0] N=self.N c2=np.ones(N) c1=-np.ones(N) d_enl_max=0.0 itmax=0 for n,l,nl in self.list_states(): nodes_nl=n-l-1 if iteration==0: eps=-1.0*self.Z**2/n**2 else: eps=self.enl[nl] if iteration<=3: delta=0.5*self.Z**2/n**2 #previous!!!!!!!!!! else: delta=self.d_enl[nl] direction='none' epsmax=self.veff[-1]-l*(l+1)/(2*self.rgrid[-1]**2) it=0 u=np.zeros(N) hist=[] while True: eps0=eps c0, c1, c2 = self.construct_coefficients(l, eps) # boundary conditions for integration from analytic behaviour (unscaled) # u(r)~r**(l+1) r->0 # u(r)~exp( -sqrt(c0(r)) ) (set u[-1]=1 and use expansion to avoid overflows) u[0:2]=rgrid[0:2]**(l+1) if not(c0[-2]<0 and c0[-1]<0): pl.plot(c0) pl.show() assert c0[-2]<0 and c0[-1]<0 u, nodes, A, ctp=shoot(u,dx,c2,c1,c0,N) it+=1 norm=self.grid.integrate(u**2) u=u/sqrt(norm) if nodes>nodes_nl: # decrease energy if direction=='up': delta/=2 eps-=delta direction='down' elif nodes<nodes_nl: # increase energy if direction=='down': delta/=2 eps+=delta direction='up' elif nodes==nodes_nl: shift=-0.5*A/(rgrid[ctp]*norm) if abs(shift)<1E-8: #convergence break if shift>0: direction='up' elif shift<0: direction='down' eps+=shift if eps>epsmax: eps=0.5*(epsmax+eps0) hist.append(eps) if it>100: print>>self.txt, 'Epsilon history for %s' %nl for h in hist: print h print>>self.txt, 'nl=%s, eps=%f' %(nl,eps) print>>self.txt, 'max epsilon',epsmax raise RuntimeError('Eigensolver out of iterations. Atom not stable?') itmax=max(it,itmax) self.unlg[nl]=u self.Rnlg[nl]=self.unlg[nl]/self.rgrid self.d_enl[nl]=abs(eps-self.enl[nl]) d_enl_max=max(d_enl_max,self.d_enl[nl]) self.enl[nl]=eps if self.verbose: print>>self.txt, '-- state %s, %i eigensolver iterations, e=%9.5f, de=%9.5f' %(nl,it,self.enl[nl],self.d_enl[nl]) assert nodes==nodes_nl assert u[1]>0.0 self.timer.stop('eigenstates') return d_enl_max, itmax def construct_coefficients(self, l, eps): c = 137.036 c2 = np.ones(self.N) if self.scalarrel == False: c0 = -2*( 0.5*l*(l+1)+self.rgrid**2*(self.veff-eps) ) c1 = -np.ones(self.N) else: # from Paolo Giannozzi: Notes on pseudopotential generation ScR_mass = array([1 + 0.5*(eps-V)/c**2 for V in self.veff]) c0 = -l*(l+1) - 2*ScR_mass*self.rgrid**2*(self.veff-eps) - self.dveff*self.rgrid/(2*ScR_mass*c**2) c1 = self.rgrid*self.dveff/(2*ScR_mass*c**2) - 1 return c0, c1, c2 def plot_Rnl(self,filename=None): """ Plot radial wave functions with matplotlib. filename: output file name + extension (extension used in matplotlib) """ if pl==None: raise AssertionError('pylab could not be imported') rmax = data[self.symbol]['R_cov']/0.529177*3 ri = np.where( self.rgrid<rmax )[0][-1] states=len(self.list_states()) p = np.ceil(np.sqrt(states)) #p**2>=states subplots fig=pl.figure() i=1 # as a function of grid points for n,l,nl in self.list_states(): ax=pl.subplot(2*p,p,i) pl.plot(self.Rnlg[nl]) pl.yticks([],[]) pl.xticks(size=5) # annotate c = 'k' if nl in self.valence: c='r' pl.text(0.5,0.4,r'$R_{%s}(r)$' %nl,transform=ax.transAxes,size=15,color=c) if ax.is_first_col(): pl.ylabel(r'$R_{nl}(r)$',size=8) i+=1 # as a function of radius i = p**2+1 for n,l,nl in self.list_states(): ax=pl.subplot(2*p,p,i) pl.plot(self.rgrid[:ri],self.Rnlg[nl][:ri]) pl.yticks([],[]) pl.xticks(size=5) if ax.is_last_row(): pl.xlabel('r (Bohr)',size=8) c = 'k' if nl in self.valence: c='r' pl.text(0.5,0.4,r'$R_{%s}(r)$' %nl,transform=ax.transAxes,size=15,color=c) if ax.is_first_col(): pl.ylabel(r'$R_{nl}(r)$',size=8) i+=1 file = '%s_KSAllElectron.pdf' %self.symbol #pl.rc('figure.subplot',wspace=0.0,hspace=0.0) fig.subplots_adjust(hspace=0.2,wspace=0.1) s='' if self.confinement!=None: s='(confined)' pl.figtext(0.4,0.95,r'$R_{nl}(r)$ for %s-%s %s' %(self.symbol,self.symbol,s)) if filename is not None: file = filename pl.savefig(file) def get_wf_range(self,nl,fractional_limit=1E-7): """ Return the maximum r for which |R(r)|<fractional_limit*max(|R(r)|) """ wfmax=max(abs(self.Rnlg[nl])) for r,wf in zip(self.rgrid[-1::-1],self.Rnlg[nl][-1::-1]): if abs(wf)>fractional_limit*wfmax: return r def list_states(self): """ List all potential states {(n,l,'nl')}. """ states=[] for l in range(self.maxl+1): for n in range(1,self.maxn+1): nl=orbit_transform((n,l),string=True) if nl in self.occu: states.append((n,l,nl)) return states def get_energy(self): return self.total_energy def get_epsilon(self,nl): """ get_eigenvalue('2p') or get_eigenvalue((2,1)) """ nls=orbit_transform(nl,string=True) if not self.solved: raise AssertionError('run calculations first.') return self.enl[nls] def effective_potential(self,r,der=0): """ Return effective potential at r or its derivatives. """ if self.veff_fct==None: self.veff_fct=Function('spline',self.rgrid,self.veff) return self.veff_fct(r,der=der) def get_radial_density(self): return self.rgrid,self.dens def Rnl(self,r,nl,der=0): """ Rnl(r,'2p') or Rnl(r,(2,1))""" nls=orbit_transform(nl,string=True) return self.Rnl_fct[nls](r,der=der) def unl(self,r,nl,der=0): """ unl(r,'2p')=Rnl(r,'2p')/r or unl(r,(2,1))...""" nls=orbit_transform(nl,string=True) return self.unl_fct[nls](r,der=der) def get_valence_orbitals(self): """ Get list of valence orbitals, e.g. ['2s','2p'] """ return self.valence def get_symbol(self): """ Return atom's chemical symbol. """ return self.symbol def get_comment(self): """ One-line comment, e.g. 'H, charge=0, quadratic, r0=4' """ comment='%s xc=%s charge=%.1f conf:%s' %(self.symbol,self.xc,float(self.charge),self.confinement_potential.get_comment()) return comment def get_valence_energies(self): """ Return list of valence energies, e.g. ['2s','2p'] --> [-39.2134,-36.9412] """ if not self.solved: raise AssertionError('run calculations first.') return [(nl,self.enl[nl]) for nl in self.valence] def write_unl(self,filename,only_valence=True,step=20): """ Append functions unl=Rnl*r, V_effective, V_confinement into file. Only valence functions by default. Parameters: ----------- filename: output file name (e.g. XX.elm) only_valence: output of only valence orbitals step: step size for output grid """ if not self.solved: raise AssertionError('run calculations first.') if only_valence: orbitals=self.valence else: orbitals=[nl for n,l,nl in self.list_states()] o=open(filename,'a') for nl in orbitals: print>>o, '\n\nu_%s=' %nl for r,u in zip(self.rgrid[::step],self.unlg[nl][::step]): print>>o, r,u print>>o,'\n\nv_effective=' for r,ve in zip(self.rgrid[::step],self.veff[::step]): print>>o, r,ve print>>o,'\n\nconfinement=' for r,vc in zip(self.rgrid[::step],self.conf[::step]): print>>o, r,vc print>>o,'\n\n'