Beispiel #1
0
 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()
Beispiel #2
0
    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
Beispiel #3
0
    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()
Beispiel #4
0
 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()
Beispiel #5
0
    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
Beispiel #6
0
    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
Beispiel #7
0
    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()
Beispiel #8
0
    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)
Beispiel #9
0
    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
Beispiel #10
0
    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, "************************************************"
Beispiel #11
0
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
Beispiel #12
0
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()
Beispiel #13
0
    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
Beispiel #14
0
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)
Beispiel #15
0
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
Beispiel #16
0
    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
Beispiel #17
0
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()
Beispiel #18
0
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
Beispiel #19
0
    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
Beispiel #20
0
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
Beispiel #21
0
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
Beispiel #22
0
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)
Beispiel #23
0
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
Beispiel #24
0
    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
Beispiel #25
0
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
Beispiel #26
0
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
Beispiel #27
0
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)
Beispiel #28
0
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'