Exemplo n.º 1
0
 def initialize(self):
     """
     This function should be called when the geometry is known (after calling setGeometry(...)).
     """
     # initialize the TD-DFTB calculator
     self.pes = PotentialEnergySurfaces(self.atomlist,
                                        Nst=max(self.state + 1, 2),
                                        **self.geom_kwds)
     # initialize internal coordinate system if needed
     if self.coord_system == "internal":
         self.IC = InternalValenceCoords(
             self.atomlist,
             freeze=self.freeze,
             verbose=self.pes.tddftb.dftb2.verbose)
Exemplo n.º 2
0
    def __init__(self,
                 atomlist0,
                 pes,
                 state1=0,
                 state2=1,
                 coord_system="cartesian",
                 c1=0.9,
                 c2=1.0,
                 epsilon=0.007):
        """
        minimal energy conical intersection (MECI)

        Parameters
        ----------
        atomlist0      : initial geometry
        pes            : instance of DFTB.PES.PotentialEnergySurfaces

        Optional
        --------
        state1, state2 : indices of lower and upper electronic states
                         between which the MECI should be found
                         (0 - ground state, 1 - first excited state)
        coord_system   : The step along the gradient is taken either directly
                         in 'cartesian' coordinates or in 'internal' redundant
                         coordinates.
        epsilon        : To avoid hitting the CI seam exactly, which would
                         result in convergence problems in the SCF cycle,
                         an energy gap of `epsilon` is maintained between
                         the upper and the lower states.
        """
        self.atomlist0 = atomlist0
        self.pes = pes
        self.state1 = state1
        self.state2 = state2
        assert self.state1 < self.state2
        self.coord_system = coord_system
        if self.coord_system == "internal":
            self.ic = InternalValenceCoords(
                self.atomlist0, verbose=self.pes.tddftb.dftb2.verbose)
        # parameters of MECI search algorithm
        self.c1 = c1
        self.c2 = c2
        self.epsilon = epsilon
Exemplo n.º 3
0
    # interpolation parameter
    rs = np.linspace(0.0, 1.0, N)

    geometries_interp = []

    if opts.coord_system == "cartesian":
        for r in rs:
            xr = x0 + r * (x1 - x0)
            geometries_interp.append(XYZ.vector2atomlist(xr, atomlist0))
    elif opts.coord_system == "internal":
        # explicit bonds, shift atom indices from 1- to 0-indexing
        explicit_bonds = [(I - 1, J - 1)
                          for (I, J) in eval(opts.explicit_bonds)]

        IC = InternalValenceCoords(atomlist0, explicit_bonds=explicit_bonds)
        # initial and final geometry in internal coordinates
        q1 = IC.cartesian2internal(x1)
        q0 = IC.cartesian2internal(x0)
        for r in rs:
            qr = q0 + r * (q1 - q0)
            xr = IC.internal2cartesian(qr)
            geometries_interp.append(XYZ.vector2atomlist(xr, atomlist0))
    else:
        raise ValueError(
            "Coordinate system '%s' not understood, valid options are 'internal' and 'cartesian'"
            % opts.coord_system)

    XYZ.write_xyz(xyz_interp, geometries_interp)
    print "Interpolated geometries written to %s" % xyz_interp
Exemplo n.º 4
0
        IJKL = (I, J, K)
        coord_name = "ANGLE(%d-%d-%d)" % (I, J, K)
    elif len(scan) == 6:
        # scan dihedral angle
        I, J, K, L, nsteps, incr = scan
        # convert angle from degrees to radians
        incr *= np.pi / 180.0
        IJKL = (I, J, K, L)
        coord_name = "DIHEDRAL(%d-%d-%d-%d)" % (I, J, K, L)
    else:
        raise ValueError("Format of scan '%s' not understood!" % scan)

    # shift indices to programmer's style (starting at 0)
    IJKL = map(lambda I: I - 1, IJKL)

    IC = InternalValenceCoords(atomlist0, freeze=freeze, verbose=opts.verbose)

    # cartesian coordinates along the scan
    scan_geometries = [atomlist0]
    # value of internal coordinate IJKL along the scan
    val0 = IC.coordinate_value(x0, IJKL)
    scan_coords = [val0]

    x1 = x0
    for i in range(0, nsteps):
        # cartesian coordinates at displaced geometry
        x1 = IC.internal_step(x1, IJKL, incr)
        # new value of internal coordinate, should be approximately
        #  val0 + incr
        val1 = IC.coordinate_value(x1, IJKL)
Exemplo n.º 5
0
class MECI:
    def __init__(self,
                 atomlist0,
                 pes,
                 state1=0,
                 state2=1,
                 coord_system="cartesian",
                 c1=0.9,
                 c2=1.0,
                 epsilon=0.007):
        """
        minimal energy conical intersection (MECI)

        Parameters
        ----------
        atomlist0      : initial geometry
        pes            : instance of DFTB.PES.PotentialEnergySurfaces

        Optional
        --------
        state1, state2 : indices of lower and upper electronic states
                         between which the MECI should be found
                         (0 - ground state, 1 - first excited state)
        coord_system   : The step along the gradient is taken either directly
                         in 'cartesian' coordinates or in 'internal' redundant
                         coordinates.
        epsilon        : To avoid hitting the CI seam exactly, which would
                         result in convergence problems in the SCF cycle,
                         an energy gap of `epsilon` is maintained between
                         the upper and the lower states.
        """
        self.atomlist0 = atomlist0
        self.pes = pes
        self.state1 = state1
        self.state2 = state2
        assert self.state1 < self.state2
        self.coord_system = coord_system
        if self.coord_system == "internal":
            self.ic = InternalValenceCoords(
                self.atomlist0, verbose=self.pes.tddftb.dftb2.verbose)
        # parameters of MECI search algorithm
        self.c1 = c1
        self.c2 = c2
        self.epsilon = epsilon

    def runTDDFTB(self, x):
        """
        Parameters
        ----------
        x       :  cartesian coordinates

        Returns
        -------
        e1,e2   :  total energies of 1st and 2nd electronic state (in Hartree)
        g1,g2   :  gradients of total energies (in a.u.)
        nac     :  non-adiabatic coupling vector between 1st and 2nd state
        """
        # compute energies and gradient of lower state
        energies1, grad1 = self.pes.getEnergiesAndGradient(x, self.state1)
        e1 = energies1[self.state1]
        g1 = grad1
        # compute energies and gradient of lower state
        energies2, grad2 = self.pes.getEnergiesAndGradient(x, self.state2)
        e2 = energies2[self.state2]
        g2 = grad2
        #
        if self.state1 == 0:
            # An approximate NAC vector can be computed for transitions
            # to the ground state.
            nac = self.pes.tddftb.NonAdiabaticCouplingVector(self.state2 - 1)
            nac = nac.flatten()
        else:
            # NAC vectors between excited states are not available. The optimization
            # algorithm seems to work with any random vector that is not parallel
            # to the gradient.
            nac = np.ones(len(x))

        return e1, e2, g1, g2, nac

    def getGradient(self, x):
        """
        The problem with the Bearpark algorithm is that there is no objective function,
        only a gradient. Following in the direction of this vector leads to the MECI.
        """
        # compute electronic structure
        e1, e2, g1, g2, nac = self.runTDDFTB(x)
        # normalized gradient difference vector
        x1 = g2 - g1
        x1 /= la.norm(x1)
        # make non-adiabatic coupling vector orthogonal to gradient difference vector
        x2 = nac - np.dot(x1, nac) * x1
        # and normalize it
        x2 /= la.norm(x2)
        # check that x1 and x2 are really orthogonal
        assert abs(np.dot(x1, x2)) < 1.0e-10, "x1 and x2 not orthogonal!"

        # P is the projector onto the 3*N-2 dimensional orthogonal complement to
        # the plance x1,x2
        P = np.eye(len(x1)) - np.outer(x1, x1) - np.outer(x2, x2)

        f = 2 * (e2 - e1 - self.epsilon) * x1

        # project gradient dE2/dq onto seam space
        g = np.dot(P, g2)

        #grad = self.c2 * ( self.c1*g + (1.0-self.c1)*f )
        grad = g + f

        return e1, e2, grad

    def adjust_shift(self, en_gap):
        """ adjust energy shift depending on the gap between the two states"""
        en_gap_eV = en_gap * AtomicData.hartree_to_eV
        if en_gap_eV > 0.4:
            self.c1 = 0.5
            self.c2 = 2.0
        if en_gap_eV <= 0.4:
            self.c1 = 0.7
            self.c2 = 1.0
        if en_gap_eV <= 0.2:
            self.c1 = 0.9

    def opt(self, max_iter=5000, step_size=1.0, gtol=0.005):
        """
        optimize MECI by following the gradient downhill

        Optional
        --------
        max_iter  :  maximum number of steps
        step_size :  The geometry is updated by making a step
                       x -> x - step_size * grad
        gtol      :  tolerance for the gradient norm
        """
        print("optimize MECI by following the gradient")
        print("  lower state :  %d" % state1)
        print("  upper state :  %d" % state2)
        print("Intermediate geometries are written to 'meci_path.xyz'")
        print("and a table with energies is written to 'meci_energies.dat'")

        # initial geometry
        x = XYZ.atomlist2vector(self.atomlist0)
        # overwrite geometries from previous run
        mode = "w"
        en_fh = open("meci_energies.dat", "w")
        print("# optimization of MECI", file=en_fh)
        print(
            "# STEP     ENERGY(1)/Hartree    ENERGY(2)/Hartree    ENERGY(3)-EPS/Hartree",
            file=en_fh)

        for i in range(0, max_iter):
            e1, e2, grad = self.getGradient(x)
            # energy gap
            en_gap = e2 - e1
            self.adjust_shift(en_gap)

            # save intermediate steps and energies
            atomlist = XYZ.vector2atomlist(x, self.atomlist0)
            XYZ.write_xyz("meci_path.xyz", [atomlist],
                          title="ENERGY= %e  GAP= %e" % (e2, en_gap),
                          mode=mode)
            print(" %4.1d      %+15.10f      %+15.10f      %+15.10f" %
                  (i, e1, e2, e2 - self.epsilon),
                  file=en_fh)
            en_fh.flush()

            # append to trajectory file
            mode = "a"
            #

            gnorm = la.norm(grad)
            print(" %4.1d    e2= %e  e2-e1= %e   |grad|= %e  (tolerance= %e)" %
                  (i, e2, en_gap, gnorm, gtol))
            if gnorm < gtol:
                break

            if self.coord_system == "cartesian":
                # descend along gradient directly in cartesian coordinates
                x -= step_size * grad
            elif self.coord_system == "internal":
                # use internal redundant coordinates
                # 1) transform cartesian to internal coordinates x -> q
                q = self.ic.cartesian2internal(x)
                # 2) transform cartesian gradient to internal coordinates
                # dE/dx -> dE/dq
                grad_intern = self.ic.transform_gradient(x, grad)
                # 3) take step along gradient in internal coordinates
                q = q - step_size * grad_intern
                # 4) deduce new cartesian coordinates from new internal coordinates q
                x = self.ic.internal2cartesian(q)

        else:
            print("exceeded maximum number of steps")

        en_fh.close()
Exemplo n.º 6
0
class GeometryOptimization:
    def __init__(self,
                 state=0,
                 calc_hessian=0,
                 coord_system="cartesian",
                 grad_tol=1.0e-5,
                 func_tol=1.0e-8,
                 max_steps=100000,
                 method='CG',
                 explicit_bonds="[]",
                 freeze="[]",
                 relaxed_scan="()"):
        """
        Parameters
        ==========
        Geometry Optimization.state: index of electronic state to be optimized (0 - ground state, 1 - first excited state).
        Geometry Optimization.calc_hessian: Should the hessian matrix be computed (1) or not (0)? If yes, the Hessian matrix is saved to the file 'hessian.dat' and the vibrational modes and frequencies are saved to the file 'vib.molden' that can be visualized with the Molden program.
        Geometry Optimization.coord_system: The optimization can be performed either directly in cartesian coordinate ('cartesian') or in redundant internal coordinates ('internal'). Cartesian coordinates are more reliable but make it difficult to converge to the minimum in a floppy molecule. Internal coordinates don't work for disconnected fragments and require a starting geometry with the correct atom connectivity.
        Geometry Optimization.grad_tol: The optimization is finished only if the norm of the gradient is smaller than this threshold.
        Geometry Optimization.func_tol: The optimization is finished only if the energy does not change more than this threshold.
        Geometry Optimization.max_steps: Maximum number of optimization steps.
        Geometry Optimization.method: Choose the optimization algorithm. 'Newton', 'Steepest Descent' and 'BFGS' have their own implementations, 'CG' requests scipy's conjugate gradient method.
        Geometry Optimization.explicit_bonds: Inserts artificial bonds between pairs of atoms. The bonds are specified as a list of tuples (I,J) of atom indices (starting at 1). This allows to join disconnected fragments.
        Geometry Optimization.freeze: Freeze internal coordinates. The internal coordinates that should be kept at their current value during the optimization are specified as a list of tuples of atom indices (starting at 1). Each tuple may contain 2, 3 or 4 atom indices, (I,J) - bond between atoms I and J, (I,J,K) - valence angle I-J-K, (I,J,K,L) - dihedral angle between bonds I-J, J-K and K-L. For example "[(1,2), (4,5,6)]" freezes the bond between atoms 1 and 2 and the angle 4-5-6. The atom indices do not necessarily have to correspond to a 'physical' bond, angle or dihedral. So, for instance, you can also freeze the distance between two atoms that are not connected.
        Geometry Optimization.relaxed_scan: Perform a relaxed scan along an internal coordinate. The coordinate is incremented from its initial value by `nsteps` steps of size `incr`. In each step the value of the scan coordinate is kept constant while all other degrees of freedom are relaxed. The internal coordinate is specified by 2, 3 or 4 atom indicies as explained for the option `freeze` followed by the number of steps and the increment, which is in Angstrom for bond lengths and degrees for angles. The format is "(I,J, nsteps, incr)" for scanning a bond length, "(I,J,K, nsteps, incr)" for scanning a valence angle and "(I,J,K,L, nsteps, incr)" for scanning a torsion.  For example "(1,2, 5, 0.1)" will scan the bond between atom 1 and 2 in 5 steps of 0.1 Angstrom and "(1,2,3, 9, 10.0)" will scan the angle 1-2-3 in 9 steps of 10.0 degrees. Dihedral angles are limited to the range [0,180], so if the angle is close to 180 degrees a negative increment should be used, otherwise the scan will stop at 180 degrees.
        """
        self.state = state
        self.calc_hessian = calc_hessian
        assert coord_system in ["cartesian", "internal"]
        self.coord_system = coord_system
        self.grad_tol = grad_tol
        self.func_tol = func_tol
        self.maxiter = max_steps
        self.method = method
        # parameters for relaxed scan
        if relaxed_scan != ():
            assert coord_system == "internal", "A relaxed scan requires 'coord_system=internal'!"

            if len(relaxed_scan) == 4:
                # scan bond length
                I, J, nsteps, incr = relaxed_scan
                # convert increment from Angstrom to bohr
                incr /= AtomicData.bohr_to_angs
                IJKL = (I, J)
            elif len(relaxed_scan) == 5:
                # scan valence angle
                I, J, K, nsteps, incr = relaxed_scan
                # convert angle from degrees to radians
                incr *= np.pi / 180.0
                IJKL = (I, J, K)
            elif len(relaxed_scan) == 6:
                # scan dihedral angle
                I, J, K, L, nsteps, incr = relaxed_scan
                # convert angle from degrees to radians
                incr *= np.pi / 180.0
                IJKL = (I, J, K, L)
            else:
                raise ValueError(
                    "Format of relaxed scan '%s' not understood!" %
                    relaxed_scan)

            # The scan coordinate has to be frozen in each scan step.
            freeze.append(IJKL)

            self.relaxed_scan_nsteps = int(nsteps)
            self.relaxed_scan_incr = incr
            # shift indices by -1 so that the first index starts at 0
            self.relaxed_scan_IJKL = tuple([int(I) - 1 for I in IJKL])

            self.optimization_type = "relaxed_scan"
        else:
            self.optimization_type = "minimize"

        # freezing of internal coordinates
        self.freeze = []
        for IJKL in freeze:
            # Indices on the command line start at 1, but internally
            # indices starting at 0 are used.
            IJKL = tuple([I - 1 for I in IJKL])
            self.freeze.append(IJKL)
            assert coord_system == "internal", "Freezing of internal coordinates require 'coord_system=internal'!"

    def setGeometry(self, atomlist, geom_kwds={}):
        self.geom_kwds = geom_kwds
        self.atomlist = atomlist

    def setOutput(self,
                  xyz_opt="opt.xyz",
                  xyz_scan="scan.xyz",
                  dat_scan="scan.dat"):
        """files where geometries and energy tables created during the optimization and scan are written to"""
        self.xyz_opt = xyz_opt
        self.xyz_scan = xyz_scan
        self.dat_scan = dat_scan

    def getGeometry(self):
        """current geometry"""
        return self.atomlist

    def getEnergy(self):
        """current energy"""
        return self.enI

    def initialize(self):
        """
        This function should be called when the geometry is known (after calling setGeometry(...)).
        """
        # initialize the TD-DFTB calculator
        self.pes = PotentialEnergySurfaces(self.atomlist,
                                           Nst=max(self.state + 1, 2),
                                           **self.geom_kwds)
        # initialize internal coordinate system if needed
        if self.coord_system == "internal":
            self.IC = InternalValenceCoords(
                self.atomlist,
                freeze=self.freeze,
                verbose=self.pes.tddftb.dftb2.verbose)

    def minimize(self):
        I = self.state

        # convert geometry to a vector
        x0 = XYZ.atomlist2vector(self.atomlist)

        # This member variable holds the last energy of the state
        # of interest.
        self.enI = 0.0
        # last available energies of all electronic states that were
        # calculated
        self.energies = None

        # FIND ENERGY MINIMUM
        # f is the objective function that should be minimized
        # it returns (f(x), f'(x))
        def f_cart(x):
            #
            if I == 0 and type(self.pes.tddftb.XmY) != type(None):
                # Only ground state is needed. However, at the start
                # a single TD-DFT calculation is performed to initialize
                # all variables (e.g. X-Y), so that the program does not
                # complain about non-existing variables.
                enI, gradI = self.pes.getEnergyAndGradient_S0(x)
                energies = np.array([enI])
            else:
                energies, gradI = self.pes.getEnergiesAndGradient(x, I)
                enI = energies[I]
            self.enI = enI
            self.energies = energies
            print("E = %2.7f     |grad| = %2.7f" % (enI, la.norm(gradI)))
            #
            # also save geometries from line searches
            save_xyz(x)

            return enI, gradI

        print("Intermediate geometries will be written to %s" % self.xyz_opt)

        # This is a callback function that is executed for each optimization step.
        # It appends the current geometry to an xyz-file.
        def save_xyz(x, mode="a"):
            self.atomlist = XYZ.vector2atomlist(x, self.atomlist)
            XYZ.write_xyz(self.xyz_opt, [self.atomlist], \
                          title="charge=%s energy= %s" % (self.geom_kwds.get("charge",0), self.enI),\
                          mode=mode)
            return x

        Nat = len(self.atomlist)

        if self.coord_system == "cartesian":
            print(
                "optimization is performed directly in cartesian coordinates")
            q0 = x0
            objective_func = f_cart
            save_geometry = save_xyz
            max_steplen = None
        elif self.coord_system == "internal":
            print(
                "optimization is performed in redundant internal coordinates")
            # transform cartesian to internal coordinates, x0 ~ q0
            q0 = self.IC.cartesian2internal(x0)

            # define functions that wrap the cartesian<->internal transformations
            def objective_func(q):
                # transform back from internal to cartesian coordinates
                x = self.IC.internal2cartesian(q)
                self.IC.cartesian2internal(x)
                # compute energy and gradient in cartesian coordinates
                en, grad_cart = f_cart(x)
                # transform gradient to internal coordinates
                grad = self.IC.transform_gradient(x, grad_cart)

                return en, grad

            def save_geometry(q, **kwds):
                # transform back from internal to cartesian coordinates
                x = self.IC.internal2cartesian(q)
                # save cartesian coordinates
                save_xyz(x, **kwds)
                return x

            def max_steplen(q0, v):
                """
                find a step size `a` such that the internal->cartesian
                transformation converges for the point q = q0+a*v
                """
                a = 1.0
                for i in range(0, 7):
                    q = q0 + a * v
                    try:
                        x = self.IC.internal2cartesian(q)
                    except NotConvergedError as e:
                        # reduce step size by factor of 1/2
                        a /= 2.0
                        continue
                    break
                else:
                    raise RuntimeError(
                        "Could not find a step size for which the transformation from internal to cartesian coordinates would work for q=q0+a*v! Last step size a= %e  |v|= %e  |a*v|= %e"
                        % (a, la.norm(v), la.norm(a * v)))
                return a

        else:
            raise ValueError("Unknown coordinate system '%s'!" %
                             self.coord_system)
        # save initial energy and geometry
        objective_func(q0)
        save_geometry(q0, mode="w")

        options = {
            'gtol': self.grad_tol,
            'maxiter': self.maxiter,
            'gtol': self.grad_tol,
            'norm': 2
        }
        if self.method == 'CG':
            # The "BFGS" method is probably better than "CG", but the line search in BFGS is expensive.
            res = optimize.minimize(objective_func,
                                    q0,
                                    method="CG",
                                    jac=True,
                                    callback=save_geometry,
                                    options=options)
            #res = optimize.minimize(objective_func, q0, method="BFGS", jac=True, callback=save_geometry, options=options)

        elif self.method in ['Steepest Descent', 'Newton', 'BFGS']:
            # My own implementation of optimization algorithms
            res = minimize(
                objective_func,
                q0,
                method=self.method,
                #line_search_method="largest",
                callback=save_geometry,
                max_steplen=max_steplen,
                maxiter=self.maxiter,
                gtol=self.grad_tol,
                ftol=self.func_tol)
        else:
            raise ValueError("Unknown optimization algorithm '%s'!" %
                             self.method)

        # save optimized geometry
        qopt = res.x
        Eopt = res.fun
        xopt = save_geometry(qopt)
        print("Optimized geometry written to %s" % self.xyz_opt)

        if self.calc_hessian == 1:
            # COMPUTE HESSIAN AND VIBRATIONAL MODES
            # The hessian is calculated by numerical differentiation of the
            # analytical cartesian gradients
            def grad(x):
                en, grad_cart = f_cart(x)
                return grad_cart

            print("Computing Hessian")
            hess = HarmonicApproximation.numerical_hessian_G(grad, xopt)
            np.savetxt("hessian.dat", hess)
            masses = AtomicData.atomlist2masses(atomlist)
            vib_freq, vib_modes = HarmonicApproximation.vibrational_analysis(xopt, hess, masses, \
                                                                             zero_threshold=1.0e-9, is_molecule=True)
            # compute thermodynamic quantities and write summary
            thermo = Thermochemistry.Thermochemistry(
                atomlist, Eopt, vib_freq,
                self.pes.tddftb.dftb2.getSymmetryGroup())
            thermo.calculate()

            # write vibrational modes to molden file
            molden = MoldenExporterSectioned(self.pes.tddftb.dftb2)
            atomlist_opt = XYZ.vector2atomlist(xopt, atomlist)
            molden.addVibrations(atomlist_opt, vib_freq.real,
                                 vib_modes.transpose())
            molden.export("vib.molden")

        ## It's better to use the script initial_conditions.py for sampling from the Wigner
        ## distribution
        """
        # SAMPLE INITIAL CONDITIONS FROM WIGNER DISTRIBUTION
        qs,ps = HarmonicApproximation.initial_conditions_wigner(xopt, hess, masses, Nsample=200)
        HarmonicApproximation.save_initial_conditions(atomlist, qs, ps, ".", "dynamics")
        """

    def relaxed_scan(self, IJKL, nsteps, incr):
        """
        perform a relaxed scan along the internal coordinate IJKL. The coordinate is
        incremented from its initial value by `nsteps` steps of size `incr`. In each
        step the value of the scan coordinate is kept constant while all other degrees
        of freedom are relaxed.

        Parameters
        ----------
        IJKL    :  tuple of 2, 3 or 4 atom indices (starting at 0)
                   (I,J)     -   bond between atoms I and J
                   (I,J,K)   -   valence angle I-J-K
                   (I,J,K,L) -   dihedral angle between the bonds I-J, J-K and K-L
        nsteps  :  number of steps
        incr    :  increment in each step, in bohr for bond lengths, in radians
                   for angles
        """
        # length of tuple IJKL determines type of internal coordinate
        typ = len(IJKL)
        coord_type = {2: "bond", 3: "angle", 4: "dihedral"}
        conv_facs = {
            2: AtomicData.bohr_to_angs,
            3: 180.0 / np.pi,
            4: 180.0 / np.pi
        }
        units = {2: "Angs", 3: "degs", 4: "degs"}
        #
        assert self.coord_system == "internal"
        # freeze scan coordinate at its current value
        self.IC.freeze(IJKL)
        print("  ============ ")
        print("  RELAXED SCAN ")
        print("  ============ ")
        print("  The internal coordinate defined by the atom indices")
        print("    IJKL = %s  " % [I + 1 for I in IJKL])
        print("  is scanned in %d steps of size %8.5f %s." %
              (nsteps, incr * conv_facs[typ], units[typ]))

        def save_step():
            """function is called after each minimization"""
            scan_coord = self.IC.coordinate_value(xi, IJKL)

            print("current value of scan coordinate : %s" % scan_coord)

            if i == 0:
                mode = "w"
            else:
                mode = "a"
            # save relaxed geometry of step i
            XYZ.write_xyz(self.xyz_scan, [atomlist],
                          title="charge=%s energy=%s" %
                          (self.geom_kwds.get("charge", 0), self.enI),
                          mode=mode)

            # save table with energies along scan
            fh = open(self.dat_scan, mode)
            if i == 0:
                # write header
                print("# Relaxed scan along %s defined by atoms %s" %
                      (coord_type[typ], [I + 1 for I in IJKL]),
                      file=fh)
                print("# state of interest: %d" % self.state, file=fh)
                print("# ", file=fh)
                print("#  Scan coordinate     Energies ", file=fh)
                print("#    %s              Hartree " % units[typ], file=fh)

            print("  %8.5f     " % scan_coord, end=' ', file=fh)
            for en in self.energies:
                print("   %e " % en, end=' ', file=fh)
            print("", file=fh)
            fh.close()

        for i in range(0, nsteps):
            print("Step %d of relaxed scan" % i)
            # relax all other coordinates
            self.minimize()
            # optimized geometry of i-th step
            atomlist = self.getGeometry()
            xi = XYZ.atomlist2vector(atomlist)
            # save geometry
            save_step()
            # take a step of size `incr` along the scan coordinate
            xip1 = self.IC.internal_step(xi, IJKL, incr)
            # update geometry
            atomlist = XYZ.vector2atomlist(xip1, atomlist)
            self.setGeometry(atomlist, geom_kwds=self.geom_kwds)

        print("Scan geometries were written to %s" % self.xyz_scan)
        print("Table with scan energies was written to %s" % self.dat_scan)

    def optimize(self):
        """
        run minimization of energy or relaxed scan
        """
        if self.optimization_type == "minimize":
            self.minimize()
        elif self.optimization_type == "relaxed_scan":
            self.relaxed_scan(self.relaxed_scan_IJKL, self.relaxed_scan_nsteps,
                              self.relaxed_scan_incr)
        else:
            raise ValueError("BUG? optimization_type = %s" %
                             self.optimization_type)
    # read final geometry
    atomlist1 = XYZ.read_xyz(xyz1)[0]
    x1 = XYZ.atomlist2vector(atomlist1)

    # interpolation parameter
    rs = np.linspace(0.0, 1.0, N)

    geometries_interp = []

    if opts.coord_system == "cartesian":
        for r in rs:
            xr = x0 + r * (x1 - x0)
            geometries_interp.append(XYZ.vector2atomlist(xr, atomlist0))
    elif opts.coord_system == "internal":
        IC = InternalValenceCoords(atomlist0)
        # initial and final geometry in internal coordinates
        q1 = IC.cartesian2internal(x1)
        q0 = IC.cartesian2internal(x0)
        for r in rs:
            qr = q0 + r * (q1 - q0)
            xr = IC.internal2cartesian(qr)
            geometries_interp.append(XYZ.vector2atomlist(xr, atomlist0))
    else:
        raise ValueError(
            "Coordinate system '%s' not understood, valid options are 'internal' and 'cartesian'"
            % opts.coord_system)

    XYZ.write_xyz(xyz_interp, geometries_interp)
    print "Interpolated geometries written to %s" % xyz_interp