Beispiel #1
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 #2
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 #3
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 #4
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 #5
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 #6
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'