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 Hotbit(Output): def __init__(self,parameters=None, elements=None, tables=None, verbose=False, charge=0.0, SCC=True, kpts=(1,1,1), rs='kappa', physical_k=True, maxiter=50, gamma_cut=None, txt=None, verbose_SCC=False, width=0.02, mixer=None, coulomb_solver=None, charge_density='Gaussian', vdw=False, vdw_parameters=None, internal={}): """ Hotbit -- density-functional tight-binding calculator for atomic simulation environment (ASE). Parameters: ----------- parameters: The directory for parametrization files. * If parameters==None, use HOTBIT_PARAMETERS environment variable. * Parametrizations given by 'elements' and 'tables' keywords override parametrizations in this directory. elements: Files for element data (*.elm). example: {'H':'H_custom.elm','C':'/../C.elm'} * If extension '.elm' is omitted, it is assumed. * Items can also be elements directly: {'H':H} (H is type Element) * If elements==None, use element info from default directory. * If elements['rest']=='default', use default parameters for all other elements than the ones specified. E.g. {'H':'H.elm','rest':'default'} (otherwise all elements present have to be specified explicitly). tables: Files for Slater-Koster tables. example: {'CH':'C_H.par','CC':'C_C.par'} * If extension '.par' is omitted, it is assumed. * If tables==None, use default interactions. * If tables['rest']='default', use default parameters for all other interactions, e.g. {'CH':'C_H.par','rest':'default'} * If tables['AB']==None, ignore interactions for A and B (both chemical and repulsive) mixer: Density mixer. example: {'name':'Anderson','mixing_constant':0.2, 'memory':5}. charge: Total charge for system (-1 means an additional electron) width: Width of Fermi occupation (eV) SCC: Self-Consistent Charge calculation * True for SCC-DFTB, False for DFTB kpts: Number of k-points. * For translational symmetry points are along the directions given by the cell vectors. * For general symmetries, you need to look at the info from the container used rs: * 'kappa': use kappa-points * 'k': use normal k-points. Only for Bravais lattices. physical_k Use physical (realistic) k-points for generally periodic systems. * Ignored with normal translational symmetry * True for physically allowed k-points in periodic symmetries. maxiter: Maximum number of self-consistent iterations * only for SCC-DFTB coulomb_solver: The Coulomb solver object. If None, a DirectCoulomb object will the automatically instantiated. * only for SCC-DFTB charge_density: Shape of the excess charge on each atom. Possibilities are: * 'Gaussian': Use atom centered Gaussians. This is the default. * 'Slater': Slater-type exponentials as used in the original SCC-DFTB scheme. * only for SCC-DFTB gamma_cut: Range for Coulomb interaction if direct summation is selected (coulomb_solver = None). * only for SCC-DFTB vdw: Include van der Waals interactions vdw_parameters: Dictionary containing the parameters for the van-der-Waals interaction for each element. i.e. { el: ( p, R0 ), ... } where *el* is the element name, *p* the polarizability and *R0* the radius where the van-der-Waals interaction starts. Will override whatever read from .elm files. txt: Filename for log-file. * None: standard output * '-': throw output to trash (/null) verbose_SCC: Increase verbosity in SCC iterations. internal: Dictionary for internal variables, some of which are set for stability purposes, some for quick and dirty bug fixes. Use these with caution! (For this reason, for the description of these variables you are forced to look at the source code.) """ from copy import copy import os if gamma_cut!=None: gamma_cut=gamma_cut/Bohr self.__dict__={ 'parameters':parameters, 'elements':elements, 'tables':tables, 'verbose':verbose, 'charge':charge, 'width':width/Hartree, 'SCC':SCC, 'kpts':kpts, 'rs':rs, 'physical_k':physical_k, 'maxiter':maxiter, 'gamma_cut':gamma_cut, 'vdw':vdw, 'vdw_parameters':vdw_parameters, 'txt':txt, 'verbose_SCC':verbose_SCC, 'mixer':mixer, 'coulomb_solver':coulomb_solver, 'charge_density':charge_density, 'internal':internal} if parameters!=None: os.environ['HOTBIT_PARAMETERS']=parameters self.init=False self.notes=[] self.dry_run = '--dry-run' in sys.argv internal0 = {'sepsilon':0., # add this to the diagonal of S to avoid LAPACK error in diagonalization 'tol_imaginary_e': 1E-13, # tolerance for imaginary band energy 'tol_mulliken':1E-5, # tolerance for mulliken charge sum deviation from integer 'tol_eigenvector_norm':1E-6, # tolerance for eigenvector norm for eigensolver 'symop_range':5} # range for the number of symmetry operations in all symmetries internal0.update(internal) for key in internal0: self.set(key,internal0[key]) #self.set_text(self.txt) #self.timer=Timer('Hotbit',txt=self.get_output()) def __del__(self): """ Delete calculator -> timing summary. """ if self.get('SCC'): try: print(self.st.solver.get_iteration_info(), file=self.txt) self.txt.flush() except: pass if len(self.notes)>0: print('Notes and warnings:', file=self.txt) for note in self.notes: print(note, file=self.txt) if self.init: self.timer.summary() Output.__del__(self) def write_electronic_data(self,filename,keys=None): """ Write key electronic data into a file with *general* format. Hotbit is not needed to analyze the resulting data file. The data will be in a dictionary with the following items: N the number of atoms norb the number of orbitals nelectrons the number of electrons charge system charge epot potential energy ebs band structure energy ecoul coulomb energy erep repulsive energy forces atomic forces symbols element symbols e single-particle energies occ occupations nk number of k-points k k-point vectors wk k-point weights dq excess Mulliken populations gap energy gap gap_prob certainty of the gap determination above dose energies for density of states (all states over k-points as well) 0 = Fermi-level dos density of states (including k-point weights) Access to data, simply: data = numpy.load(filename) print data['epot'] parameters: ----------- filename: output file name keys: list of items (key names) to save. If None, save all. """ data = {} data['N'] = self.el.N data['norb'] = self.st.norb data['charge'] = self.get('charge') data['nelectrons'] = self.el.get_number_of_electrons() data['erep'] = self.rep.get_repulsive_energy() data['ecoul'] = self.get_coulomb_energy(self.el.atoms) data['ebs'] = self.get_band_structure_energy(self.el.atoms) data['epot'] = self.get_potential_energy(self.el.atoms) data['forces'] = self.get_forces(self.el.atoms) data['symbols'] = self.el.symbols data['e'] = self.st.e data['occ'] = self.st.f data['nk'] = self.st.nk data['k'] = self.st.k data['wk'] = self.st.wk data['dq'] = self.st.mulliken() data['gap'], data['gap_prob'] = self.get_energy_gap() data['dose'], data['dos'] = self.get_density_of_states(False) for key in list(data.keys()): if keys!=None and key not in keys: del data[key] import pickle f = open(filename, 'w') pickle.dump(data,f) f.close() def set(self,key,value): if key == 'txt': self.set_text(value) elif self.init==True and key not in ['charge']: raise AssertionError('Parameters cannot be set after initialization.') else: self.__dict__[key]=value def get_atoms(self): """ Return the current atoms object. """ atoms = self.el.atoms.copy() atoms.set_calculator(self) return atoms def add_note(self,note): """ Add warning (etc) note to be printed in log file end. """ self.notes.append(note) def greetings(self): """ Simple greetings text """ from time import asctime from os import uname from os.path import abspath, curdir from os import environ self.version=hotbit_version print('\n\n\n\n\n', file=self.txt) print(' _ _ _ _ _', file=self.txt) print('| |__ ___ | |_ | |__ |_| |_', file=self.txt) print('| _ \ / _ \| _|| _ \| | _|', file=self.txt) print('| | | | ( ) | |_ | ( ) | | |_', file=self.txt) print('|_| |_|\___/ \__|\____/|_|\__| ver.',self.version, file=self.txt) print('Distributed under GNU GPL; see %s' %environ.get('HOTBIT_DIR')+'/LICENSE', file=self.txt) print('Date:',asctime(), file=self.txt) dat=uname() print('Nodename:',dat[1], file=self.txt) print('Arch:',dat[4], file=self.txt) print('Dir:',abspath(curdir), file=self.txt) print('System:',self.el.get_name(), file=self.txt) print(' Charge=%4.1f' % self.charge, file=self.txt) print(' Container', self.el.container_info(), file=self.txt) print('Symmetry operations (if any):', file=self.txt) rs = self.get('rs') kpts = self.get('kpts') M = self.el.get_number_of_transformations() for i in range(3): print(' %i: pbc=' %i, self.el.atoms.get_pbc()[i], end=' ', file=self.txt) if type(kpts)==type([]): print(', %s-points=%i, M=%.f' %(rs,len(kpts),M[i]), file=self.txt) else: print(', %s-points=%i, M=%.f' %(rs,kpts[i],M[i]), file=self.txt) print('Electronic temperature:', self.width*Hartree,'eV', file=self.txt) mixer = self.st.solver.mixer print('Mixer:', mixer.get('name'), 'with memory =', mixer.get('memory'), ', mixing parameter =', mixer.get('beta'), file=self.txt) print(self.el.greetings(), file=self.txt) print(self.ia.greetings(), file=self.txt) print(self.rep.greetings(), file=self.txt) if self.pp.exists(): print(self.pp.greetings(), file=self.txt) def out(self,text): print(text, file=self.txt) self.txt.flush() def set_text(self,txt): """ Set up the output file. """ if txt=='-' or txt=='null': self.txt = open('/dev/null','w') elif hasattr(txt, 'write'): self.txt = txt elif txt is None: from sys import stdout self.txt=stdout else: self.txt=open(txt,'a') # check if the output of timer must be changed also if 'timer' in self.__dict__: self.timer.txt = self.get_output() def get(self,arg=None): """ Get calculator input parameters. arg: 'kpts','width',... """ if arg==None: return self.__dict__ else: return self.__dict__[arg] def memory_estimate(self): """ Print an estimate for memory consumption in GB. If script run with --dry-run, exit. """ if self.st.nk>1: number = 16. #complex else: number = 8. #real M = self.st.nk*self.st.norb**2*number # H S dH0 dS wf H1 dH rho rhoe mem = M + M + 3*M + 3*M + M + M + 3*M + M + M print('Memory consumption estimate: > %.2f GB' %(mem/1E9), file=self.txt) self.txt.flush() if self.dry_run: raise SystemExit def solve_ground_state(self,atoms): """ If atoms moved, solve electronic structure. """ if not self.init: assert type(atoms)!=type(None) self._initialize(atoms) if type(atoms)==type(None): pass elif self.calculation_required(atoms,'ground state'): self.el.update_geometry(atoms) t0 = time() self.st.solve() self.el.set_solved('ground state') t1 = time() self.flags['Mulliken'] = False self.flags['DOS'] = False self.flags['bonds'] = False if self.verbose: print("Solved in %0.2f seconds" % (t1-t0), file=self.get_output()) #if self.get('SCC'): # atoms.set_charges(-self.st.get_dq()) else: pass def _initialize(self,atoms): """ Initialization of hotbit. """ if not self.init: self.set_text(self.txt) self.timer=Timer('Hotbit',txt=self.get_output()) self.start_timing('initialization') self.el=Elements(self,atoms) self.ia=Interactions(self) self.st=States(self) self.rep=Repulsion(self) self.pp=PairPotential(self) if self.get('vdw'): if self.get('vdw_parameters') is not None: self.el.update_vdw(self.get('vdw_parameters')) setup_vdw(self) self.env=Environment(self) pbc=atoms.get_pbc() # FIXME: gamma_cut -stuff #if self.get('SCC') and np.any(pbc) and self.get('gamma_cut')==None: # raise NotImplementedError('SCC not implemented for periodic systems yet (see parameter gamma_cut).') if np.any(pbc) and abs(self.get('charge'))>0.0 and self.get('SCC'): raise AssertionError('Charged system cannot be periodic.') self.flush() self.flags = {} self.flags['Mulliken'] = False self.flags['DOS'] = False self.flags['bonds'] = False self.flags['grid'] = False self.stop_timing('initialization') self.el.set_atoms(atoms) if not self.init: self.init=True self.greetings() def calculation_required(self,atoms,quantities): """ Check if a calculation is required. Check if the quantities in the quantities list have already been calculated for the atomic configuration atoms. The quantities can be one or more of: 'ground state', 'energy', 'forces', 'magmoms', and 'stress'. """ return self.el.calculation_required(atoms,quantities) def get_potential_energy(self,atoms,force_consistent=False): """ Return the potential energy of present system. """ if force_consistent: raise NotImplementedError if self.calculation_required(atoms,['energy']): self.solve_ground_state(atoms) self.start_timing('energy') ebs=self.get_band_structure_energy(atoms) ecoul=self.get_coulomb_energy(atoms) erep=self.rep.get_repulsive_energy() epp=self.pp.get_energy() self.epot = ebs + ecoul + erep + epp - self.el.efree*Hartree self.stop_timing('energy') self.el.set_solved('energy') return self.epot.copy() def get_forces(self,atoms): """ Return forces (in eV/Angstrom) Ftot = F(band structure) + F(coulomb) + F(repulsion). """ if self.calculation_required(atoms,['forces']): self.solve_ground_state(atoms) self.start_timing('forces') fbs=self.st.get_band_structure_forces() frep=self.rep.get_repulsive_forces() fcoul=self.st.es.gamma_forces() #zero for non-SCC fpp = self.pp.get_forces() self.stop_timing('forces') self.f = (fbs+frep+fcoul+fpp)*(Hartree/Bohr) self.el.set_solved('forces') return self.f.copy() def get_band_energies(self, kpts=None, shift=True, rs='kappa', h1=False): ''' Return band energies for explicitly given list of k-points. parameters: =========== kpts: list of k-points; e.g. kpts=[(0,0,0),(pi/2,0,0),(pi,0,0)] k- or kappa-points, depending on parameter rs. if None, return for all k-points in the calculation shift: shift zero to the Fermi-level rs: use 'kappa'- or 'k'-points in reciprocal space h1: Add Coulomb part to hamiltonian matrix. Required for consistent use of SCC. ''' if kpts is None: e = self.st.e * Hartree else: if rs=='k': klist = k_to_kappa_points(kpts,self.el.atoms) elif rs=='kappa': klist = kpts e = self.st.get_band_energies(klist,h1)*Hartree if shift: return e-self.get_fermi_level() else: return e def get_stress(self,atoms): self.solve_ground_state(atoms) # TODO: ASE needs an array from this method, would it be proper to # somehow inform that the stresses are not calculated? return np.zeros((6,)) def get_charge(self): """ Return system's total charge. """ return self.get('charge') def get_eigenvalues(self): """ Return eigenvalues without shifts. For alternative, look at method get_band_energies. """ return self.st.get_eigenvalues()*Hartree def get_energy_gap(self): """ Return the energy gap. (in eV) Gap is the energy difference between the first states above and below Fermi-level. Return also the probability of having returned the gap; it is the difference in the occupations of these states, divided by 2. """ eigs = (self.get_eigenvalues() - self.get_fermi_level()).flatten() occ = self.get_occupations().flatten() ehi, elo=1E10,-1E10 for e,f in zip(eigs,occ): if elo<e<=0.0: elo = e flo = f elif 0.0<e<ehi: ehi = e fhi = f return ehi-elo, (flo-fhi)/2 def get_state_indices(self, state): """ Return the k-point index and band index of given state. parameters: ----------- state: 'H**O', or 'LUMO' H**O is the first state below Fermi-level. LUMO is the first state above Fermi-level. """ eigs = (self.get_eigenvalues() - self.get_fermi_level()).flatten() if state=='H**O': k,a = np.unravel_index(np.ma.masked_array(eigs,eigs>0.0).argmax(),(self.st.nk,self.st.norb)) if state=='LUMO': k,a = np.unravel_index(np.ma.masked_array(eigs,eigs<0.0).argmin(),(self.st.nk,self.st.norb)) return k,a def get_occupations(self): #self.solve_ground_state(atoms) return self.st.get_occupations() def get_band_structure_energy(self,atoms): if self.calculation_required(atoms, ['ebs']): self.solve_ground_state(atoms) self.ebs = self.st.get_band_structure_energy()*Hartree self.el.set_solved('ebs') return self.ebs def get_coulomb_energy(self,atoms): if self.calculation_required(atoms,['ecoul']): self.solve_ground_state(atoms) self.ecoul = self.st.es.coulomb_energy()*Hartree self.st return self.ecoul # some not implemented ASE-assumed methods def get_fermi_level(self): """ Return the Fermi-energy (chemical potential) in eV. """ return self.st.occu.get_mu() * Hartree def set_atoms(self,atoms): """ Initialize the calculator for given atomic system. """ if self.init==True and atoms.get_chemical_symbols()!=self.el.atoms.get_chemical_symbols(): raise RuntimeError('Calculator initialized for %s. Create new calculator for %s.' %(self.el.get_name(),mix.parse_name_for_atoms(atoms))) else: self._initialize(atoms) def get_occupation_numbers(self,kpt=0): """ Return occupation numbers for given k-point index. """ return self.st.f[kpt].copy() def get_number_of_bands(self): """ Return the total number of orbitals. """ return self.st.norb def start_timing(self, label): self.timer.start(label) def stop_timing(self, label): self.timer.stop(label) # # various analysis methods # def get_dielectric_function(self,width=0.05,cutoff=None,N=400): """ Return the imaginary part of the dielectric function for non-SCC. Note: Uses approximation that requires that the orientation of neighboring unit cells does not change much. (Exact for Bravais lattice.) See, e.g., Marder, Condensed Matter Physics, or Popov New J. Phys 6, 17 (2004) parameters: ----------- width: energy broadening in eV cutoff: cutoff energy in eV N: number of points in energy grid return: ------- e[:], d[:,0:2] """ self.start_timing('dielectric function') width = width/Hartree otol = 0.05 # tolerance for occupations if cutoff==None: cutoff = 1E10 else: cutoff = cutoff/Hartree st = self.st nk, e, f, wk = st.nk, st.e, st.f, st.wk ex, wt = [], [] for k in range(nk): wf = st.wf[k] wfc = wf.conjugate() dS = st.dS[k].transpose((0,2,1)) ek = e[k] fk = f[k] kweight = wk[k] # electron excitation ka-->kb; restrict the search: bmin = list(fk<2-otol).index(True) amin = list(ek>ek[bmin]-cutoff).index(True) amax = list(fk<otol).index(True) for a in range(amin,amax+1): bmax = list(ek>ek[a]+cutoff).index(True) for b in range(max(a+1,bmin),bmax+1): de = ek[b]-ek[a] df = fk[a]-fk[b] if df<otol: continue # P = < ka | P | kb > P = 1j*hbar*np.dot(wfc[a],np.dot(dS,wf[b])) ex.append( de ) wt.append( kweight*df*np.abs(P)**2 ) ex, wt = np.array(ex), np.array(wt) cutoff = min( ex.max(),cutoff ) y = np.zeros((N,3)) for d in range(3): # Lorenzian should be used, but long tail would bring divergence at zero energy x,y[:,d] = broaden( ex,wt[:,d],width,'gaussian',N=N,a=width,b=cutoff ) y[:,d] = y[:,d]/x**2 const = (4*np.pi**2/hbar) self.stop_timing('dielectric function') return x*Hartree, y*const #y also in eV, Ang # # grid stuff # def set_grid(self,h=0.2,cutoff=3.0): if self.calculation_required(self.el.atoms,['energy']): raise AssertionError('Electronic structure is not solved yet!') if self.flags['grid']==False: self.gd = Grids(self,h,cutoff) self.flags['grid']=True def get_grid_basis_orbital(self,I,otype,k=0,pad=True): """ Return basis orbital on grid. parameters: =========== I: atom index otype: orbital type ('s','px','py',...) k: k-point index (basis functions are really the extended Bloch functions for periodic systems) pad: padded edges in the array """ if self.flags['grid']==False: raise AssertionError('Grid needs to be set first by method "set_grid".') return self.gd.get_grid_basis_orbital(I,otype,k,pad) def get_grid_wf(self,a,k=0,pad=True): """ Return eigenfunction on a grid. parameters: =========== a: state (band) index k: k-vector index pad: padded edges """ if self.flags['grid']==False: raise AssertionError('Grid needs to be set first by method "set_grid".') return self.gd.get_grid_wf(a,k,pad) def get_grid_wf_density(self,a,k=0,pad=True): """ Return eigenfunction density. Density is not normalized; accurate quantitative analysis on this density are best avoided. parameters: =========== a: state (band) index k: k-vector index pad: padded edges """ if self.flags['grid']==False: raise AssertionError('Grid needs to be set first by method "set_grid".') return self.gd.get_grid_wf_density(a,k,pad) def get_grid_density(self,pad=True): """ Return electron density on grid. Do not perform accurate analysis on this density. Integrated density differs from the total number of electrons. Bader analysis inaccurate. parameters: pad: padded edges """ if self.flags['grid']==False: raise AssertionError('Grid needs to be set first by method "set_grid".') return self.gd.get_grid_density(pad) def get_grid_LDOS(self,bias=None,window=None,pad=True): """ Return electron density over selected states around the Fermi-level. parameters: ----------- bias: bias voltage (eV) with respect to Fermi-level. Negative means probing occupied states. window: 2-tuple for lower and upper bounds wrt. Fermi-level pad: padded edges """ if self.flags['grid']==False: raise AssertionError('Grid needs to be set first by method "set_grid".') return self.gd.get_grid_LDOS(bias,window,pad) # # Mulliken population analysis tools # def _init_mulliken(self): """ Initialize Mulliken analysis. """ if self.calculation_required(self.el.atoms,['energy']): raise AssertionError('Electronic structure is not solved yet!') if self.flags['Mulliken']==False: self.MA = MullikenAnalysis(self) self.flags['Mulliken']=True def get_dq(self,atoms=None): """ Return atoms' excess Mulliken populations. The total populations subtracted by the numbers of valence electrons. """ self.solve_ground_state(atoms) return self.st.get_dq() def get_charges(self,atoms=None): """ Return atoms' electric charges (Mulliken). """ return -self.get_dq(atoms) def get_atom_mulliken(self,I): """ Return Mulliken population for atom I. This is the total population, without the number of valence electrons subtracted. parameters: =========== I: atom index """ self._init_mulliken() return self.MA.get_atom_mulliken(I) def get_basis_mulliken(self,mu): """ Return Mulliken population of given basis state. parameters: =========== mu: orbital index (see Elements' methods for indices) """ self._init_mulliken() return self.MA.get_basis_mulliken(mu) def get_basis_wf_mulliken(self,mu,k,a,wk=True): """ Return Mulliken population for given basis state and wavefunction. parameters: =========== mu: basis state index k: k-vector index a: eigenstate index wk: include k-point weight in the population? """ self._init_mulliken() return self.MA.get_basis_wf_mulliken(mu,k,a,wk) def get_atom_wf_mulliken(self,I,k,a,wk=True): """ Return Mulliken population for given atom and wavefunction. parameters: =========== I: atom index (if None, return an array for all atoms) k: k-vector index a: eigenstate index wk: embed k-point weight in population """ self._init_mulliken() return self.MA.get_atom_wf_mulliken(I,k,a,wk) def get_atom_wf_all_orbital_mulliken(self,I,k,a): """ Return orbitals' Mulliken populations for given atom and wavefunction. parameters: =========== I: atom index (returned array size = number of orbitals on I) k: k-vector index a: eigenstate index """ self._init_mulliken() return self.MA.get_atom_wf_all_orbital_mulliken(I,k,a) def get_atom_wf_all_angmom_mulliken(self,I,k,a,wk=True): """ Return atom's Mulliken populations for all angmom for given wavefunction. parameters: =========== I: atom index k: k-vector index a: eigenstate index wk: embed k-point weight into population return: array (length 3) containing s,p and d-populations """ self._init_mulliken() return self.MA.get_atom_wf_all_angmom_mulliken(I,k,a,wk) # # Densities of states methods # def _init_DOS(self): """ Initialize Density of states analysis. """ if self.calculation_required(self.el.atoms,['energy']): raise AssertionError('Electronic structure is not solved yet!') if self.flags['DOS']==False: self.DOS = DensityOfStates(self) self.flags['DOS']=True def get_local_density_of_states(self,projected=False,width=0.05,window=None,npts=501): """ Return state density for all atoms as a function of energy. parameters: =========== projected: return local density of states projected for angular momenta 0,1 and 2 (s,p and d) width: energy broadening (in eV) window: energy window around Fermi-energy; 2-tuple (eV) npts: number of grid points for energy return: projected==False: energy grid, ldos[atom,grid] projected==True: energy grid, ldos[atom, grid], pldos[atom, angmom, grid] """ self._init_DOS() return self.DOS.get_local_density_of_states(projected,width,window,npts) def get_density_of_states(self,broaden=False,projected=False,occu=False,width=0.05,window=None,npts=501): """ Return the full density of states. Sum of states over k-points. Zero is the Fermi-level. Spin-degeneracy is NOT counted. parameters: =========== broaden: * If True, return broadened DOS in regular grid in given energy window. * If False, return energies of all states, followed by their k-point weights. projected: project DOS for angular momenta occu: for not broadened case, return also state occupations width: Gaussian broadening (eV) window: energy window around Fermi-energy; 2-tuple (eV) npts: number of data points in output return: * if projected: e[:],dos[:],pdos[l,:] (angmom l=0,1,2) * if not projected: e[:],dos[:] * if broaden: e[:] is on regular grid, otherwise e[:] are eigenvalues and dos[...] corresponding weights * if occu: e[:],dos[:],occu[:] """ self._init_DOS() return self.DOS.get_density_of_states(broaden,projected,occu,width,window,npts) # Bonding analysis def _init_bonds(self): """ Initialize Mulliken bonding analysis. """ if self.calculation_required(self.el.atoms,['energy']): raise AssertionError('Electronic structure is not solved yet!') if self.flags['bonds']==False: self.bonds = MullikenBondAnalysis(self) self.flags['bonds']=True def get_atom_energy(self,I=None): """ Return the energy of atom I (in eV). Warning: bonding & atom energy analysis less clear for systems where orbitals overlap with own periodic images. parameters: =========== I: atom index. If None, return all atoms' energies as an array. """ self._init_bonds() return self.bonds.get_atom_energy(I) def get_mayer_bond_order(self,i,j): """ Return Mayer bond-order between two atoms. Warning: bonding & atom energy analysis less clear for systems where orbitals overlap with own periodic images. parameters: =========== I: first atom index J: second atom index """ self._init_bonds() return self.bonds.get_mayer_bond_order(i,j) def get_promotion_energy(self,I=None): """ Return atom's promotion energy (in eV). Defined as: E_prom,I = sum_(mu in I) [q_(mu) - q_(mu)^0] epsilon_mu parameters: =========== I: atom index. If None, return all atoms' energies as an array. """ self._init_bonds() return self.bonds.get_promotion_energy(I) def get_bond_energy(self,i,j): """ Return the absolute bond energy between atoms (in eV). Warning: bonding & atom energy analysis less clear for systems where orbitals overlap with own periodic images. parameters: =========== i,j: atom indices """ self._init_bonds() return self.bonds.get_bond_energy(i,j) def get_atom_and_bond_energy(self,i=None): """ Return given atom's contribution to cohesion. parameters: =========== i: atom index. If None, return all atoms' energies as an array. """ self._init_bonds() return self.bonds.get_atom_and_bond_energy(i) def get_covalent_energy(self,mode='default',i=None,j=None,width=None,window=None,npts=501): """ Return covalent bond energies in different modes. (eV) ecov is described in Bornsen, Meyer, Grotheer, Fahnle, J. Phys.:Condens. Matter 11, L287 (1999) and Koskinen, Makinen Comput. Mat. Sci. 47, 237 (2009) parameters: =========== mode: 'default' total covalent energy 'orbitals' covalent energy for orbital pairs 'atoms' covalent energy for atom pairs 'angmom' covalent energy for angular momentum components i,j: atom or orbital indices, or angular momentum pairs width: * energy broadening (in eV) for ecov * if None, return energy eigenvalues and corresponding covalent energies in arrays, directly window: energy window (in eV wrt Fermi-level) for broadened ecov npts: number of points in energy grid (only with broadening) return: ======= x,y: * if width==None, x is list of energy eigenvalues (including k-points) and y covalent energies of those eigenstates * if width!=None, x is energy grid for ecov. * energies (both energy grid and ecov) are in eV. Note: energies are always shifted so that Fermi-level is at zero. Occupations are not otherwise take into account (while k-point weights are) """ self._init_bonds() return self.bonds.get_covalent_energy(mode,i,j,width,window,npts) def add_pair_potential(self,i,j,v,eVA=True): """ Add pair interaction potential function for elements or atoms parameters: =========== i,j: * atom indices, if integers (0,1,2,...) * elements, if strings ('C','H',...) v: Pair potential function. Only one potential per element and atom pair allowed. Syntax: v(r,der=0), v(r=None) returning the interaction range in Bohr or Angstrom. eVA: True for v in eV and Angstrom False for v in Hartree and Bohr """ self.pp.add_pair_potential(i,j,v,eVA)
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 DirectCoulomb(Coulomb): def __init__(self, cutoff=None, timer=None): """ Instantiate a new DirectCoulomb object which computes the electrostatic interaction by direct summation. Parameters: ----------- cutoff: If not None, the Coulomb interaction will be smoothly forced to zero at this distance by multiplication with erfc(r/cutoff) """ if timer is None: self.timer = Timer('DirectCoulomb') else: self.timer = timer self.cutoff = cutoff # Last positions self.r_av = None # Last charges self.q_a = None def update(self, a, q=None): if q is None: q = a.get_initial_charges() r = a.get_positions() # FIXME!!! Check for change in cell, symmetries if self.r_av is None or self.q_a is None: self._update(a, q) elif np.any(r != self.r_av) or np.any(q != self.q_a): self._update(a, q) def _update(self, a, q): """ Compute the electrostatic potential and field on each atom in a. Parameters: ----------- a: Hotbit Atoms object, or atoms object that implements the transform and rotation interface. q: Charges """ self.timer.start('direct_coulomb') self.a = a self.r_av = a.get_positions().copy() self.q_a = q.copy() nat = len(a) il, jl, dl, nl = get_neighbors(a, self.cutoff) if il is not None: if self.cutoff is None: phi = q[jl]/dl dl **= 2 E = q[jl].reshape(-1, 1)*nl/dl.reshape(-1, 1) else: f = erfc(dl/self.cutoff) df = 2/sqrt(pi)*np.exp(-(dl/self.cutoff)**2)/self.cutoff phi = q[jl]*f/dl E = q[jl]*(df + f/dl)/dl E = E.reshape(-1, 1)*nl self.phi_a = np.zeros(nat, dtype=float) self.E_av = np.zeros([nat, 3], dtype=float) if il is not None: # FIXME!!! Is there some fast numpy magic to compute this? for i in xrange(nat): self.phi_a[i] = phi[il == i].sum() self.E_av[i, :] = E[il == i].sum(axis=0) self.timer.stop('direct_coulomb') def get_potential(self, a=None): """ Return the electrostatic potential for each atom. """ if a is not None: self.update(a) return self.phi_a def get_field(self, a=None): """ Return the electrostatic field for each atom. """ if a is not None: self.update(a) return self.E_av def get_potential_and_field(self, a=None): """ Return the both, the electrostatic potential and the field for each atom. """ if a is not None: self.update(a) return self.phi_a, self.E_av def get_gamma(self, a=None): """ Return the gamma correlation matrix, i.e. phi(i) = gamma(i, j)*q(j) """ if a is not None: self.update(a) self.timer.start('get_gamma') nat = len(self.a) il, jl, dl, nl = get_neighbors(self.a, self.cutoff) if il is None: G = None else: G = np.zeros([nat, nat], dtype=float) if self.cutoff is None: for i, j, d in zip(il, jl, dl): G[i, j] += 1.0/d else: for i, j, d in zip(il, jl, dl): G[i, j] += 1.0*erfc(d/self.cutoff)/d self.timer.stop('get_gamma') return G ### For use as a standalone calculator def get_potential_energy(self, a=None): """ Return the Coulomb energy. """ if a is not None: self.update(a) return np.sum(self.q_a*self.phi_a)/2 def get_forces(self, a=None): """ Return forces """ if a is not None: self.update(a) return self.q_a.reshape(-1, 1)*self.E_av
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 MultipoleExpansion(Coulomb): _TOL = 1e-6 def __init__(self, l_max=8, n=3, k=5, r0=None, timer=None): """ Instantiate a new MultipoleExpansion object which computes the electrostatic interaction by direct summation using a telescoped multipole expansion. Parameters: ----------- l_max: Order of the expansion (maximum angular momentum, typically 5 to 8) n: Number of cells to combine during each telescoping step k: Summation cutoff. The interaction range will be n**k (number of cells, typically k = 5). """ if l_max < 1: raise ValueError("l_max must be >= 1.") if np.any(np.array(n) < 1): raise ValueError("n must be >= 1.") if np.any(np.array(k) < 1): raise ValueError("k must be >= 1.") self.l_max = l_max if type(n) == int: n = np.array([n] * 3) else: n = np.array(n) if len(n) != 3: raise TypeError("n must be an integer scalar or a 3-tuple.") # The multipole-to-multipole operation is carried out over the # range [self.n1, self.n2] self.n1 = -((n - 1) / 2) self.n2 = n / 2 # self.dx = -0.5*(self.n1 + self.n2) # The multipole-to-local operation is carried out over cells farther # away, hence the range [self.m1, self.m2] n **= 2 self.m1 = -((n - 1) / 2) self.m2 = n / 2 if type(k) == int: self.k = np.array([k] * 3) else: self.k = np.array(k) if len(self.k) != 3: raise TypeError("k must be an integer scalar or a 3-tuple.") self.r0_v = None if r0 is not None: self.r0_v = np.asarray(r0).copy() if timer is None: self.timer = Timer("MultipoleExpansion") else: self.timer = timer # Last positions self.r_av = None # Last charges self.q_a = None def update(self, a, q=None): if q is None: q = a.get_initial_charges() r = a.get_positions() # FIXME!!! Check for change in cell, symmetries if self.r_av is None or self.q_a is None: self._update(a, q) elif np.any(r != self.r_av) or np.any(q != self.q_a): self._update(a, q) def _update(self, a, q): """ Compute multipoles, do the transformations, and compute the electrostatic potential and field on each atom in a. Parameters: ----------- a: Hotbit Atoms object, or atoms object that implements the transform and rotation interface. q: Charges """ self.timer.start("multipole_to_multipole") self.r_av = a.get_positions().copy() self.q_a = q.copy() nat = len(a) r = a.get_positions() if self.r0_v is None: r0_v = np.sum(r, axis=0) / len(a) else: r0_v = self.r0_v T0_l, T_L = get_moments(r, q, self.l_max, r0_v) self.M = [(T0_l.copy(), T_L.copy())] self.r0 = [r0_v] sym_ranges = a.get_symmetry_operation_ranges() for (s1, s2), k in zip(sym_ranges, self.k): if s2 != np.Inf and k != 1: print sym_ranges print self.k raise ValueError("For non-periodic symmetries the k-value must " "be 1.") n1, n2, n3 = n_from_ranges(sym_ranges, self.n1, self.n2) # Compute telescoped multipoles level = np.ones(3, dtype=int) for k in range(np.max(self.k) - 2): M0_l = T0_l M_L = T_L T0_l = np.zeros_like(M0_l) T_L = np.zeros_like(M_L) if k >= self.k[0] - 2: _n1 = [0, 1] else: _n1 = n1 if k >= self.k[1] - 2: _n2 = [0, 1] else: _n2 = n2 if k >= self.k[2] - 2: _n3 = [0, 1] else: _n3 = n3 r0_v = np.zeros(3, dtype=float) n = 0 # Determine center of gravity for x1 in range(*_n1): for x2 in range(*_n2): for x3 in range(*_n3): x = np.array([x1, x2, x3]) r0_v += a.transform(self.r0[k], x * level) n += 1 r0_v /= n self.r0 += [r0_v] # self.r0 += [ self.r0[0] ] # Transform multipoles for x1 in range(*_n1): for x2 in range(*_n2): for x3 in range(*_n3): # Loop over all symmetry operations and compute # telescoped multipoles # FIXME!!! Currently only supports continuous # symmetries, think about discrete/recurrent ones. x = np.array([x1, x2, x3]) # + self.dx # The origin is already okay, skip it # if np.any(np.abs(x) > self._TOL): r1 = a.transform(self.r0[k], x * level) T = a.rotation(x * level) S0_l, S_L = transform_multipole(T, self.l_max, M0_l, M_L) multipole_to_multipole(r1 - self.r0[k], self.l_max, S0_l, S_L, T0_l, T_L) self.M += [(T0_l.copy(), T_L.copy())] level *= self.n2 - self.n1 + 1 self.timer.stop("multipole_to_multipole") ### self.timer.start("multipole_to_local") # Compute the local expansion from telescoped multipoles L0_l, L_L = zero_moments(self.l_max) m1, m2, m3 = n_from_ranges(sym_ranges, self.m1, self.m2) Mi = len(self.M) - 1 for k in range(np.max(self.k) - 1): M0_l, M_L = self.M[Mi] if k >= self.k[0] - 1: _m1 = [0, 1] else: _m1 = m1 if k >= self.k[1] - 1: _m2 = [0, 1] else: _m2 = m2 if k >= self.k[2] - 1: _m3 = [0, 1] else: _m3 = m3 for x1 in range(*_m1): for x2 in range(*_m2): for x3 in range(*_m3): # Loop over all symmetry operations and compute the # local expansion from the telescoped multipoles x = np.array([x1, x2, x3]) # + self.dx # No local expansion in the inner region if np.any(x < self.n1) or np.any(x > self.n2): r1 = a.transform(self.r0[Mi], x * level) T = a.rotation(x * level) S0_l, S_L = transform_multipole(T, self.l_max, M0_l, M_L) multipole_to_local(-r1 + self.r0[Mi], self.l_max, S0_l, S_L, L0_l, L_L) level /= self.n2 - self.n1 + 1 Mi -= 1 self.L = (L0_l, L_L) self.timer.stop("multipole_to_local") ### self.phi_a = np.zeros(nat, dtype=float) self.E_av = np.zeros([nat, 3], dtype=float) ### self.timer.start("local_to_local") for i in a: loc0_l, loc_L = local_to_local(i.position - self.r0[0], self.l_max, L0_l, L_L, 1) self.phi_a[i.index] = loc0_l[0] self.E_av[i.index, :] = [-loc_L[0].real, -loc_L[0].imag, loc0_l[1]] self.timer.stop("local_to_local") ### self.timer.start("near_field") # Contribution of neighboring boxes for x1 in range(*n1): for x2 in range(*n2): for x3 in range(*n3): # self-interaction needs to be treated separately if x1 != 0 or x2 != 0 or x3 != 0: x = np.array([x1, x2, x3]) # construct a matrix with distances r1 = a.transform(self.r0[0], x) T = a.rotation(x) rT = np.dot(r - self.r0[0], np.transpose(T)) dr = r.reshape(nat, 1, 3) - (r1 + rT).reshape(1, nat, 3) abs_dr = np.sqrt(np.sum(dr * dr, axis=2)) phi = q / abs_dr E = q.reshape(1, nat, 1) * dr / (abs_dr ** 3).reshape(nat, nat, 1) self.phi_a += np.sum(phi, axis=1) self.E_av += np.sum(E, axis=1) # Self-contribution dr = r.reshape(nat, 1, 3) - r.reshape(1, nat, 3) abs_dr = np.sqrt(np.sum(dr * dr, axis=2)) # Avoid divide by zero abs_dr[diag_indices_from(abs_dr)] = 1.0 phi = q / abs_dr E = q.reshape(1, nat, 1) * dr / (abs_dr ** 3).reshape(nat, nat, 1) phi[diag_indices_from(phi)] = 0.0 E[diag_indices_from(phi)] = 0.0 self.phi_a += np.sum(phi, axis=1) self.E_av += np.sum(E, axis=1) # Dipole correction for 3D sum s1, s2, s3 = sym_ranges if s1[1] == np.Inf and s2[1] == np.Inf and s3[1] == np.Inf: Ml0, Mlm = self.M[0] dip = np.array([-2 * Mlm[0].real, 2 * Mlm[0].imag, Ml0[1]]) dip *= 4 * pi / (3 * a.get_volume()) self.phi_a -= np.dot(r - self.r0[0], dip) self.E_av += dip self.timer.stop("near_field") def get_moments(self): """ Return the multipole moments. """ return self.M def get_local_expansion(self): """ Return the local expansion of the potential. """ return self.L def get_potential(self, a=None): """ Return the electrostatic potential for each atom. """ if a is not None: self.update(a) return self.phi_a def get_field(self, a=None): """ Return the electrostatic field for each atom. """ if a is not None: self.update(a) return self.E_av def get_potential_and_field(self, a=None): """ Return the both, the electrostatic potential and the field for each atom. """ if a is not None: self.update(a) return self.phi_a, self.E_av ### For use as a standalone calculator def get_potential_energy(self, a=None): """ Return the Coulomb energy. """ if a is not None: self.update(a) return np.sum(self.q_a * self.phi_a) / 2 def get_forces(self, a=None): """ Return forces """ if a is not None: self.update(a) return self.q_a.reshape(-1, 1) * self.E_av
class MultipoleExpansion(Coulomb): _TOL = 1e-6 def __init__(self, l_max=8, n=3, k=5, r0=None, timer=None): """ Instantiate a new MultipoleExpansion object which computes the electrostatic interaction by direct summation using a telescoped multipole expansion. Parameters: ----------- l_max: Order of the expansion (maximum angular momentum, typically 5 to 8) n: Number of cells to combine during each telescoping step k: Summation cutoff. The interaction range will be n**k (number of cells, typically k = 5). """ if l_max < 1: raise ValueError('l_max must be >= 1.') if np.any(np.array(n) < 1): raise ValueError('n must be >= 1.') if np.any(np.array(k) < 1): raise ValueError('k must be >= 1.') self.l_max = l_max if type(n) == int: n = np.array([n] * 3) else: n = np.array(n) if len(n) != 3: raise TypeError('n must be an integer scalar or a 3-tuple.') # The multipole-to-multipole operation is carried out over the # range [self.n1, self.n2] self.n1 = -((n - 1) // 2) self.n2 = n // 2 #self.dx = -0.5*(self.n1 + self.n2) # The multipole-to-local operation is carried out over cells farther # away, hence the range [self.m1, self.m2] n **= 2 self.m1 = -((n - 1) // 2) self.m2 = n // 2 if type(k) == int: self.k = np.array([k] * 3) else: self.k = np.array(k) if len(self.k) != 3: raise TypeError('k must be an integer scalar or a 3-tuple.') self.r0_v = None if r0 is not None: self.r0_v = np.asarray(r0).copy() if timer is None: self.timer = Timer('MultipoleExpansion') else: self.timer = timer # Last positions self.r_av = None # Last charges self.q_a = None def update(self, a, q=None): if q is None: q = a.get_initial_charges() r = a.get_positions() # FIXME!!! Check for change in cell, symmetries if self.r_av is None or self.q_a is None: self._update(a, q) elif np.any(r != self.r_av) or np.any(q != self.q_a): self._update(a, q) def _update(self, a, q): """ Compute multipoles, do the transformations, and compute the electrostatic potential and field on each atom in a. Parameters: ----------- a: Hotbit Atoms object, or atoms object that implements the transform and rotation interface. q: Charges """ self.timer.start('multipole_to_multipole') self.r_av = a.get_positions().copy() self.q_a = q.copy() nat = len(a) r = a.get_positions() if self.r0_v is None: r0_v = np.sum(r, axis=0) / len(a) else: r0_v = self.r0_v T0_l, T_L = get_moments(r, q, self.l_max, r0_v) self.M = [(T0_l.copy(), T_L.copy())] self.r0 = [r0_v] sym_ranges = a.get_symmetry_operation_ranges() for (s1, s2), k in zip(sym_ranges, self.k): if s2 != np.Inf and k != 1: print(sym_ranges) print(self.k) raise ValueError( 'For non-periodic symmetries the k-value must ' 'be 1.') n1, n2, n3 = n_from_ranges(sym_ranges, self.n1, self.n2) # Compute telescoped multipoles level = np.ones(3, dtype=int) for k in range(np.max(self.k) - 2): M0_l = T0_l M_L = T_L T0_l = np.zeros_like(M0_l) T_L = np.zeros_like(M_L) if k >= self.k[0] - 2: _n1 = [0, 1] else: _n1 = n1 if k >= self.k[1] - 2: _n2 = [0, 1] else: _n2 = n2 if k >= self.k[2] - 2: _n3 = [0, 1] else: _n3 = n3 r0_v = np.zeros(3, dtype=float) n = 0 # Determine center of gravity for x1 in range(*_n1): for x2 in range(*_n2): for x3 in range(*_n3): x = np.array([x1, x2, x3]) r0_v += a.transform(self.r0[k], x * level) n += 1 r0_v /= n self.r0 += [r0_v] #self.r0 += [ self.r0[0] ] # Transform multipoles for x1 in range(*_n1): for x2 in range(*_n2): for x3 in range(*_n3): # Loop over all symmetry operations and compute # telescoped multipoles # FIXME!!! Currently only supports continuous # symmetries, think about discrete/recurrent ones. x = np.array([x1, x2, x3]) #+ self.dx # The origin is already okay, skip it #if np.any(np.abs(x) > self._TOL): r1 = a.transform(self.r0[k], x * level) T = a.rotation(x * level) S0_l, S_L = transform_multipole( T, self.l_max, M0_l, M_L) multipole_to_multipole(r1 - self.r0[k], self.l_max, S0_l, S_L, T0_l, T_L) self.M += [(T0_l.copy(), T_L.copy())] level *= self.n2 - self.n1 + 1 self.timer.stop('multipole_to_multipole') ### self.timer.start('multipole_to_local') # Compute the local expansion from telescoped multipoles L0_l, L_L = zero_moments(self.l_max) m1, m2, m3 = n_from_ranges(sym_ranges, self.m1, self.m2) Mi = len(self.M) - 1 for k in range(np.max(self.k) - 1): M0_l, M_L = self.M[Mi] if k >= self.k[0] - 1: _m1 = [0, 1] else: _m1 = m1 if k >= self.k[1] - 1: _m2 = [0, 1] else: _m2 = m2 if k >= self.k[2] - 1: _m3 = [0, 1] else: _m3 = m3 for x1 in range(*_m1): for x2 in range(*_m2): for x3 in range(*_m3): # Loop over all symmetry operations and compute the # local expansion from the telescoped multipoles x = np.array([x1, x2, x3]) #+ self.dx # No local expansion in the inner region if np.any(x < self.n1) or np.any(x > self.n2): r1 = a.transform(self.r0[Mi], x * level) T = a.rotation(x * level) S0_l, S_L = transform_multipole( T, self.l_max, M0_l, M_L) multipole_to_local(-r1 + self.r0[Mi], self.l_max, S0_l, S_L, L0_l, L_L) level //= self.n2 - self.n1 + 1 Mi -= 1 self.L = (L0_l, L_L) self.timer.stop('multipole_to_local') ### self.phi_a = np.zeros(nat, dtype=float) self.E_av = np.zeros([nat, 3], dtype=float) ### self.timer.start('local_to_local') for i in a: loc0_l, loc_L = local_to_local(i.position - self.r0[0], self.l_max, L0_l, L_L, 1) self.phi_a[i.index] = loc0_l[0] self.E_av[i.index, :] = [-loc_L[0].real, -loc_L[0].imag, loc0_l[1]] self.timer.stop('local_to_local') ### self.timer.start('near_field') # Contribution of neighboring boxes for x1 in range(*n1): for x2 in range(*n2): for x3 in range(*n3): # self-interaction needs to be treated separately if x1 != 0 or x2 != 0 or x3 != 0: x = np.array([x1, x2, x3]) # construct a matrix with distances r1 = a.transform(self.r0[0], x) T = a.rotation(x) rT = np.dot(r - self.r0[0], np.transpose(T)) dr = r.reshape(nat, 1, 3) - \ (r1+rT).reshape(1, nat, 3) abs_dr = np.sqrt(np.sum(dr * dr, axis=2)) phi = q / abs_dr E = q.reshape(1, nat, 1)*dr/ \ (abs_dr**3).reshape(nat, nat, 1) self.phi_a += np.sum(phi, axis=1) self.E_av += np.sum(E, axis=1) # Self-contribution dr = r.reshape(nat, 1, 3) - r.reshape(1, nat, 3) abs_dr = np.sqrt(np.sum(dr * dr, axis=2)) # Avoid divide by zero abs_dr[diag_indices_from(abs_dr)] = 1.0 phi = q / abs_dr E = q.reshape(1, nat, 1) * dr / (abs_dr**3).reshape(nat, nat, 1) phi[diag_indices_from(phi)] = 0.0 E[diag_indices_from(phi)] = 0.0 self.phi_a += np.sum(phi, axis=1) self.E_av += np.sum(E, axis=1) # Dipole correction for 3D sum s1, s2, s3 = sym_ranges if s1[1] == np.Inf and s2[1] == np.Inf and s3[1] == np.Inf: Ml0, Mlm = self.M[0] dip = np.array([-2 * Mlm[0].real, 2 * Mlm[0].imag, Ml0[1]]) dip *= 4 * pi / (3 * a.get_volume()) self.phi_a -= np.dot(r - self.r0[0], dip) self.E_av += dip self.timer.stop('near_field') def get_moments(self): """ Return the multipole moments. """ return self.M def get_local_expansion(self): """ Return the local expansion of the potential. """ return self.L def get_potential(self, a=None): """ Return the electrostatic potential for each atom. """ if a is not None: self.update(a) return self.phi_a def get_field(self, a=None): """ Return the electrostatic field for each atom. """ if a is not None: self.update(a) return self.E_av def get_potential_and_field(self, a=None): """ Return the both, the electrostatic potential and the field for each atom. """ if a is not None: self.update(a) return self.phi_a, self.E_av ### For use as a standalone calculator def get_potential_energy(self, a=None): """ Return the Coulomb energy. """ if a is not None: self.update(a) return np.sum(self.q_a * self.phi_a) / 2 def get_forces(self, a=None): """ Return forces """ if a is not None: self.update(a) return self.q_a.reshape(-1, 1) * self.E_av
class DirectCoulomb(Coulomb): def __init__(self, cutoff=None, timer=None): """ Instantiate a new DirectCoulomb object which computes the electrostatic interaction by direct summation. Parameters: ----------- cutoff: If not None, the Coulomb interaction will be smoothly forced to zero at this distance by multiplication with erfc(r/cutoff) """ if timer is None: self.timer = Timer('DirectCoulomb') else: self.timer = timer self.cutoff = cutoff # Last positions self.r_av = None # Last charges self.q_a = None def update(self, a, q=None): if q is None: q = a.get_initial_charges() r = a.get_positions() # FIXME!!! Check for change in cell, symmetries if self.r_av is None or self.q_a is None: self._update(a, q) elif np.any(r != self.r_av) or np.any(q != self.q_a): self._update(a, q) def _update(self, a, q): """ Compute the electrostatic potential and field on each atom in a. Parameters: ----------- a: Hotbit Atoms object, or atoms object that implements the transform and rotation interface. q: Charges """ self.timer.start('direct_coulomb') self.a = a self.r_av = a.get_positions().copy() self.q_a = q.copy() nat = len(a) il, jl, dl, nl = get_neighbors(a, self.cutoff) if il is not None: if self.cutoff is None: phi = q[jl] / dl dl **= 2 E = q[jl].reshape(-1, 1) * nl / dl.reshape(-1, 1) else: f = erfc(dl / self.cutoff) df = 2 / sqrt(pi) * np.exp(-(dl / self.cutoff)**2) / self.cutoff phi = q[jl] * f / dl E = q[jl] * (df + f / dl) / dl E = E.reshape(-1, 1) * nl self.phi_a = np.zeros(nat, dtype=float) self.E_av = np.zeros([nat, 3], dtype=float) if il is not None: # FIXME!!! Is there some fast numpy magic to compute this? for i in xrange(nat): self.phi_a[i] = phi[il == i].sum() self.E_av[i, :] = E[il == i].sum(axis=0) self.timer.stop('direct_coulomb') def get_potential(self, a=None): """ Return the electrostatic potential for each atom. """ if a is not None: self.update(a) return self.phi_a def get_field(self, a=None): """ Return the electrostatic field for each atom. """ if a is not None: self.update(a) return self.E_av def get_potential_and_field(self, a=None): """ Return the both, the electrostatic potential and the field for each atom. """ if a is not None: self.update(a) return self.phi_a, self.E_av def get_gamma(self, a=None): """ Return the gamma correlation matrix, i.e. phi(i) = gamma(i, j)*q(j) """ if a is not None: self.update(a) self.timer.start('get_gamma') nat = len(self.a) il, jl, dl, nl = get_neighbors(self.a, self.cutoff) if il is None: G = None else: G = np.zeros([nat, nat], dtype=float) if self.cutoff is None: for i, j, d in zip(il, jl, dl): G[i, j] += 1.0 / d else: for i, j, d in zip(il, jl, dl): G[i, j] += 1.0 * erfc(d / self.cutoff) / d self.timer.stop('get_gamma') return G ### For use as a standalone calculator def get_potential_energy(self, a=None): """ Return the Coulomb energy. """ if a is not None: self.update(a) return np.sum(self.q_a * self.phi_a) / 2 def get_forces(self, a=None): """ Return forces """ if a is not None: self.update(a) return self.q_a.reshape(-1, 1) * self.E_av
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 EwaldSum(Coulomb): def __init__(self, accuracy_goal, weight, timer=None): self.accuracy_goal = accuracy_goal self.weight = weight if timer is None: self.timer = Timer('EwaldSum') else: self.timer = timer def update(self, a, q): """ Compute the electrostatic potential. Parameters: ----------- a: Hotbit Atoms object, or atoms object that implements the transform and rotation interface. q: Charges """ self.alpha = (self.weight * pi**3 * len(a) / a.get_volume())**(1. / 3) self.sqrt_alpha = sqrt(self.alpha) self.G_cutoff = 2 * sqrt(log(10.0) * self.accuracy_goal * self.alpha) self.r_cutoff = sqrt(log(10.0) * self.accuracy_goal / self.alpha) cell_cv = a.get_cell() rec_cell_vc = np.linalg.inv(cell_cv) r_av = a.get_positions() self.timer.start('reciprocal sum') # Reciprocal sum lx, ly, lz = np.sqrt(np.sum(rec_cell_vc**2, axis=0)) maxGx = int(self.G_cutoff / (2 * pi * lx)) + 1 maxGy = int(self.G_cutoff / (2 * pi * ly)) + 1 maxGz = int(self.G_cutoff / (2 * pi * lz)) + 1 Gx = 2 * pi * np.arange(-maxGx, maxGx + 1).reshape(-1, 1, 1, 1) Gy = 2 * pi * np.arange(-maxGy, maxGy + 1).reshape(1, -1, 1, 1) Gz = 2 * pi * np.arange(-maxGz, maxGz + 1).reshape(1, 1, -1, 1) G = Gx * np.array([1, 0, 0]) + Gy * np.array( [0, 1, 0]) + Gz * np.array([0, 0, 1]) G = np.dot(G, rec_cell_vc) si = np.sum(np.sin(np.tensordot(G, r_av, axes=(3, 1))) * q, axis=3) co = np.sum(np.cos(np.tensordot(G, r_av, axes=(3, 1))) * q, axis=3) G_sq = np.sum(G * G, axis=3) rec_G_sq = 1.0 / G_sq rec_G_sq[maxGx, maxGy, maxGz] = 0.0 phase = np.tensordot(G, r_av, axes=(3, 1)) si.shape = (2 * maxGx + 1, 2 * maxGy + 1, 2 * maxGz + 1, 1) co.shape = (2 * maxGx + 1, 2 * maxGy + 1, 2 * maxGz + 1, 1) self.phi_a = np.sum(np.sum(np.sum( (np.exp(-G_sq / (4 * self.alpha)) * rec_G_sq).reshape( 2 * maxGx + 1, 2 * maxGy + 1, 2 * maxGz + 1, 1) * (si * np.sin(phase) + co * np.cos(phase)), axis=0), axis=0), axis=0) self.phi_a *= 4 * pi / a.get_volume() self.timer.stop('reciprocal sum') self.timer.start('real space sum') # Real space sum lx, ly, lz = np.sqrt(np.sum(cell_cv**2, axis=1)) maxrx = int(self.r_cutoff / lx) + 1 maxry = int(self.r_cutoff / ly) + 1 maxrz = int(self.r_cutoff / lz) + 1 nat = len(a) r = a.get_positions() for x in range(-maxrx, maxrx + 1): for y in range(-maxry, maxry + 1): for z in range(-maxrz, maxrz + 1): if x != 0 or y != 0 or z != 0: r1 = np.dot([x, y, z], cell_cv) dr = r.reshape(nat, 1, 3) - \ (r1+r).reshape(1, nat, 3) abs_dr = np.sqrt(np.sum(dr * dr, axis=2)) phi = q * erfc(self.sqrt_alpha * abs_dr) / abs_dr self.phi_a += np.sum(phi, axis=1) ## Self-contribution dr = r.reshape(nat, 1, 3) - r.reshape(1, nat, 3) abs_dr = np.sqrt(np.sum(dr * dr, axis=2)) ## Avoid divide by zero abs_dr[diag_indices_from(abs_dr)] = 1.0 phi = q * erfc(self.sqrt_alpha * abs_dr) / abs_dr phi[diag_indices_from(phi)] = 0.0 self.phi_a += np.sum(phi, axis=1) self.timer.stop('real space sum') # Self energy self.phi_a -= 2 * q * sqrt(self.alpha / pi) def get_potential(self): """ Return the electrostatic potential for each atom. """ return self.phi_a ### For use as a standalone calculator ### Note: These functions assume eV/A units def get_potential_energy(self, a, q=None): if q is None: q = a.get_charges() self.update(a, q) return Hartree * Bohr * np.sum(q * self.phi_a) / 2
class Hotbit(Output): def __init__( self, parameters=None, elements=None, tables=None, verbose=False, charge=0.0, SCC=True, kpts=(1, 1, 1), rs="kappa", physical_k=True, maxiter=50, gamma_cut=None, txt=None, verbose_SCC=False, width=0.02, mixer=None, coulomb_solver=None, charge_density="Gaussian", vdw=False, vdw_parameters=None, internal={}, ): """ Hotbit -- density-functional tight-binding calculator for atomic simulation environment (ASE). Parameters: ----------- parameters: The directory for parametrization files. * If parameters==None, use HOTBIT_PARAMETERS environment variable. * Parametrizations given by 'elements' and 'tables' keywords override parametrizations in this directory. elements: Files for element data (*.elm). example: {'H':'H_custom.elm','C':'/../C.elm'} * If extension '.elm' is omitted, it is assumed. * Items can also be elements directly: {'H':H} (H is type Element) * If elements==None, use element info from default directory. * If elements['rest']=='default', use default parameters for all other elements than the ones specified. E.g. {'H':'H.elm','rest':'default'} (otherwise all elements present have to be specified explicitly). tables: Files for Slater-Koster tables. example: {'CH':'C_H.par','CC':'C_C.par'} * If extension '.par' is omitted, it is assumed. * If tables==None, use default interactions. * If tables['rest']='default', use default parameters for all other interactions, e.g. {'CH':'C_H.par','rest':'default'} * If tables['AB']==None, ignore interactions for A and B (both chemical and repulsive) mixer: Density mixer. example: {'name':'Anderson','mixing_constant':0.2, 'memory':5}. charge: Total charge for system (-1 means an additional electron) width: Width of Fermi occupation (eV) SCC: Self-Consistent Charge calculation * True for SCC-DFTB, False for DFTB kpts: Number of k-points. * For translational symmetry points are along the directions given by the cell vectors. * For general symmetries, you need to look at the info from the container used rs: * 'kappa': use kappa-points * 'k': use normal k-points. Only for Bravais lattices. physical_k Use physical (realistic) k-points for generally periodic systems. * Ignored with normal translational symmetry * True for physically allowed k-points in periodic symmetries. maxiter: Maximum number of self-consistent iterations * only for SCC-DFTB coulomb_solver: The Coulomb solver object. If None, a DirectCoulomb object will the automatically instantiated. * only for SCC-DFTB charge_density: Shape of the excess charge on each atom. Possibilities are: * 'Gaussian': Use atom centered Gaussians. This is the default. * 'Slater': Slater-type exponentials as used in the original SCC-DFTB scheme. * only for SCC-DFTB gamma_cut: Range for Coulomb interaction if direct summation is selected (coulomb_solver = None). * only for SCC-DFTB vdw: Include van der Waals interactions vdw_parameters: Dictionary containing the parameters for the van-der-Waals interaction for each element. i.e. { el: ( p, R0 ), ... } where *el* is the element name, *p* the polarizability and *R0* the radius where the van-der-Waals interaction starts. Will override whatever read from .elm files. txt: Filename for log-file. * None: standard output * '-': throw output to trash (/null) verbose_SCC: Increase verbosity in SCC iterations. internal: Dictionary for internal variables, some of which are set for stability purposes, some for quick and dirty bug fixes. Use these with caution! (For this reason, for the description of these variables you are forced to look at the source code.) """ from copy import copy import os if gamma_cut != None: gamma_cut = gamma_cut / Bohr self.__dict__ = { "parameters": parameters, "elements": elements, "tables": tables, "verbose": verbose, "charge": charge, "width": width / Hartree, "SCC": SCC, "kpts": kpts, "rs": rs, "physical_k": physical_k, "maxiter": maxiter, "gamma_cut": gamma_cut, "vdw": vdw, "vdw_parameters": vdw_parameters, "txt": txt, "verbose_SCC": verbose_SCC, "mixer": mixer, "coulomb_solver": coulomb_solver, "charge_density": charge_density, "internal": internal, } if parameters != None: os.environ.data["HOTBIT_PARAMETERS"] = parameters self.init = False self.notes = [] self.dry_run = "--dry-run" in sys.argv internal0 = { "sepsilon": 0.0, # add this to the diagonal of S to avoid LAPACK error in diagonalization "tol_imaginary_e": 1e-13, # tolerance for imaginary band energy "tol_mulliken": 1e-5, # tolerance for mulliken charge sum deviation from integer "tol_eigenvector_norm": 1e-6, # tolerance for eigenvector norm for eigensolver "symop_range": 5, } # range for the number of symmetry operations in all symmetries internal0.update(internal) for key in internal0: self.set(key, internal0[key]) # self.set_text(self.txt) # self.timer=Timer('Hotbit',txt=self.get_output()) def __del__(self): """ Delete calculator -> timing summary. """ if self.get("SCC"): try: print >>self.txt, self.st.solver.get_iteration_info() self.txt.flush() except: pass if len(self.notes) > 0: print >>self.txt, "Notes and warnings:" for note in self.notes: print >>self.txt, note if self.init: self.timer.summary() Output.__del__(self) def write_electronic_data(self, filename, keys=None): """ Write key electronic data into a file with *general* format. Hotbit is not needed to analyze the resulting data file. The data will be in a dictionary with the following items: N the number of atoms norb the number of orbitals nelectrons the number of electrons charge system charge epot potential energy ebs band structure energy ecoul coulomb energy erep repulsive energy forces atomic forces symbols element symbols e single-particle energies occ occupations nk number of k-points k k-point vectors wk k-point weights dq excess Mulliken populations gap energy gap gap_prob certainty of the gap determination above dose energies for density of states (all states over k-points as well) 0 = Fermi-level dos density of states (including k-point weights) Access to data, simply: data = numpy.load(filename) print data['epot'] parameters: ----------- filename: output file name keys: list of items (key names) to save. If None, save all. """ data = {} data["N"] = self.el.N data["norb"] = self.st.norb data["charge"] = self.get("charge") data["nelectrons"] = self.el.get_number_of_electrons() data["erep"] = self.rep.get_repulsive_energy() data["ecoul"] = self.get_coulomb_energy(self.el.atoms) data["ebs"] = self.get_band_structure_energy(self.el.atoms) data["epot"] = self.get_potential_energy(self.el.atoms) data["forces"] = self.get_forces(self.el.atoms) data["symbols"] = self.el.symbols data["e"] = self.st.e data["occ"] = self.st.f data["nk"] = self.st.nk data["k"] = self.st.k data["wk"] = self.st.wk data["dq"] = self.st.mulliken() data["gap"], data["gap_prob"] = self.get_energy_gap() data["dose"], data["dos"] = self.get_density_of_states(False) for key in data.keys(): if keys != None and key not in keys: del data[key] import pickle f = open(filename, "w") pickle.dump(data, f) f.close() def set(self, key, value): if key == "txt": self.set_text(value) elif self.init == True and key not in ["charge"]: raise AssertionError("Parameters cannot be set after initialization.") else: self.__dict__[key] = value def get_atoms(self): """ Return the current atoms object. """ atoms = self.el.atoms.copy() atoms.set_calculator(self) return atoms def add_note(self, note): """ Add warning (etc) note to be printed in log file end. """ self.notes.append(note) def greetings(self): """ Simple greetings text """ from time import asctime from os import uname from os.path import abspath, curdir from os import environ self.version = hotbit_version print >>self.txt, "\n\n\n\n\n" print >>self.txt, " _ _ _ _ _" print >>self.txt, "| |__ ___ | |_ | |__ |_| |_" print >>self.txt, "| _ \ / _ \| _|| _ \| | _|" print >>self.txt, "| | | | ( ) | |_ | ( ) | | |_" print >>self.txt, "|_| |_|\___/ \__|\____/|_|\__| ver.", self.version print >>self.txt, "Distributed under GNU GPL; see %s" % environ.get("HOTBIT_DIR") + "/LICENSE" print >>self.txt, "Date:", asctime() dat = uname() print >>self.txt, "Nodename:", dat[1] print >>self.txt, "Arch:", dat[4] print >>self.txt, "Dir:", abspath(curdir) print >>self.txt, "System:", self.el.get_name() print >>self.txt, " Charge=%4.1f" % self.charge print >>self.txt, " Container", self.el.container_info() print >>self.txt, "Symmetry operations (if any):" rs = self.get("rs") kpts = self.get("kpts") M = self.el.get_number_of_transformations() for i in range(3): print >>self.txt, " %i: pbc=" % i, self.el.atoms.get_pbc()[i], if type(kpts) == type([]): print >>self.txt, ", %s-points=%i, M=%.f" % (rs, len(kpts), M[i]) else: print >>self.txt, ", %s-points=%i, M=%.f" % (rs, kpts[i], M[i]) print >>self.txt, "Electronic temperature:", self.width * Hartree, "eV" mixer = self.st.solver.mixer print >>self.txt, "Mixer:", mixer.get("name"), "with memory =", mixer.get( "memory" ), ", mixing parameter =", mixer.get("beta") print >>self.txt, self.el.greetings() print >>self.txt, self.ia.greetings() print >>self.txt, self.rep.greetings() if self.pp.exists(): print >>self.txt, self.pp.greetings() def out(self, text): print >>self.txt, text self.txt.flush() def set_text(self, txt): """ Set up the output file. """ if txt == "-" or txt == "null": self.txt = open("/dev/null", "w") elif hasattr(txt, "write"): self.txt = txt elif txt is None: from sys import stdout self.txt = stdout else: self.txt = open(txt, "a") # check if the output of timer must be changed also if "timer" in self.__dict__: self.timer.txt = self.get_output() def get(self, arg=None): """ Get calculator input parameters. arg: 'kpts','width',... """ if arg == None: return self.__dict__ else: return self.__dict__[arg] def memory_estimate(self): """ Print an estimate for memory consumption in GB. If script run with --dry-run, exit. """ if self.st.nk > 1: number = 16.0 # complex else: number = 8.0 # real M = self.st.nk * self.st.norb ** 2 * number # H S dH0 dS wf H1 dH rho rhoe mem = M + M + 3 * M + 3 * M + M + M + 3 * M + M + M print >>self.txt, "Memory consumption estimate: > %.2f GB" % (mem / 1e9) self.txt.flush() if self.dry_run: raise SystemExit def solve_ground_state(self, atoms): """ If atoms moved, solve electronic structure. """ if not self.init: assert type(atoms) != type(None) self._initialize(atoms) if type(atoms) == type(None): pass elif self.calculation_required(atoms, "ground state"): self.el.update_geometry(atoms) t0 = time() self.st.solve() self.el.set_solved("ground state") t1 = time() self.flags["Mulliken"] = False self.flags["DOS"] = False self.flags["bonds"] = False if self.verbose: print >>self.get_output(), "Solved in %0.2f seconds" % (t1 - t0) # if self.get('SCC'): # atoms.set_charges(-self.st.get_dq()) else: pass def _initialize(self, atoms): """ Initialization of hotbit. """ if not self.init: self.set_text(self.txt) self.timer = Timer("Hotbit", txt=self.get_output()) self.start_timing("initialization") self.el = Elements(self, atoms) self.ia = Interactions(self) self.st = States(self) self.rep = Repulsion(self) self.pp = PairPotential(self) if self.get("vdw"): if self.get("vdw_parameters") is not None: self.el.update_vdw(self.get("vdw_parameters")) setup_vdw(self) self.env = Environment(self) pbc = atoms.get_pbc() # FIXME: gamma_cut -stuff # if self.get('SCC') and np.any(pbc) and self.get('gamma_cut')==None: # raise NotImplementedError('SCC not implemented for periodic systems yet (see parameter gamma_cut).') if np.any(pbc) and abs(self.get("charge")) > 0.0 and self.get("SCC"): raise AssertionError("Charged system cannot be periodic.") self.flush() self.flags = {} self.flags["Mulliken"] = False self.flags["DOS"] = False self.flags["bonds"] = False self.flags["grid"] = False self.stop_timing("initialization") self.el.set_atoms(atoms) if not self.init: self.init = True self.greetings() def calculation_required(self, atoms, quantities): """ Check if a calculation is required. Check if the quantities in the quantities list have already been calculated for the atomic configuration atoms. The quantities can be one or more of: 'ground state', 'energy', 'forces', 'magmoms', and 'stress'. """ return self.el.calculation_required(atoms, quantities) def get_potential_energy(self, atoms): """ Return the potential energy of present system. """ if self.calculation_required(atoms, ["energy"]): self.solve_ground_state(atoms) self.start_timing("energy") ebs = self.get_band_structure_energy(atoms) ecoul = self.get_coulomb_energy(atoms) erep = self.rep.get_repulsive_energy() epp = self.pp.get_energy() self.epot = ebs + ecoul + erep + epp - self.el.efree * Hartree self.stop_timing("energy") self.el.set_solved("energy") return self.epot.copy() def get_forces(self, atoms): """ Return forces (in eV/Angstrom) Ftot = F(band structure) + F(coulomb) + F(repulsion). """ if self.calculation_required(atoms, ["forces"]): self.solve_ground_state(atoms) self.start_timing("forces") fbs = self.st.get_band_structure_forces() frep = self.rep.get_repulsive_forces() fcoul = self.st.es.gamma_forces() # zero for non-SCC fpp = self.pp.get_forces() self.stop_timing("forces") self.f = (fbs + frep + fcoul + fpp) * (Hartree / Bohr) self.el.set_solved("forces") return self.f.copy() def get_band_energies(self, kpts=None, shift=True, rs="kappa", h1=False): """ Return band energies for explicitly given list of k-points. parameters: =========== kpts: list of k-points; e.g. kpts=[(0,0,0),(pi/2,0,0),(pi,0,0)] k- or kappa-points, depending on parameter rs. if None, return for all k-points in the calculation shift: shift zero to the Fermi-level rs: use 'kappa'- or 'k'-points in reciprocal space h1: Add Coulomb part to hamiltonian matrix. Required for consistent use of SCC. """ if kpts == None: e = self.st.e * Hartree else: if rs == "k": klist = k_to_kappa_points(kpts, self.el.atoms) elif rs == "kappa": klist = kpts e = self.st.get_band_energies(klist, h1) * Hartree if shift: return e - self.get_fermi_level() else: return e def get_stress(self, atoms): self.solve_ground_state(atoms) # TODO: ASE needs an array from this method, would it be proper to # somehow inform that the stresses are not calculated? return np.zeros((6,)) def get_charge(self): """ Return system's total charge. """ return self.get("charge") def get_eigenvalues(self): """ Return eigenvalues without shifts. For alternative, look at method get_band_energies. """ return self.st.get_eigenvalues() * Hartree def get_energy_gap(self): """ Return the energy gap. (in eV) Gap is the energy difference between the first states above and below Fermi-level. Return also the probability of having returned the gap; it is the difference in the occupations of these states, divided by 2. """ eigs = (self.get_eigenvalues() - self.get_fermi_level()).flatten() occ = self.get_occupations().flatten() ehi, elo = 1e10, -1e10 for e, f in zip(eigs, occ): if elo < e <= 0.0: elo = e flo = f elif 0.0 < e < ehi: ehi = e fhi = f return ehi - elo, (flo - fhi) / 2 def get_state_indices(self, state): """ Return the k-point index and band index of given state. parameters: ----------- state: 'H**O', or 'LUMO' H**O is the first state below Fermi-level. LUMO is the first state above Fermi-level. """ eigs = (self.get_eigenvalues() - self.get_fermi_level()).flatten() if state == "H**O": k, a = np.unravel_index(np.ma.masked_array(eigs, eigs > 0.0).argmax(), (self.st.nk, self.st.norb)) if state == "LUMO": k, a = np.unravel_index(np.ma.masked_array(eigs, eigs < 0.0).argmin(), (self.st.nk, self.st.norb)) return k, a def get_occupations(self): # self.solve_ground_state(atoms) return self.st.get_occupations() def get_band_structure_energy(self, atoms): if self.calculation_required(atoms, ["ebs"]): self.solve_ground_state(atoms) self.ebs = self.st.get_band_structure_energy() * Hartree self.el.set_solved("ebs") return self.ebs def get_coulomb_energy(self, atoms): if self.calculation_required(atoms, ["ecoul"]): self.solve_ground_state(atoms) self.ecoul = self.st.es.coulomb_energy() * Hartree self.st return self.ecoul # some not implemented ASE-assumed methods def get_fermi_level(self): """ Return the Fermi-energy (chemical potential) in eV. """ return self.st.occu.get_mu() * Hartree def set_atoms(self, atoms): """ Initialize the calculator for given atomic system. """ if self.init == True and atoms.get_chemical_symbols() != self.el.atoms.get_chemical_symbols(): raise RuntimeError( "Calculator initialized for %s. Create new calculator for %s." % (self.el.get_name(), mix.parse_name_for_atoms(atoms)) ) else: self._initialize(atoms) def get_occupation_numbers(self, kpt=0): """ Return occupation numbers for given k-point index. """ return self.st.f[kpt].copy() def get_number_of_bands(self): """ Return the total number of orbitals. """ return self.st.norb def start_timing(self, label): self.timer.start(label) def stop_timing(self, label): self.timer.stop(label) # # various analysis methods # def get_dielectric_function(self, width=0.05, cutoff=None, N=400): """ Return the imaginary part of the dielectric function for non-SCC. Note: Uses approximation that requires that the orientation of neighboring unit cells does not change much. (Exact for Bravais lattice.) See, e.g., Marder, Condensed Matter Physics, or Popov New J. Phys 6, 17 (2004) parameters: ----------- width: energy broadening in eV cutoff: cutoff energy in eV N: number of points in energy grid return: ------- e[:], d[:,0:2] """ self.start_timing("dielectric function") width = width / Hartree otol = 0.05 # tolerance for occupations if cutoff == None: cutoff = 1e10 else: cutoff = cutoff / Hartree st = self.st nk, e, f, wk = st.nk, st.e, st.f, st.wk ex, wt = [], [] for k in range(nk): wf = st.wf[k] wfc = wf.conjugate() dS = st.dS[k].transpose((0, 2, 1)) ek = e[k] fk = f[k] kweight = wk[k] # electron excitation ka-->kb; restrict the search: bmin = list(fk < 2 - otol).index(True) amin = list(ek > ek[bmin] - cutoff).index(True) amax = list(fk < otol).index(True) for a in xrange(amin, amax + 1): bmax = list(ek > ek[a] + cutoff).index(True) for b in range(max(a + 1, bmin), bmax + 1): de = ek[b] - ek[a] df = fk[a] - fk[b] if df < otol: continue # P = < ka | P | kb > P = 1j * hbar * np.dot(wfc[a], np.dot(dS, wf[b])) ex.append(de) wt.append(kweight * df * np.abs(P) ** 2) ex, wt = np.array(ex), np.array(wt) cutoff = min(ex.max(), cutoff) y = np.zeros((N, 3)) for d in range(3): # Lorenzian should be used, but long tail would bring divergence at zero energy x, y[:, d] = broaden(ex, wt[:, d], width, "gaussian", N=N, a=width, b=cutoff) y[:, d] = y[:, d] / x ** 2 const = 4 * np.pi ** 2 / hbar self.stop_timing("dielectric function") return x * Hartree, y * const # y also in eV, Ang # # grid stuff # def set_grid(self, h=0.2, cutoff=3.0): if self.calculation_required(self.el.atoms, ["energy"]): raise AssertionError("Electronic structure is not solved yet!") if self.flags["grid"] == False: self.gd = Grids(self, h, cutoff) self.flags["grid"] = True def get_grid_basis_orbital(self, I, otype, k=0, pad=True): """ Return basis orbital on grid. parameters: =========== I: atom index otype: orbital type ('s','px','py',...) k: k-point index (basis functions are really the extended Bloch functions for periodic systems) pad: padded edges in the array """ if self.flags["grid"] == False: raise AssertionError('Grid needs to be set first by method "set_grid".') return self.gd.get_grid_basis_orbital(I, otype, k, pad) def get_grid_wf(self, a, k=0, pad=True): """ Return eigenfunction on a grid. parameters: =========== a: state (band) index k: k-vector index pad: padded edges """ if self.flags["grid"] == False: raise AssertionError('Grid needs to be set first by method "set_grid".') return self.gd.get_grid_wf(a, k, pad) def get_grid_wf_density(self, a, k=0, pad=True): """ Return eigenfunction density. Density is not normalized; accurate quantitative analysis on this density are best avoided. parameters: =========== a: state (band) index k: k-vector index pad: padded edges """ if self.flags["grid"] == False: raise AssertionError('Grid needs to be set first by method "set_grid".') return self.gd.get_grid_wf_density(a, k, pad) def get_grid_density(self, pad=True): """ Return electron density on grid. Do not perform accurate analysis on this density. Integrated density differs from the total number of electrons. Bader analysis inaccurate. parameters: pad: padded edges """ if self.flags["grid"] == False: raise AssertionError('Grid needs to be set first by method "set_grid".') return self.gd.get_grid_density(pad) def get_grid_LDOS(self, bias=None, window=None, pad=True): """ Return electron density over selected states around the Fermi-level. parameters: ----------- bias: bias voltage (eV) with respect to Fermi-level. Negative means probing occupied states. window: 2-tuple for lower and upper bounds wrt. Fermi-level pad: padded edges """ if self.flags["grid"] == False: raise AssertionError('Grid needs to be set first by method "set_grid".') return self.gd.get_grid_LDOS(bias, window, pad) # # Mulliken population analysis tools # def _init_mulliken(self): """ Initialize Mulliken analysis. """ if self.calculation_required(self.el.atoms, ["energy"]): raise AssertionError("Electronic structure is not solved yet!") if self.flags["Mulliken"] == False: self.MA = MullikenAnalysis(self) self.flags["Mulliken"] = True def get_dq(self, atoms=None): """ Return atoms' excess Mulliken populations. The total populations subtracted by the numbers of valence electrons. """ self.solve_ground_state(atoms) return self.st.get_dq() def get_charges(self, atoms=None): """ Return atoms' electric charges (Mulliken). """ return -self.get_dq(atoms) def get_atom_mulliken(self, I): """ Return Mulliken population for atom I. This is the total population, without the number of valence electrons subtracted. parameters: =========== I: atom index """ self._init_mulliken() return self.MA.get_atom_mulliken(I) def get_basis_mulliken(self, mu): """ Return Mulliken population of given basis state. parameters: =========== mu: orbital index (see Elements' methods for indices) """ self._init_mulliken() return self.MA.get_basis_mulliken(mu) def get_basis_wf_mulliken(self, mu, k, a, wk=True): """ Return Mulliken population for given basis state and wavefunction. parameters: =========== mu: basis state index k: k-vector index a: eigenstate index wk: include k-point weight in the population? """ self._init_mulliken() return self.MA.get_basis_wf_mulliken(mu, k, a, wk) def get_atom_wf_mulliken(self, I, k, a, wk=True): """ Return Mulliken population for given atom and wavefunction. parameters: =========== I: atom index (if None, return an array for all atoms) k: k-vector index a: eigenstate index wk: embed k-point weight in population """ self._init_mulliken() return self.MA.get_atom_wf_mulliken(I, k, a, wk) def get_atom_wf_all_orbital_mulliken(self, I, k, a): """ Return orbitals' Mulliken populations for given atom and wavefunction. parameters: =========== I: atom index (returned array size = number of orbitals on I) k: k-vector index a: eigenstate index """ self._init_mulliken() return self.MA.get_atom_wf_all_orbital_mulliken(I, k, a) def get_atom_wf_all_angmom_mulliken(self, I, k, a, wk=True): """ Return atom's Mulliken populations for all angmom for given wavefunction. parameters: =========== I: atom index k: k-vector index a: eigenstate index wk: embed k-point weight into population return: array (length 3) containing s,p and d-populations """ self._init_mulliken() return self.MA.get_atom_wf_all_angmom_mulliken(I, k, a, wk) # # Densities of states methods # def _init_DOS(self): """ Initialize Density of states analysis. """ if self.calculation_required(self.el.atoms, ["energy"]): raise AssertionError("Electronic structure is not solved yet!") if self.flags["DOS"] == False: self.DOS = DensityOfStates(self) self.flags["DOS"] = True def get_local_density_of_states(self, projected=False, width=0.05, window=None, npts=501): """ Return state density for all atoms as a function of energy. parameters: =========== projected: return local density of states projected for angular momenta 0,1 and 2 (s,p and d) width: energy broadening (in eV) window: energy window around Fermi-energy; 2-tuple (eV) npts: number of grid points for energy return: projected==False: energy grid, ldos[atom,grid] projected==True: energy grid, ldos[atom, grid], pldos[atom, angmom, grid] """ self._init_DOS() return self.DOS.get_local_density_of_states(projected, width, window, npts) def get_density_of_states(self, broaden=False, projected=False, occu=False, width=0.05, window=None, npts=501): """ Return the full density of states. Sum of states over k-points. Zero is the Fermi-level. Spin-degeneracy is NOT counted. parameters: =========== broaden: * If True, return broadened DOS in regular grid in given energy window. * If False, return energies of all states, followed by their k-point weights. projected: project DOS for angular momenta occu: for not broadened case, return also state occupations width: Gaussian broadening (eV) window: energy window around Fermi-energy; 2-tuple (eV) npts: number of data points in output return: * if projected: e[:],dos[:],pdos[l,:] (angmom l=0,1,2) * if not projected: e[:],dos[:] * if broaden: e[:] is on regular grid, otherwise e[:] are eigenvalues and dos[...] corresponding weights * if occu: e[:],dos[:],occu[:] """ self._init_DOS() return self.DOS.get_density_of_states(broaden, projected, occu, width, window, npts) # Bonding analysis def _init_bonds(self): """ Initialize Mulliken bonding analysis. """ if self.calculation_required(self.el.atoms, ["energy"]): raise AssertionError("Electronic structure is not solved yet!") if self.flags["bonds"] == False: self.bonds = MullikenBondAnalysis(self) self.flags["bonds"] = True def get_atom_energy(self, I=None): """ Return the energy of atom I (in eV). Warning: bonding & atom energy analysis less clear for systems where orbitals overlap with own periodic images. parameters: =========== I: atom index. If None, return all atoms' energies as an array. """ self._init_bonds() return self.bonds.get_atom_energy(I) def get_mayer_bond_order(self, i, j): """ Return Mayer bond-order between two atoms. Warning: bonding & atom energy analysis less clear for systems where orbitals overlap with own periodic images. parameters: =========== I: first atom index J: second atom index """ self._init_bonds() return self.bonds.get_mayer_bond_order(i, j) def get_promotion_energy(self, I=None): """ Return atom's promotion energy (in eV). Defined as: E_prom,I = sum_(mu in I) [q_(mu) - q_(mu)^0] epsilon_mu parameters: =========== I: atom index. If None, return all atoms' energies as an array. """ self._init_bonds() return self.bonds.get_promotion_energy(I) def get_bond_energy(self, i, j): """ Return the absolute bond energy between atoms (in eV). Warning: bonding & atom energy analysis less clear for systems where orbitals overlap with own periodic images. parameters: =========== i,j: atom indices """ self._init_bonds() return self.bonds.get_bond_energy(i, j) def get_atom_and_bond_energy(self, i=None): """ Return given atom's contribution to cohesion. parameters: =========== i: atom index. If None, return all atoms' energies as an array. """ self._init_bonds() return self.bonds.get_atom_and_bond_energy(i) def get_covalent_energy(self, mode="default", i=None, j=None, width=None, window=None, npts=501): """ Return covalent bond energies in different modes. (eV) ecov is described in Bornsen, Meyer, Grotheer, Fahnle, J. Phys.:Condens. Matter 11, L287 (1999) and Koskinen, Makinen Comput. Mat. Sci. 47, 237 (2009) parameters: =========== mode: 'default' total covalent energy 'orbitals' covalent energy for orbital pairs 'atoms' covalent energy for atom pairs 'angmom' covalent energy for angular momentum components i,j: atom or orbital indices, or angular momentum pairs width: * energy broadening (in eV) for ecov * if None, return energy eigenvalues and corresponding covalent energies in arrays, directly window: energy window (in eV wrt Fermi-level) for broadened ecov npts: number of points in energy grid (only with broadening) return: ======= x,y: * if width==None, x is list of energy eigenvalues (including k-points) and y covalent energies of those eigenstates * if width!=None, x is energy grid for ecov. * energies (both energy grid and ecov) are in eV. Note: energies are always shifted so that Fermi-level is at zero. Occupations are not otherwise take into account (while k-point weights are) """ self._init_bonds() return self.bonds.get_covalent_energy(mode, i, j, width, window, npts) def add_pair_potential(self, i, j, v, eVA=True): """ Add pair interaction potential function for elements or atoms parameters: =========== i,j: * atom indices, if integers (0,1,2,...) * elements, if strings ('C','H',...) v: Pair potential function. Only one potential per element and atom pair allowed. Syntax: v(r,der=0), v(r=None) returning the interaction range in Bohr or Angstrom. eVA: True for v in eV and Angstrom False for v in Hartree and Bohr """ self.pp.add_pair_potential(i, j, v, eVA)
class EwaldSum(Coulomb): def __init__(self, accuracy_goal, weight, timer=None): self.accuracy_goal = accuracy_goal self.weight = weight if timer is None: self.timer = Timer('EwaldSum') else: self.timer = timer def update(self, a, q): """ Compute the electrostatic potential. Parameters: ----------- a: Hotbit Atoms object, or atoms object that implements the transform and rotation interface. q: Charges """ self.alpha = (self.weight*pi**3*len(a)/a.get_volume())**(1./3) self.sqrt_alpha = sqrt(self.alpha) self.G_cutoff = 2*sqrt(log(10.0)*self.accuracy_goal*self.alpha) self.r_cutoff = sqrt(log(10.0)*self.accuracy_goal/self.alpha) cell_cv = a.get_cell() rec_cell_vc = np.linalg.inv(cell_cv) r_av = a.get_positions() self.timer.start('reciprocal sum') # Reciprocal sum lx, ly, lz = np.sqrt(np.sum(rec_cell_vc**2, axis=0)) maxGx = int(self.G_cutoff/(2*pi*lx))+1 maxGy = int(self.G_cutoff/(2*pi*ly))+1 maxGz = int(self.G_cutoff/(2*pi*lz))+1 Gx = 2*pi * np.arange(-maxGx, maxGx+1).reshape(-1, 1, 1, 1) Gy = 2*pi * np.arange(-maxGy, maxGy+1).reshape( 1, -1, 1, 1) Gz = 2*pi * np.arange(-maxGz, maxGz+1).reshape( 1, 1, -1, 1) G = Gx*np.array([1,0,0])+Gy*np.array([0,1,0])+Gz*np.array([0,0,1]) G = np.dot(G, rec_cell_vc) si = np.sum( np.sin(np.tensordot(G, r_av, axes=(3,1)))*q, axis=3) co = np.sum( np.cos(np.tensordot(G, r_av, axes=(3,1)))*q, axis=3) G_sq = np.sum( G*G, axis=3 ) rec_G_sq = 1.0/G_sq rec_G_sq[maxGx, maxGy, maxGz] = 0.0 phase = np.tensordot(G, r_av, axes=(3, 1)) si.shape = ( 2*maxGx+1, 2*maxGy+1, 2*maxGz+1, 1 ) co.shape = ( 2*maxGx+1, 2*maxGy+1, 2*maxGz+1, 1 ) self.phi_a = np.sum( np.sum( np.sum( ( np.exp(-G_sq/(4*self.alpha))*rec_G_sq ).reshape(2*maxGx+1, 2*maxGy+1, 2*maxGz+1, 1) * ( si * np.sin(phase) + co * np.cos(phase) ), axis=0 ), axis=0 ), axis=0 ) self.phi_a *= 4*pi/a.get_volume() self.timer.stop('reciprocal sum') self.timer.start('real space sum') # Real space sum lx, ly, lz = np.sqrt(np.sum(cell_cv**2, axis=1)) maxrx = int(self.r_cutoff/lx)+1 maxry = int(self.r_cutoff/ly)+1 maxrz = int(self.r_cutoff/lz)+1 nat = len(a) r = a.get_positions() for x in range(-maxrx, maxrx+1): for y in range(-maxry, maxry+1): for z in range(-maxrz, maxrz+1): if x != 0 or y != 0 or z != 0: r1 = np.dot([x,y,z], cell_cv) dr = r.reshape(nat, 1, 3) - \ (r1+r).reshape(1, nat, 3) abs_dr = np.sqrt(np.sum(dr*dr, axis=2)) phi = q*erfc(self.sqrt_alpha*abs_dr)/abs_dr self.phi_a += np.sum(phi, axis=1) ## Self-contribution dr = r.reshape(nat, 1, 3) - r.reshape(1, nat, 3) abs_dr = np.sqrt(np.sum(dr*dr, axis=2)) ## Avoid divide by zero abs_dr[diag_indices_from(abs_dr)] = 1.0 phi = q*erfc(self.sqrt_alpha*abs_dr)/abs_dr phi[diag_indices_from(phi)] = 0.0 self.phi_a += np.sum(phi, axis=1) self.timer.stop('real space sum') # Self energy self.phi_a -= 2*q*sqrt(self.alpha/pi) def get_potential(self): """ Return the electrostatic potential for each atom. """ return self.phi_a ### For use as a standalone calculator ### Note: These functions assume eV/A units def get_potential_energy(self, a, q=None): if q is None: q = a.get_charges() self.update(a, q) return Hartree * Bohr * np.sum(q*self.phi_a)/2
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'