Пример #1
0
    def __init__(self,symbol1,symbol2,r_cut,s=None,k=3,txt=None,tol=0.005):
        """
        Class for fitting the short-range repulsive potential.


        Fitting uses eV and Angstrom also internally, only the
        output file (.par) is in Hartrees and Bohrs. The weights
        used in the methods append_* are the inverse of standard
        deviations (weight=1/sigma). For more details of the
        approach used here, look at Pekka Koskinen and Ville Makinen,
        Computational Materials Science 47, 237 (2009), page 244.

        Parameters:
        ===========
        symbol1:        chemical symbol for the first element
        symbol2:        chemical symbol for the second element
        txt:            output filename or None for stdout
        r_cut:          the repulsion cutoff
        s:              smoothing parameter. If None, use s = N - np.sqrt(2*N)
                        where N is the number of data points.
        k:              order of spline for V_rep'(R), cubic by default.
                        Uses smaller order if not enough points to fit V_rep'(R)
        tol:            tolerance for distances still considered the same


        Usage:
        ======
        1. Initialize class
           * rep = RepulsiveFitting('Au','Au',r_cut=3.3,s=100)

        2. Collect data from structures. The data collected is points r_i
           and V_rep'(r_i), that is, repulsive distance and the force.
           Use the append_* methods.
           * rep.append_dimer(weight=0.5,calc=calc0,R=2.49,comment='Au2')
           * rep.append_energy_curve(weight=1.0,calc=calc0,
                 traj='dimer_curve.traj',label='DFT dimer',comment='dimer curve')

        3. Given the set of points [r_i,V_rep'(r_i)], fit a spline with given order.
           * fit()
           Fitting will produce a spline-interpolated V_rep'(r), which is then integrated
           to given spline-interpolated V_rep(r).

        4. Output repulsion into a file and plot the repulsion
           * rep.write_par('Au_Au_no_repulsion.par',filename='Au_Au_repulsion.par')
           * rep.plot('AuAu_repulsion.pdf')

        """
        self.elm1=Element(symbol1)
        self.elm2=Element(symbol2)
        self.sym1=self.elm1.get_symbol()
        self.sym2=self.elm2.get_symbol()
        self.r_cut = r_cut
        self.s = s
        self.k = k
        self.tol = tol

        self.r_dimer = None
        self.deriv=[]
        self.comments=''
        self.scale=1.025                    # scaling factor for scalable systems
        self.structures = []
        self.v=None

        if txt==None:
            self.txt=stdout
        else:
            self.txt=open(txt,'a')
        print>>self.txt, 'Fitting repulsion curve between %s and %s' % (self.sym1, self.sym2)
        self.colors = ['red','green','blue','cyan','yellow','orange','magenta','pink','black']
        self.colori = 0
Пример #2
0
    def __init__(self,symbol1,symbol2,r_cut,s=None,k=3,txt=None,tol=0.005):
        """
        Class for fitting the short-range repulsive potential.


        Fitting uses eV and Angstrom also internally, only the
        output file (.par) is in Hartrees and Bohrs. The weights
        used in the methods append_* are the inverse of standard
        deviations (weight=1/sigma). For more details of the
        approach used here, look at Pekka Koskinen and Ville Makinen,
        Computational Materials Science 47, 237 (2009), page 244.

        Parameters:
        ===========
        symbol1:        chemical symbol for the first element
        symbol2:        chemical symbol for the second element
        txt:            output filename or None for stdout
        r_cut:          the repulsion cutoff
        s:              smoothing parameter. If None, use s = N - np.sqrt(2*N)
                        where N is the number of data points.
        k:              order of spline for V_rep'(R), cubic by default.
                        Uses smaller order if not enough points to fit V_rep'(R)
        tol:            tolerance for distances still considered the same


        Usage:
        ======
        1. Initialize class
           * rep = RepulsiveFitting('Au','Au',r_cut=3.3,s=100)

        2. Collect data from structures. The data collected is points r_i
           and V_rep'(r_i), that is, repulsive distance and the force.
           Use the append_* methods.
           * rep.append_dimer(weight=0.5,calc=calc0,R=2.49,comment='Au2')
           * rep.append_energy_curve(weight=1.0,calc=calc0,
                 traj='dimer_curve.traj',label='DFT dimer',comment='dimer curve')

        3. Given the set of points [r_i,V_rep'(r_i)], fit a spline with given order.
           * fit()
           Fitting will produce a spline-interpolated V_rep'(r), which is then integrated
           to given spline-interpolated V_rep(r).

        4. Output repulsion into a file and plot the repulsion
           * rep.write_par('Au_Au_no_repulsion.par',filename='Au_Au_repulsion.par')
           * rep.plot('AuAu_repulsion.pdf')

        """
        self.elm1=Element(symbol1)
        self.elm2=Element(symbol2)
        self.sym1=self.elm1.get_symbol()
        self.sym2=self.elm2.get_symbol()
        self.r_cut = r_cut
        self.s = s
        self.k = k
        self.tol = tol

        self.r_dimer = None
        self.deriv=[]
        self.comments=''
        self.scale=1.025                    # scaling factor for scalable systems
        self.structures = []
        self.v=None

        if txt==None:
            self.txt=stdout
        else:
            self.txt=open(txt,'a')
        print('Fitting repulsion curve between %s and %s' % (self.sym1, self.sym2), file=self.txt)
        self.colors = ['red','green','blue','cyan','yellow','orange','magenta','pink','black']
        self.colori = 0
Пример #3
0
class RepulsiveFitting:

    def __init__(self,symbol1,symbol2,r_cut,s=None,k=3,txt=None,tol=0.005):
        """
        Class for fitting the short-range repulsive potential.


        Fitting uses eV and Angstrom also internally, only the
        output file (.par) is in Hartrees and Bohrs. The weights
        used in the methods append_* are the inverse of standard
        deviations (weight=1/sigma). For more details of the
        approach used here, look at Pekka Koskinen and Ville Makinen,
        Computational Materials Science 47, 237 (2009), page 244.

        Parameters:
        ===========
        symbol1:        chemical symbol for the first element
        symbol2:        chemical symbol for the second element
        txt:            output filename or None for stdout
        r_cut:          the repulsion cutoff
        s:              smoothing parameter. If None, use s = N - np.sqrt(2*N)
                        where N is the number of data points.
        k:              order of spline for V_rep'(R), cubic by default.
                        Uses smaller order if not enough points to fit V_rep'(R)
        tol:            tolerance for distances still considered the same


        Usage:
        ======
        1. Initialize class
           * rep = RepulsiveFitting('Au','Au',r_cut=3.3,s=100)

        2. Collect data from structures. The data collected is points r_i
           and V_rep'(r_i), that is, repulsive distance and the force.
           Use the append_* methods.
           * rep.append_dimer(weight=0.5,calc=calc0,R=2.49,comment='Au2')
           * rep.append_energy_curve(weight=1.0,calc=calc0,
                 traj='dimer_curve.traj',label='DFT dimer',comment='dimer curve')

        3. Given the set of points [r_i,V_rep'(r_i)], fit a spline with given order.
           * fit()
           Fitting will produce a spline-interpolated V_rep'(r), which is then integrated
           to given spline-interpolated V_rep(r).

        4. Output repulsion into a file and plot the repulsion
           * rep.write_par('Au_Au_no_repulsion.par',filename='Au_Au_repulsion.par')
           * rep.plot('AuAu_repulsion.pdf')

        """
        self.elm1=Element(symbol1)
        self.elm2=Element(symbol2)
        self.sym1=self.elm1.get_symbol()
        self.sym2=self.elm2.get_symbol()
        self.r_cut = r_cut
        self.s = s
        self.k = k
        self.tol = tol

        self.r_dimer = None
        self.deriv=[]
        self.comments=''
        self.scale=1.025                    # scaling factor for scalable systems
        self.structures = []
        self.v=None

        if txt==None:
            self.txt=stdout
        else:
            self.txt=open(txt,'a')
        print>>self.txt, 'Fitting repulsion curve between %s and %s' % (self.sym1, self.sym2)
        self.colors = ['red','green','blue','cyan','yellow','orange','magenta','pink','black']
        self.colori = 0


    def __call__(self,r,der=0):
        """
        Return repulsion or its derivative.

        This is the already fitted and integrated V_rep(R).

        parameters:
        ===========
        r:            radius (Angstroms)
        der:          der=0 for V_rep(r)
                      der=1 for V_rep'(r)
        """
        if self.v==None:
            raise AssertionError('Repulsion is not yet fitted')
        return self.v(r,der=der)

    def get_range(self):
        """
        Return rmin,rmax for fitting points.
        """
        r = np.array([s[0] for s in self.deriv])
        return r.min(), r.max()

        self.deriv

    def plot(self, filename=None):
        """
        Plot vrep and derivative together with fit info.

        parameters:
        ===========
        filename:     graphics output file name
        """
        try:
            import pylab as pl
        except:
            raise AssertionError('pylab could not be imported')
        r=np.linspace(0,self.r_cut)
        v=[self(x,der=0) for x in r]
        vp=[self(x,der=1) for x in r]
        rmin=0.95*min([d[0] for d in self.deriv])
        rmax = 1.1*self.r_cut

        fig=pl.figure()
        pl.subplots_adjust(wspace=0.25)

        # Vrep
        pl.subplot(1,2,1)
        pl.ylabel(r'$V_{rep}(r)$  (eV)')
        pl.xlabel(r'$r$  ($\AA$)')
        if self.r_dimer!=None:
            pl.axvline(x=self.r_dimer,c='r',ls=':')
        pl.axvline(x=self.r_cut,c='r',ls=':')
        pl.plot(r,v)
        pl.ylim(ymin=0,ymax=self(rmin))
        pl.xlim(xmin=rmin, xmax=self.r_cut)

        # Vrep'
        pl.subplot(1,2,2)
        pl.ylabel(r'$dV_{rep}(r)/dr$ (eV/$\AA$)')
        pl.xlabel(r'$r$ ($\AA$)')
        pl.plot(r,vp,label=r'$dV_{rep}(r)/dr$')
        for s in self.deriv:
            pl.scatter( [s[0]],[s[1]],s=100*s[2],c=s[3],label=s[4])
        pl.axvline(x=self.r_cut,c='r',ls=':')
        if self.r_dimer!=None:
            pl.axvline(x=self.r_dimer,c='r',ls=':')

        ymin = 0
        for point in self.deriv:
            if rmin<=point[0]<=rmax: ymin = min(ymin,point[1])
        ymax = np.abs(ymin)*0.1
        pl.axhline(0,ls='--',c='k')
        if self.r_dimer!=None:
            pl.text(self.r_dimer, ymax, r'$r_{dimer}$')
        pl.text(self.r_cut, ymax, r'$r_{cut}$')
        pl.xlim(xmin=rmin, xmax=rmax)
        pl.ylim(ymin=ymin, ymax=ymax)
        #pl.subtitle('Fitting for %s and %s' % (self.sym1, self.sym2))
        pl.rc('font', size=10)
        pl.rc('legend',fontsize=8)
        pl.legend(loc=4)
        file = '%s_%s_repulsion.pdf' % (self.sym1, self.sym2)
        if filename!=None:
            file=filename
        pl.savefig(file)
        pl.clf()


    def add_comment(self,s=None):
        """
        Append some comment for par-file.

        These comments will end up in Hotbit's output logfile each time
        fitted repulsion is used in calculations. For this reason,
        use as short and concise comments as possible.

        parameters:
        ===========
        s:            comment as a string
        """
        if s in [None, '']:
            return
        add='|'
        if len(self.comments)==0:
            add=''
        self.comments+=add+s


    def fit(self):
        """
        Fit spline into {r, V_rep'(r)} -data points.
        """
        from scipy.interpolate import splrep, splev
        self.k = min(len(self.deriv),self.k)

        x = np.array([self.deriv[i][0] for i in range(len(self.deriv))])
        y = np.array([self.deriv[i][1] for i in range(len(self.deriv))])
        w = np.array([self.deriv[i][2] for i in range(len(self.deriv))])
        # sort values so that x is in ascending order
        indices = x.argsort()
        x, y, w = x[indices], y[indices], w[indices]
        x, y, w = self._group_closeby_points(x,y,w)
        # use only points that are closer than r_cut
        indices = np.where(x < self.r_cut)
        x, y, w = list(x[indices]), list(y[indices]), list(w[indices])
        # force the spline curve to go to zero at x=r_cut
        x.append(self.r_cut)
        y.append(0.0)
        w.append(1E3*max(w))
        if self.s == None:
            # from documentation of splrep in scipy.interpolate.fitpack
            self.s = len(x) - np.sqrt(2*len(x))

        print>>self.txt, "\nFitting spline for V_rep'(R) with parameters"
        print>>self.txt, "  k=%i, s=%0.4f, r_cut=%0.4f\n" %(self.k, self.s, self.r_cut)
        tck = splrep(x, y, w, s=self.s, k=self.k)

        def dv_rep(r):
            return splev(r, tck)

        v_rep = self._integrate_vrep(dv_rep, self.r_cut)

        def potential(r, der=0):
            if der == 0:
                return v_rep(r)
            elif der == 1:
                return dv_rep(r)
            else:
                raise NotImplementedError("Only 0th and 1st derivatives")
        self.v = potential


    def _group_closeby_points(self, x, y, w):
        """
        If there are many y-values with almost the same x-values,
        it is impossible to make spline fit to these points.
        For these points the y will be the weighted average of
        the y-points and the weight is the sum of the weights of
        averaged y-points.
        """
        accuracy = 4 # the number of decimals to maintain
        pseudo_x = np.array(x*10**accuracy, dtype=int)
        groups = np.zeros(len(x), dtype=int)
        g = 0
        for i in range(1,len(pseudo_x)):
            if pseudo_x[i] != pseudo_x[i-1]:
                groups[i] = groups[i-1] + 1
            else:
                groups[i] = groups[i-1]
        new_x = []
        new_y = []
        new_w = []
        for g in range(max(groups)+1):
            same = np.where(groups == g)
            new_x.append(np.average(x[same]))
            new_y.append(np.dot(y[same],w[same])/np.sum(w[same]))
            new_w.append(np.sum(w[same]))
        return np.array(new_x), np.array(new_y), np.array(new_w)


    def write_par(self, inputpar, filename=None):
        """
        Write the full par-file to file.

        parameters:
        ===========
        inputpar:     the par-file where the repulsion is appended
        filename:     output file
        """
        from time import asctime
        import shutil

        if filename==None:
            filename = 'repulsion_'+inputpar

        shutil.copy(inputpar, filename)
        f = open(filename, 'a')
        # add comments
        print >> f, "repulsion_comment="
        print >> f, "%s\nparameters r_cut = %0.4f Ang, s = %0.4f, k = %3i" % (asctime(),self.r_cut, self.s, self.k)
        if len(self.structures)>1:
            print >> f, "The systems used to produce this fit:"
            for data in self.structures:
                print >> f, "%20s %3s" % (data['filename'], data['charge'])
        if len(self.comments) > 0:
            print >> f, self.comments
        print >> f, '\n\nrepulsion='
        for r in np.linspace(0.1, self.r_cut, 100):
            print >> f, r/Bohr, self(r)/Hartree
        f.close()


    def _integrate_vrep(self, dv_rep, r_cut, N=100):
        """
        Integrate V'_rep(r) from r_cut to zero to get the V_rep(r)
        """
        from box.interpolation import SplineFunction
        from scipy.integrate import quadrature
        r_g = np.linspace(r_cut, 0, N)
        dr = r_g[1] - r_g[0]
        v_rep = np.zeros(N)
        for i in range(1,len(r_g)):
            v_rep[i] = v_rep[i-1]
            val, err = quadrature(dv_rep, r_g[i-1], r_g[i], tol=1.0e-12, maxiter=50)
            v_rep[i] += val
        # SplineFunction wants the x-values in ascending order
        return SplineFunction(r_g[::-1], v_rep[::-1])


    def _set_calc(self,atoms,calc):
        """
        Set calculator for given atoms.
        """
        if type(atoms)==type(''):
            a = read(atoms)
        else:
            a = atoms.copy()
        c = copy(calc)
        a.set_calculator(c)
        return a,c


    def _get_color(self,color):
        """ Get next color in line if color==None """
        if color==None:
            index = self.colori
            self.colori +=1
            if self.colori == len(self.colors): self.colors=0
            return self.colors[index]
        else:
            return color

    #
    #       Fitting methods
    #
    def append_point(self,weight,R,dvrep,comment=None,label=None,color='g'):
        """
        Add point to vrep'-fitting.

        parameters:
        ===========
        weight:    fitting weight
        R:         radius (Angstroms)
        dvrep:     V_rep'(R) (eV/Angstroms)
        comment:   fitting comment for par file (replaced by label if None)
        label:     plotting label (replaced by comment if None)
        """
        if comment==None: comment=label
        if label==None: label=comment
        self.deriv.append([R,dvrep,weight,color,label])
        if comment!='_nolegend_':
            self.add_comment(comment)
        return R,dvrep,weight


    def append_scalable_system(self,weight,calc,atoms,comment=None,label=None,color=None):
        """
        Use scalable equilibrium system in repulsion fitting.

        Scalable means that atoms is an equilibrium system, which
        has only given bond lengths R, and whose dimensions can be
        scaled E_DFT(R), and, because of equilibrium, E_DFT'(R)=0.
        Hence

            E_DFT'(R) = E_wr'(R) + N*V_rep'(R) = 0

            ==> V_rep'(R) = -E_wr'(R)/N

        where E_wr = E_bs + E_coul is the DFTB energy without
        repulsion.

        parameters:
        ===========
        weight:        fitting weight
        calc:          Hotbit calculator (remember charge and k-points)
        atoms:         filename or ase.Atoms instance
        comment:       fitting comment for par file (replaced by label if None)
        label:         plotting label (replaced by comment if None)
        color:         plotting color
        """
        atoms, calc = self._set_calc(atoms,calc)
        if comment==None: comment=label
        if label==None: label=comment

        e1 = atoms.get_potential_energy()
        R, N = self._get_repulsion_distances(calc)
        atoms.set_cell( atoms.get_cell()*self.scale, scale_atoms=True )
        e2 = atoms.get_potential_energy()

        dEwr=(e2-e1)/(self.scale*R-R)
        color = self._get_color(color)
        comment += ';w=%.1f' %weight
        self.append_point(weight,R,-dEwr/N,comment,label,color)
        print>>self.txt, '\nAdding a scalable system %s with %i bonds at R=%.4f.' %(atoms.get_chemical_symbols(),N,R)


    def append_dimer(self,weight,calc,R,comment=None,label='dimer',color=None):
        """
        Use dimer bond length in fitting.

        parameters:
        ===========
        weight:    fitting weight
        calc:      Hotbit calculator used in calculation
                   (remember Gamma-point and charge)
        R:         dimer bond length (Angstroms)
        comment:   fitting comment for par-file (replaced by label if None)
        label:     plotting label (replaced by comment if None)
        color:     plotting color
        """
        if comment==None: comment=label
        self.r_dimer = R
        atoms = Atoms([self.sym1,self.sym2],[(0,0,0),(R,0,0)],pbc=False)
        atoms.center(vacuum=5)
        color = self._get_color(color)
        self.append_scalable_system(weight,calc,atoms,comment=comment,label=label,color=color)


    def append_equilibrium_trajectory(self,weight,calc,traj,comment=None,label=None,color=None):
        """
        Calculates the V'rep(r) from a given equilibrium trajectory.

        The trajectory is set of three (or more, albeit not necessary) frames
        where atoms move near their equilibrium structure. To first approximation,
        the energies of these frames ARE THE SAME. This method is then
        equivalent to append_energy_curve method for given trajectory, with a flat
        energy curve.
        * Atoms should move as parallel to the fitted bonds as possible.
        * Amplitude should be small enough (say, 0.01 Angstroms)


        parameters:
        ===========
        weight:              fitting weight
        calc:                Hotbit calculator (remember charge and k-points)
        traj:                filename for ASE trajectory (energies need not be defined)
        comment:             fitting comment for par-file (replaced by comment if None)
        label:               plotting label (replaced by comment if None)
        color:               plotting color
        """
        traj1 = PickleTrajectory(traj)
        atoms2 = traj1[0].copy()
        calc2 = NullCalculator()
        atoms2.set_calculator(calc2)
        tmpfile = '_tmp.traj'
        traj2 = PickleTrajectory(tmpfile,'w',atoms2)
        for atoms1 in traj1:
            atoms2.set_positions(atoms1.get_positions())
            atoms2.set_cell( atoms1.get_cell() )
            atoms2.get_potential_energy()
            traj2.write()
        traj2.close()

        self.append_energy_curve(weight,calc,tmpfile,comment,label,color)
        os.remove(tmpfile)
        if os.path.isfile(tmpfile+'.bak'):
            os.remove(tmpfile+'.bak')

    def append_energy_slope(self,weight,p,dEdp,p0,calc,traj,comment=None,label=None,color=None):
        """
        Calculates the V'rep(r) at one point using trajectory over parameters p.

        Trajectory is calculated using parameters p, giving E(p), where E is the total energy
        without Vrep(r). The pair distance R=R(p). At p=p0, we set dE/dp|p=p0=dEdp, from which
        we can set V'rep(R(p)) as

                    dEdp - dE/dp(p0)
        V'rep(r) = -------------------
                      N * dR/dp

        parameters:
        ===========
        weight:              fitting weight
        p:                   parameter list
        dEdp:                slope of energy at p0
        p0:                  the point where energy slope is set
        calc:                Hotbit calculator (remember charge and k-points)
        traj:                filename for ASE trajectory, or PickleTrajectory
                             object
        comment:             fitting comment for par-file (replaced by comment if None)
        label:               plotting label (replaced by comment if None)
        color:               plotting color
        """
        raise NotImplementedError('This method was never tested properly.')
        from box.interpolation import SplineFunction
        R, E, N = [], [], []
        for atoms in traj:
            a, c = self._set_calc(atoms,calc)
            e = a.get_potential_energy()
            r, n = self._get_repulsion_distances(c)
            if n>0 and r<self.r_cut:
                E.append( atoms.get_potential_energy() )
                R.append(r)
                N.append(n)

        R,E,N = np.array(R), np.array(E), np.array(N)
        if np.any(N[0]!=N):
            raise NotImplementedError('The number of bonds changes during trajectory; check implementation.')

        Ef = SplineFunction(p,E)
        Rf = SplineFunction(p,R)

        color = self._get_color(color)
        comment += ';w=%.1f;N=%i' %(weight,N[0])
        return self.append_point(weight,Rf(p0),(dEdp-Ef(p0,der=1))/(N[0]*Rf(p0,der=1)),comment,label,color)


    def append_energy_curve(self,weight,calc,traj,comment=None,label=None,color=None):
        """
        Calculates the V'rep(r) from a given ase-trajectory.

        The trajectory can be anything, as long as the ONLY missing energy
        from DFTB calculation is N*V_rep(R). Hence

            E_DFT(R) = E_wr(R) + N*V_rep(R)

                         E_DFT'(R) - E_wr'(R)
            V_rep'(R) =  ------------------ ,
                                N

        where R is the nn. distance,N is the number of A-B pairs taken into account,
        and E_wr(R) = E_bs(R) + E_coul(R) is the DFTB energy without repulsion.
        At least 3 points in energy curve needed, preferably more.

        parameters:
        ===========
        weight:              fitting weight
        calc:                Hotbit calculator (remember charge and k-points)
        traj:                filename for ASE trajectory, or PickleTrajectory
                             object
        comment:             fitting comment for par-file (replaced by comment if None)
        label:               plotting label (replaced by comment if None)
        color:               plotting color
        """
        if comment==None: comment=label
        if label==None: label=comment

        #if not ( isinstance(traj, type(PickleTrajectory)) or isinstance(traj, list) ):
        if not ( isinstance(traj, type(PickleTrajectory)) or isinstance(traj, list) ):
            print>>self.txt, "\nAppending energy curve data from %s..." %traj
            traj = PickleTrajectory(traj)
        else:
            print>>self.txt, '\nAppending energy curve data...'
        Edft, Ewr, N, R = [], [], [], []
        if len(traj)<3:
            raise AssertionError('At least 3 points in energy curve required.')
        for atoms in traj:
            a, c = self._set_calc(atoms,calc)
            e = a.get_potential_energy()
            r, n = self._get_repulsion_distances(c)
            if n>0 and r<self.r_cut:
                Edft.append( atoms.get_potential_energy() )
                Ewr.append( e )
                R.append(r)
                N.append(n)
        Edft = np.array(Edft)
        Ewr = np.array(Ewr)
        N = np.array(N)
        R = np.array(R)

        if np.any( N-N[0]!=0 ):
            raise RuntimeError('The number of bonds changes within trajectory %s.' %traj[0].get_name())

        # sort radii because of spline
        ind = R.argsort()
        R    = R[ind]
        Edft = Edft[ind]
        Ewr  = Ewr[ind]
        from box.interpolation import SplineFunction
        k = min(len(Edft)-2,3)
        vrep = SplineFunction(R, (Edft-Ewr)/N, k=k, s=0)

        color = self._get_color(color)
        for i, r in enumerate(R):
            if i==0:
                com = comment + ';w=%.1f' %weight
            else:
                label='_nolegend_'
                com = None
            self.append_point(weight/np.sqrt(len(R)),r, vrep(r,der=1), com, label, color)
        print>>self.txt, "Appended %i points around R=%.4f...%.4f" %(len(N),R.min(),R.max())


    def append_homogeneous_cluster(self,weight,calc,atoms,comment=None,label=None,color=None):
        """
        Use homonuclear cluster in fitting, even with different bond lengths.

        Construct repulsive forces so that residual forces F_DFT-(F_wr+F_rep),
        where F_DFT are DFT forces (zero if cluster in equilibrium), F_wr are
        DFTB forces without repulsion, and F_rep are the repulsive forces.
        That is, we minimize the function

           sum_i |F_DFT_i - F_WR_i - F_rep_i|^2

        with respect a and b, where V_rep'(R) = a + b*(r-r_cut). Then, add fitting points
        from rmin to rmax, where these values span all pair distances below r_cut
        within the cluster.

        Only finite, non-periodic systems can be used.

        parameters:
        ===========
        weight:        fitting weight
        calc:          Hotbit calculator (remember charge and no k-points)
        atoms:         filename or ASE.Atoms instance
        comment:       fitting comment for par-file (replaced by label if None)
        label:         plotting label (replaced by comment if None)
        color:         plotting color
        """
        import numpy as np
        if comment==None: comment=label
        if label==None: label=comment
        if type(atoms)==type(''):
            atoms = read(atoms)

        N = len(atoms)
        try:
            f_DFT = atoms.get_forces()
            print>>self.txt, "    Use forces"
        except:
            f_DFT = np.zeros((N,3))
            print>>self.txt, "    No forces (equilibrium cluster)"

        atoms, calc = self._set_calc(atoms,calc)
        print>>self.txt, "\nAppending homogeneous cluster %s..." % atoms.get_name()

        f_wr = atoms.get_forces()
        distances = calc.rep.get_repulsion_distances(self.sym1,self.sym2,self.r_cut)
        rmin, rmax = distances.min(), distances.max()

        def dvrep(r,p):
            """ Auxiliary first-order polynomial for repulsion derivative """
            return p[0]+p[1]*(r-self.r_cut)


        def to_minimize(p,atoms,fdft,fwr):
            """ Function sum_I |F_DFT_I - F_TB_I|^2 to minimize. """
            N = len(atoms)
            pos = atoms.get_positions()
            resid = np.zeros((N,3))
            frep  = np.zeros((N,3))
            for i in range(N):
                for j in range(N):
                    if i==j: continue
                    rij = pos[j]-pos[i]
                    dij = np.linalg.norm(rij)
                    if dij>self.r_cut:
                        continue
                    else:
                        frep[i] += dvrep(dij,p)*rij/dij
            resid = fdft - ( fwr + frep )
            return sum([ np.linalg.norm(resid[i])**2 for i in range(N) ])

        from scipy.optimize import fmin
        p = fmin( to_minimize,[-1.0,5.0],args=(atoms,f_DFT,f_wr),xtol=1E-5,ftol=1E-5 )
        print>>self.txt, '   Cluster: V_rep(R)=%.6f + %.6f (r-%.2f)' %(p[0],p[1],self.r_cut)

        color = self._get_color(color)
        npp = 6
        rlist = np.linspace(rmin,rmax,npp)
        for i,r in enumerate(rlist):
            if i==0:
                com = comment
                com += ';w=%.1f' %weight
            else:
                label = '_nolegend_'
                com = None
            self.append_point(weight/np.sqrt(npp), r, dvrep(r,p), com, label, color)


    def _get_repulsion_distances(self,calc):
        """
        Return distances below r_cut for given system in calculator.

        return:
        =======
        R:     the mean repulsion distance
        N:     number of bonds
        """
        distances = calc.rep.get_repulsion_distances(self.sym1,self.sym2,self.r_cut)
        if len(distances)==0:
            return 0.0,distances
        R = distances.mean()
        rmin, rmax = distances.min(), distances.max()
        if  rmax - rmin > self.tol:
            atoms = calc.get_atoms()
            raise AssertionError('Bond lengths in %s are not the same, they vary between %.6f ... %.6f' %(atoms.get_name(),rmin,rmax) )
        N = len(distances)
        return R,N


    def write_fitting_data(self, filename, pickle=True):
        f = open(filename,'w')
        if pickle:
            import pickle
            pickle.dump(self.deriv, f)
            pickle.dump(self.structures, f)
            pickle.dump(self.comments, f)
        else:
            print>>f, self.deriv
            print>>f, self.structures
            print>>f, self.comments
        f.close()


    def load_fitting_data(self, filename):
        import pickle
        f = open(filename,'r')
        self.deriv = pickle.load(f)
        self.structures = pickle.load(f)
        self.comments = pickle.load(f)
        f.close()


    def _get_trajs_for_fitting(self):
        return self.structures
Пример #4
0
class RepulsiveFitting:

    def __init__(self,symbol1,symbol2,r_cut,s=None,k=3,txt=None,tol=0.005):
        """
        Class for fitting the short-range repulsive potential.


        Fitting uses eV and Angstrom also internally, only the
        output file (.par) is in Hartrees and Bohrs. The weights
        used in the methods append_* are the inverse of standard
        deviations (weight=1/sigma). For more details of the
        approach used here, look at Pekka Koskinen and Ville Makinen,
        Computational Materials Science 47, 237 (2009), page 244.

        Parameters:
        ===========
        symbol1:        chemical symbol for the first element
        symbol2:        chemical symbol for the second element
        txt:            output filename or None for stdout
        r_cut:          the repulsion cutoff
        s:              smoothing parameter. If None, use s = N - np.sqrt(2*N)
                        where N is the number of data points.
        k:              order of spline for V_rep'(R), cubic by default.
                        Uses smaller order if not enough points to fit V_rep'(R)
        tol:            tolerance for distances still considered the same


        Usage:
        ======
        1. Initialize class
           * rep = RepulsiveFitting('Au','Au',r_cut=3.3,s=100)

        2. Collect data from structures. The data collected is points r_i
           and V_rep'(r_i), that is, repulsive distance and the force.
           Use the append_* methods.
           * rep.append_dimer(weight=0.5,calc=calc0,R=2.49,comment='Au2')
           * rep.append_energy_curve(weight=1.0,calc=calc0,
                 traj='dimer_curve.traj',label='DFT dimer',comment='dimer curve')

        3. Given the set of points [r_i,V_rep'(r_i)], fit a spline with given order.
           * fit()
           Fitting will produce a spline-interpolated V_rep'(r), which is then integrated
           to given spline-interpolated V_rep(r).

        4. Output repulsion into a file and plot the repulsion
           * rep.write_par('Au_Au_no_repulsion.par',filename='Au_Au_repulsion.par')
           * rep.plot('AuAu_repulsion.pdf')

        """
        self.elm1=Element(symbol1)
        self.elm2=Element(symbol2)
        self.sym1=self.elm1.get_symbol()
        self.sym2=self.elm2.get_symbol()
        self.r_cut = r_cut
        self.s = s
        self.k = k
        self.tol = tol

        self.r_dimer = None
        self.deriv=[]
        self.comments=''
        self.scale=1.025                    # scaling factor for scalable systems
        self.structures = []
        self.v=None

        if txt==None:
            self.txt=stdout
        else:
            self.txt=open(txt,'a')
        print('Fitting repulsion curve between %s and %s' % (self.sym1, self.sym2), file=self.txt)
        self.colors = ['red','green','blue','cyan','yellow','orange','magenta','pink','black']
        self.colori = 0


    def __call__(self,r,der=0):
        """
        Return repulsion or its derivative.

        This is the already fitted and integrated V_rep(R).

        parameters:
        ===========
        r:            radius (Angstroms)
        der:          der=0 for V_rep(r)
                      der=1 for V_rep'(r)
        """
        if self.v==None:
            raise AssertionError('Repulsion is not yet fitted')
        return self.v(r,der=der)

    def get_range(self):
        """
        Return rmin,rmax for fitting points.
        """
        r = np.array([s[0] for s in self.deriv])
        return r.min(), r.max()

        self.deriv

    def plot(self, filename=None):
        """
        Plot vrep and derivative together with fit info.

        parameters:
        ===========
        filename:     graphics output file name
        """
        try:
            import pylab as pl
        except:
            raise AssertionError('pylab could not be imported')
        r=np.linspace(0,self.r_cut)
        v=[self(x,der=0) for x in r]
        vp=[self(x,der=1) for x in r]
        rmin=0.95*min([d[0] for d in self.deriv])
        rmax = 1.1*self.r_cut

        fig=pl.figure()
        pl.subplots_adjust(wspace=0.25)

        # Vrep
        pl.subplot(1,2,1)
        pl.ylabel(r'$V_{rep}(r)$  (eV)')
        pl.xlabel(r'$r$  ($\AA$)')
        if self.r_dimer!=None:
            pl.axvline(x=self.r_dimer,c='r',ls=':')
        pl.axvline(x=self.r_cut,c='r',ls=':')
        pl.plot(r,v)
        pl.ylim(ymin=0,ymax=self(rmin))
        pl.xlim(xmin=rmin, xmax=self.r_cut)

        # Vrep'
        pl.subplot(1,2,2)
        pl.ylabel(r'$dV_{rep}(r)/dr$ (eV/$\AA$)')
        pl.xlabel(r'$r$ ($\AA$)')
        pl.plot(r,vp,label=r'$dV_{rep}(r)/dr$')
        for s in self.deriv:
            pl.scatter( [s[0]],[s[1]],s=100*s[2],c=s[3],label=s[4])
        pl.axvline(x=self.r_cut,c='r',ls=':')
        if self.r_dimer!=None:
            pl.axvline(x=self.r_dimer,c='r',ls=':')

        ymin = 0
        for point in self.deriv:
            if rmin<=point[0]<=rmax: ymin = min(ymin,point[1])
        ymax = np.abs(ymin)*0.1
        pl.axhline(0,ls='--',c='k')
        if self.r_dimer!=None:
            pl.text(self.r_dimer, ymax, r'$r_{dimer}$')
        pl.text(self.r_cut, ymax, r'$r_{cut}$')
        pl.xlim(xmin=rmin, xmax=rmax)
        pl.ylim(ymin=ymin, ymax=ymax)
        #pl.subtitle('Fitting for %s and %s' % (self.sym1, self.sym2))
        pl.rc('font', size=10)
        pl.rc('legend',fontsize=8)
        pl.legend(loc=4)
        file = '%s_%s_repulsion.pdf' % (self.sym1, self.sym2)
        if filename!=None:
            file=filename
        pl.savefig(file)
        pl.clf()


    def add_comment(self,s=None):
        """
        Append some comment for par-file.

        These comments will end up in Hotbit's output logfile each time
        fitted repulsion is used in calculations. For this reason,
        use as short and concise comments as possible.

        parameters:
        ===========
        s:            comment as a string
        """
        if s in [None, '']:
            return
        add='|'
        if len(self.comments)==0:
            add=''
        self.comments+=add+s


    def fit(self):
        """
        Fit spline into {r, V_rep'(r)} -data points.
        """
        from scipy.interpolate import splrep, splev
        self.k = min(len(self.deriv),self.k)

        x = np.array([self.deriv[i][0] for i in range(len(self.deriv))])
        y = np.array([self.deriv[i][1] for i in range(len(self.deriv))])
        w = np.array([self.deriv[i][2] for i in range(len(self.deriv))])
        # sort values so that x is in ascending order
        indices = x.argsort()
        x, y, w = x[indices], y[indices], w[indices]
        x, y, w = self._group_closeby_points(x,y,w)
        # use only points that are closer than r_cut
        indices = np.where(x < self.r_cut)
        x, y, w = list(x[indices]), list(y[indices]), list(w[indices])
        # force the spline curve to go to zero at x=r_cut
        x.append(self.r_cut)
        y.append(0.0)
        w.append(1E3*max(w))
        if self.s == None:
            # from documentation of splrep in scipy.interpolate.fitpack
            self.s = len(x) - np.sqrt(2*len(x))

        print("\nFitting spline for V_rep'(R) with parameters", file=self.txt)
        print("  k=%i, s=%0.4f, r_cut=%0.4f\n" %(self.k, self.s, self.r_cut), file=self.txt)
        tck = splrep(x, y, w, s=self.s, k=self.k)

        def dv_rep(r):
            return splev(r, tck)

        v_rep = self._integrate_vrep(dv_rep, self.r_cut)

        def potential(r, der=0):
            if der == 0:
                return v_rep(r)
            elif der == 1:
                return dv_rep(r)
            else:
                raise NotImplementedError("Only 0th and 1st derivatives")
        self.v = potential


    def _group_closeby_points(self, x, y, w):
        """
        If there are many y-values with almost the same x-values,
        it is impossible to make spline fit to these points.
        For these points the y will be the weighted average of
        the y-points and the weight is the sum of the weights of
        averaged y-points.
        """
        accuracy = 4 # the number of decimals to maintain
        pseudo_x = np.array(x*10**accuracy, dtype=int)
        groups = np.zeros(len(x), dtype=int)
        g = 0
        for i in range(1,len(pseudo_x)):
            if pseudo_x[i] != pseudo_x[i-1]:
                groups[i] = groups[i-1] + 1
            else:
                groups[i] = groups[i-1]
        new_x = []
        new_y = []
        new_w = []
        for g in range(max(groups)+1):
            same = np.where(groups == g)
            new_x.append(np.average(x[same]))
            new_y.append(np.dot(y[same],w[same])/np.sum(w[same]))
            new_w.append(np.sum(w[same]))
        return np.array(new_x), np.array(new_y), np.array(new_w)


    def write_par(self, inputpar, filename=None):
        """
        Write the full par-file to file.

        parameters:
        ===========
        inputpar:     the par-file where the repulsion is appended
        filename:     output file
        """
        from time import asctime
        import shutil

        if filename==None:
            filename = 'repulsion_'+inputpar

        shutil.copy(inputpar, filename)
        f = open(filename, 'a')
        # add comments
        print("repulsion_comment=", file=f)
        print("%s\nparameters r_cut = %0.4f Ang, s = %0.4f, k = %3i" % (asctime(),self.r_cut, self.s, self.k), file=f)
        if len(self.structures)>1:
            print("The systems used to produce this fit:", file=f)
            for data in self.structures:
                print("%20s %3s" % (data['filename'], data['charge']), file=f)
        if len(self.comments) > 0:
            print(self.comments, file=f)
        print('\n\nrepulsion=', file=f)
        for r in np.linspace(0.1, self.r_cut, 100):
            print(r/Bohr, self(r)/Hartree, file=f)
        f.close()


    def _integrate_vrep(self, dv_rep, r_cut, N=100):
        """
        Integrate V'_rep(r) from r_cut to zero to get the V_rep(r)
        """
        from box.interpolation import SplineFunction
        from scipy.integrate import quadrature
        r_g = np.linspace(r_cut, 0, N)
        dr = r_g[1] - r_g[0]
        v_rep = np.zeros(N)
        for i in range(1,len(r_g)):
            v_rep[i] = v_rep[i-1]
            val, err = quadrature(dv_rep, r_g[i-1], r_g[i], tol=1.0e-12, maxiter=50)
            v_rep[i] += val
        # SplineFunction wants the x-values in ascending order
        return SplineFunction(r_g[::-1], v_rep[::-1])


    def _set_calc(self,atoms,calc):
        """
        Set calculator for given atoms.
        """
        if type(atoms)==type(''):
            a = read(atoms)
        else:
            a = atoms.copy()
        c = copy(calc)
        a.set_calculator(c)
        return a,c


    def _get_color(self,color):
        """ Get next color in line if color==None """
        if color==None:
            index = self.colori
            self.colori +=1
            if self.colori == len(self.colors): self.colors=0
            return self.colors[index]
        else:
            return color

    #
    #       Fitting methods
    #
    def append_point(self,weight,R,dvrep,comment=None,label=None,color='g'):
        """
        Add point to vrep'-fitting.

        parameters:
        ===========
        weight:    fitting weight
        R:         radius (Angstroms)
        dvrep:     V_rep'(R) (eV/Angstroms)
        comment:   fitting comment for par file (replaced by label if None)
        label:     plotting label (replaced by comment if None)
        """
        if comment==None: comment=label
        if label==None: label=comment
        self.deriv.append([R,dvrep,weight,color,label])
        if comment!='_nolegend_':
            self.add_comment(comment)
        return R,dvrep,weight


    def append_scalable_system(self,weight,calc,atoms,comment=None,label=None,color=None):
        """
        Use scalable equilibrium system in repulsion fitting.

        Scalable means that atoms is an equilibrium system, which
        has only given bond lengths R, and whose dimensions can be
        scaled E_DFT(R), and, because of equilibrium, E_DFT'(R)=0.
        Hence

            E_DFT'(R) = E_wr'(R) + N*V_rep'(R) = 0

            ==> V_rep'(R) = -E_wr'(R)/N

        where E_wr = E_bs + E_coul is the DFTB energy without
        repulsion.

        parameters:
        ===========
        weight:        fitting weight
        calc:          Hotbit calculator (remember charge and k-points)
        atoms:         filename or ase.Atoms instance
        comment:       fitting comment for par file (replaced by label if None)
        label:         plotting label (replaced by comment if None)
        color:         plotting color
        """
        atoms, calc = self._set_calc(atoms,calc)
        if comment==None: comment=label
        if label==None: label=comment

        e1 = atoms.get_potential_energy()
        R, N = self._get_repulsion_distances(calc)
        atoms.set_cell( atoms.get_cell()*self.scale, scale_atoms=True )
        e2 = atoms.get_potential_energy()

        dEwr=(e2-e1)/(self.scale*R-R)
        color = self._get_color(color)
        comment += ';w=%.1f' %weight
        self.append_point(weight,R,-dEwr/N,comment,label,color)
        print('\nAdding a scalable system %s with %i bonds at R=%.4f.' %(atoms.get_chemical_symbols(),N,R), file=self.txt)


    def append_dimer(self,weight,calc,R,comment=None,label='dimer',color=None):
        """
        Use dimer bond length in fitting.

        parameters:
        ===========
        weight:    fitting weight
        calc:      Hotbit calculator used in calculation
                   (remember Gamma-point and charge)
        R:         dimer bond length (Angstroms)
        comment:   fitting comment for par-file (replaced by label if None)
        label:     plotting label (replaced by comment if None)
        color:     plotting color
        """
        if comment==None: comment=label
        self.r_dimer = R
        atoms = Atoms([self.sym1,self.sym2],[(0,0,0),(R,0,0)],pbc=False)
        atoms.center(vacuum=5)
        color = self._get_color(color)
        self.append_scalable_system(weight,calc,atoms,comment=comment,label=label,color=color)


    def append_equilibrium_trajectory(self,weight,calc,traj,comment=None,label=None,color=None):
        """
        Calculates the V'rep(r) from a given equilibrium trajectory.

        The trajectory is set of three (or more, albeit not necessary) frames
        where atoms move near their equilibrium structure. To first approximation,
        the energies of these frames ARE THE SAME. This method is then
        equivalent to append_energy_curve method for given trajectory, with a flat
        energy curve.
        * Atoms should move as parallel to the fitted bonds as possible.
        * Amplitude should be small enough (say, 0.01 Angstroms)


        parameters:
        ===========
        weight:              fitting weight
        calc:                Hotbit calculator (remember charge and k-points)
        traj:                filename for ASE trajectory (energies need not be defined)
        comment:             fitting comment for par-file (replaced by comment if None)
        label:               plotting label (replaced by comment if None)
        color:               plotting color
        """
        traj1 = PickleTrajectory(traj)
        atoms2 = traj1[0].copy()
        calc2 = NullCalculator()
        atoms2.set_calculator(calc2)
        tmpfile = '_tmp.traj'
        traj2 = PickleTrajectory(tmpfile,'w',atoms2)
        for atoms1 in traj1:
            atoms2.set_positions(atoms1.get_positions())
            atoms2.set_cell( atoms1.get_cell() )
            atoms2.get_potential_energy()
            traj2.write()
        traj2.close()

        self.append_energy_curve(weight,calc,tmpfile,comment,label,color)
        os.remove(tmpfile)
        if os.path.isfile(tmpfile+'.bak'):
            os.remove(tmpfile+'.bak')

    def append_energy_slope(self,weight,p,dEdp,p0,calc,traj,comment=None,label=None,color=None):
        """
        Calculates the V'rep(r) at one point using trajectory over parameters p.

        Trajectory is calculated using parameters p, giving E(p), where E is the total energy
        without Vrep(r). The pair distance R=R(p). At p=p0, we set dE/dp|p=p0=dEdp, from which
        we can set V'rep(R(p)) as

                    dEdp - dE/dp(p0)
        V'rep(r) = -------------------
                      N * dR/dp

        parameters:
        ===========
        weight:              fitting weight
        p:                   parameter list
        dEdp:                slope of energy at p0
        p0:                  the point where energy slope is set
        calc:                Hotbit calculator (remember charge and k-points)
        traj:                filename for ASE trajectory, or PickleTrajectory
                             object
        comment:             fitting comment for par-file (replaced by comment if None)
        label:               plotting label (replaced by comment if None)
        color:               plotting color
        """
        raise NotImplementedError('This method was never tested properly.')
        from box.interpolation import SplineFunction
        R, E, N = [], [], []
        for atoms in traj:
            a, c = self._set_calc(atoms,calc)
            e = a.get_potential_energy()
            r, n = self._get_repulsion_distances(c)
            if n>0 and r<self.r_cut:
                E.append( atoms.get_potential_energy() )
                R.append(r)
                N.append(n)

        R,E,N = np.array(R), np.array(E), np.array(N)
        if np.any(N[0]!=N):
            raise NotImplementedError('The number of bonds changes during trajectory; check implementation.')

        Ef = SplineFunction(p,E)
        Rf = SplineFunction(p,R)

        color = self._get_color(color)
        comment += ';w=%.1f;N=%i' %(weight,N[0])
        return self.append_point(weight,Rf(p0),(dEdp-Ef(p0,der=1))/(N[0]*Rf(p0,der=1)),comment,label,color)


    def append_energy_curve(self,weight,calc,traj,comment=None,label=None,color=None):
        """
        Calculates the V'rep(r) from a given ase-trajectory.

        The trajectory can be anything, as long as the ONLY missing energy
        from DFTB calculation is N*V_rep(R). Hence

            E_DFT(R) = E_wr(R) + N*V_rep(R)

                         E_DFT'(R) - E_wr'(R)
            V_rep'(R) =  ------------------ ,
                                N

        where R is the nn. distance,N is the number of A-B pairs taken into account,
        and E_wr(R) = E_bs(R) + E_coul(R) is the DFTB energy without repulsion.
        At least 3 points in energy curve needed, preferably more.

        parameters:
        ===========
        weight:              fitting weight
        calc:                Hotbit calculator (remember charge and k-points)
        traj:                filename for ASE trajectory, or PickleTrajectory
                             object
        comment:             fitting comment for par-file (replaced by comment if None)
        label:               plotting label (replaced by comment if None)
        color:               plotting color
        """
        if comment==None: comment=label
        if label==None: label=comment

        #if not ( isinstance(traj, type(PickleTrajectory)) or isinstance(traj, list) ):
        if not ( isinstance(traj, type(PickleTrajectory)) or isinstance(traj, list) ):
            print("\nAppending energy curve data from %s..." %traj, file=self.txt)
            traj = PickleTrajectory(traj)
        else:
            print('\nAppending energy curve data...', file=self.txt)
        Edft, Ewr, N, R = [], [], [], []
        if len(traj)<3:
            raise AssertionError('At least 3 points in energy curve required.')
        for atoms in traj:
            a, c = self._set_calc(atoms,calc)
            e = a.get_potential_energy()
            r, n = self._get_repulsion_distances(c)
            if n>0 and r<self.r_cut:
                Edft.append( atoms.get_potential_energy() )
                Ewr.append( e )
                R.append(r)
                N.append(n)
        Edft = np.array(Edft)
        Ewr = np.array(Ewr)
        N = np.array(N)
        R = np.array(R)

        if np.any( N-N[0]!=0 ):
            raise RuntimeError('The number of bonds changes within trajectory.')

        # sort radii because of spline
        ind = R.argsort()
        R    = R[ind]
        Edft = Edft[ind]
        Ewr  = Ewr[ind]
        from box.interpolation import SplineFunction
        k = min(len(Edft)-2,3)
        vrep = SplineFunction(R, (Edft-Ewr)/N, k=k, s=0)

        color = self._get_color(color)
        for i, r in enumerate(R):
            if i==0:
                com = comment + ';w=%.1f' %weight
            else:
                label='_nolegend_'
                com = None
            self.append_point(weight/np.sqrt(len(R)),r, vrep(r,der=1), com, label, color)
        print("Appended %i points around R=%.4f...%.4f" %(len(N),R.min(),R.max()), file=self.txt)


    def append_homogeneous_cluster(self,weight,calc,atoms,comment=None,label=None,color=None):
        """
        Use homonuclear cluster in fitting, even with different bond lengths.

        Construct repulsive forces so that residual forces F_DFT-(F_wr+F_rep),
        where F_DFT are DFT forces (zero if cluster in equilibrium), F_wr are
        DFTB forces without repulsion, and F_rep are the repulsive forces.
        That is, we minimize the function

           sum_i |F_DFT_i - F_WR_i - F_rep_i|^2

        with respect a and b, where V_rep'(R) = a + b*(r-r_cut). Then, add fitting points
        from rmin to rmax, where these values span all pair distances below r_cut
        within the cluster.

        Only finite, non-periodic systems can be used.

        parameters:
        ===========
        weight:        fitting weight
        calc:          Hotbit calculator (remember charge and no k-points)
        atoms:         filename or ASE.Atoms instance
        comment:       fitting comment for par-file (replaced by label if None)
        label:         plotting label (replaced by comment if None)
        color:         plotting color
        """
        import numpy as np
        if comment==None: comment=label
        if label==None: label=comment
        if type(atoms)==type(''):
            atoms = read(atoms)

        N = len(atoms)
        try:
            f_DFT = atoms.get_forces()
            print("    Use forces", file=self.txt)
        except:
            f_DFT = np.zeros((N,3))
            print("    No forces (equilibrium cluster)", file=self.txt)

        atoms, calc = self._set_calc(atoms,calc)
        print("\nAppending homogeneous cluster.", file=self.txt)

        f_wr = atoms.get_forces()
        distances = calc.rep.get_repulsion_distances(self.sym1,self.sym2,self.r_cut)
        rmin, rmax = distances.min(), distances.max()

        def dvrep(r,p):
            """ Auxiliary first-order polynomial for repulsion derivative """
            return p[0]+p[1]*(r-self.r_cut)


        def to_minimize(p,atoms,fdft,fwr):
            """ Function sum_I |F_DFT_I - F_TB_I|^2 to minimize. """
            N = len(atoms)
            pos = atoms.get_positions()
            resid = np.zeros((N,3))
            frep  = np.zeros((N,3))
            for i in range(N):
                for j in range(N):
                    if i==j: continue
                    rij = pos[j]-pos[i]
                    dij = np.linalg.norm(rij)
                    if dij>self.r_cut:
                        continue
                    else:
                        frep[i] += dvrep(dij,p)*rij/dij
            resid = fdft - ( fwr + frep )
            return sum([ np.linalg.norm(resid[i])**2 for i in range(N) ])

        from scipy.optimize import fmin
        p = fmin( to_minimize,[-1.0,5.0],args=(atoms,f_DFT,f_wr),xtol=1E-5,ftol=1E-5 )
        print('   Cluster: V_rep(R)=%.6f + %.6f (r-%.2f)' %(p[0],p[1],self.r_cut), file=self.txt)

        color = self._get_color(color)
        npp = 6
        rlist = np.linspace(rmin,rmax,npp)
        for i,r in enumerate(rlist):
            if i==0:
                com = comment
                com += ';w=%.1f' %weight
            else:
                label = '_nolegend_'
                com = None
            self.append_point(weight/np.sqrt(npp), r, dvrep(r,p), com, label, color)


    def _get_repulsion_distances(self,calc):
        """
        Return distances below r_cut for given system in calculator.

        return:
        =======
        R:     the mean repulsion distance
        N:     number of bonds
        """
        distances = calc.rep.get_repulsion_distances(self.sym1,self.sym2,self.r_cut)
        if len(distances)==0:
            return 0.0,distances
        R = distances.mean()
        rmin, rmax = distances.min(), distances.max()
        if  rmax - rmin > self.tol:
            atoms = calc.get_atoms()
            raise AssertionError('Bond lengths in are not the same, they vary between %.6f ... %.6f' %(rmin,rmax) )
        N = len(distances)
        return R,N


    def write_fitting_data(self, filename, pickle=True):
        f = open(filename,'w')
        if pickle:
            import pickle
            pickle.dump(self.deriv, f)
            pickle.dump(self.structures, f)
            pickle.dump(self.comments, f)
        else:
            print(self.deriv, file=f)
            print(self.structures, file=f)
            print(self.comments, file=f)
        f.close()


    def load_fitting_data(self, filename):
        import pickle
        f = open(filename,'r')
        self.deriv = pickle.load(f)
        self.structures = pickle.load(f)
        self.comments = pickle.load(f)
        f.close()


    def _get_trajs_for_fitting(self):
        return self.structures