Example #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)
 def __init__(self,
              symbols,
              coordinates,
              tstep,
              nstates,
              charge,
              sc_threshold=0.001):
     # build list of atoms
     atomlist = []
     for s, xyz in zip(symbols, coordinates):
         Z = AtomicData.atomic_number(s)
         atomlist.append((Z, xyz))
     self.dt_nuc = tstep  # nuclear time step in a.u.
     self.nstates = nstates  # number of electronic states including the ground state
     self.sc_threshold = sc_threshold  # threshold for coefficients that are included in the
     # computation of the scalar coupling
     self.Nat = len(atomlist)
     self.masses = AtomicData.atomlist2masses(atomlist)
     self.pes = PotentialEnergySurfaces(atomlist, nstates, charge=charge)
     # save results from last step
     self.last_step = None
    energy_file = args[1]
    force_file = args[2]

    print("Compute forces with DFTB")
    print("========================")
    print("")
    fh_en = open(energy_file, "w")
    print("# ELECTRONIC ENERGY / HARTREE", file=fh_en)

    # first geometry
    atomlist = XYZ.read_xyz(geom_file)[0]
    # read charge from title line in .xyz file
    kwds = XYZ.extract_keywords_xyz(geom_file)
    charge = kwds.get("charge", opts.charge)

    pes = PotentialEnergySurfaces(atomlist, charge=charge)
    # dftbaby needs one excited states calculation to set all variables
    x = XYZ.atomlist2vector(atomlist)
    pes.getEnergies(x)

    for i, atomlist in enumerate(XYZ.read_xyz(geom_file)):
        # compute electronic ground state forces with DFTB
        x = XYZ.atomlist2vector(atomlist)
        en = pes.getEnergy_S0(x)
        # total ground state energy including repulsive potential
        en_tot = en[0]
        print("Structure %d   enTot= %s Hartree" % (i, en_tot))
        # electronic energy without repulsive potential
        en_elec = pes.tddftb.dftb2.getEnergies()[2]
        gradVrep, gradE0, gradExc = pes.grads.gradient(I=0)
        # exclude repulsive potential from forces
class dftbaby_handler:
    def __init__(self,
                 symbols,
                 coordinates,
                 tstep,
                 nstates,
                 charge,
                 sc_threshold=0.001):
        # build list of atoms
        atomlist = []
        for s, xyz in zip(symbols, coordinates):
            Z = AtomicData.atomic_number(s)
            atomlist.append((Z, xyz))
        self.dt_nuc = tstep  # nuclear time step in a.u.
        self.nstates = nstates  # number of electronic states including the ground state
        self.sc_threshold = sc_threshold  # threshold for coefficients that are included in the
        # computation of the scalar coupling
        self.Nat = len(atomlist)
        self.masses = AtomicData.atomlist2masses(atomlist)
        self.pes = PotentialEnergySurfaces(atomlist, nstates, charge=charge)
        # save results from last step
        self.last_step = None

    def getBrightestState(self, coordinates):
        """
        following a call to __init__(...), this function determines which excited state has the largest
        oscillator strength. This state is taken as the initially excited state, on which
        the non-adiabatic dynamic starts.

        Returns
        -------
        state: index of the initial photoexcited state
        """
        x = np.ravel(coordinates).real
        # only excitation energies
        energies = self.pes.getEnergies(x)
        en_exc = energies[1:] - energies[0]
        # compute oscillator strengths
        tdip = self.pes.getTransitionDipoles()
        # oscillator strengths  f[I] = 2/3 omega[I] |<0|r|I>|^2
        f = 2.0 / 3.0 * en_exc * (tdip[0, 1:, 0]**2 + tdip[0, 1:, 1]**2 +
                                  tdip[0, 1:, 2]**2)
        print "The dynamics starts on the excited state with the largest oscillator strength."
        state = 1 + np.argmax(f)
        print " State     Excitation energy / eV      Oscillator strength / e*bohr"
        for i in range(0, self.nstates - 1):
            print " %d         %.7f                   %.7f" % (
                i + 1, en_exc[i] * AtomicData.hartree_to_eV, f[i]),
            if i + 1 == state:
                print "      (initial)"
            else:
                print ""
        print "selected initial state: %d" % state
        return state

    def getFragmentExcitation(self, coordinates, ifrag, iorb, afrag, aorb):

        x = np.ravel(coordinates).real
        # only excitation energies
        energies = self.pes.getEnergies(x)

        # localize orbitals and project iorb->aorb excitation onto adiabatic
        # eigenstates
        LocOrb = OrbitalLocalization(self.pes.tddftb)
        proj = LocOrb.projectLocalizedExcitation(ifrag, iorb, afrag, aorb)

        return proj

    def getAll_S0(self, coordinates, has_scf=False):
        """
        ground state calculation only, in case the excited state calculation has failed.
        """
        x = np.ravel(coordinates).real
        energies = np.zeros(self.nstates)
        coupl = np.zeros((self.nstates, self.nstates))
        tdip = np.zeros((self.nstates, self.nstates, 3))
        olap = np.eye(self.nstates)
        # ground state energy
        try:
            E0, gradTot = self.pes.getEnergyAndGradient_S0(x, has_scf=has_scf)
            # The excitation energies are set to  0, this forces a hop to the ground state
            energies[:] = E0
            # convert everything into the format expected by Jens' program
            accel = -gradTot / self.masses
            accel = np.reshape(accel, (self.Nat, 3))
        except SCFNotConverged as e:
            if self.last_step != None:
                print "WARNING: %s" % e
                print "Trying to continue with gradient and energy from last step!"
                energies, accel, coupl, tdip, olap = self.last_step
                self.pes.resetSCF()
            else:
                # If it fails in the first step, there is something wrong
                raise e

        return energies, accel, 0 * coupl, 0 * tdip, olap

    def getAll(self, coordinates, state):
        x = np.ravel(coordinates).real
        try:
            energies, gradTot = self.pes.getEnergiesAndGradient(x, state)
        except (SCFNotConverged, ExcitedStatesNotConverged,
                ExcitedStatesError) as e:
            # The SCF calculation has not converged or TDDFT has broken down
            # probably because we have hit a conical
            # intersection or because the molecule has dissociated.
            # We try to continue on the ground state until we have passed the CI.
            print "WARNING: %s" % e
            print "Trying to continue with ground state gradient, energies=E0 and zero coupling!"
            if self.last_step != None or (state == 0):
                energies, accel, coupl, tdip, olap = self.getAll_S0(
                    coordinates, has_scf=True)
                self.pes.resetXY()
                return energies, accel, 0 * coupl, 0 * tdip, olap
            else:
                # If the TD-DFT calculation fails already in the first step, there must be a serious problem
                raise e

        coupl, olap = self.pes.getScalarCouplings(threshold=self.sc_threshold)
        # olap = <Psi_A(t)|Psi_B(t+dt)>
        # coupl = <Psi_A|d/dR Psi_B>*dR/dt * dt
        # divide by nuclear time step
        coupl /= self.dt_nuc
        #
        tdip = self.pes.getTransitionDipoles()
        # convert everything into the format expected by Jens' program
        accel = -gradTot / self.masses
        accel = np.reshape(accel, (self.Nat, 3))
        # save results...
        self.last_step = energies, accel, coupl, tdip, olap
        # ...and return them
        return energies, accel, coupl, tdip, olap

    #############
    # TIME SERIES:
    # To analyze trajectories it is helpful to write out additional quantities along the trajectory.
    #
    # calculate quantities along the trajectory
    def writeTimeSeries(self, fish, time_series=[]):
        atomlist = self.pes.tddftb.dftb2.getGeometry()
        Nat = len(atomlist)  # number of QM atoms
        x = XYZ.atomlist2vector(atomlist)
        coordinates = np.reshape(x, (Nat, 3))
        symbols = [AtomicData.atom_names[Z - 1] for (Z, pos) in atomlist]
        # fish is an instance of the class fish.moleculardynamics
        if "particle-hole charges" in time_series:
            # ph charges are weighted by quantum populations |C(I)|^2
            particle_charges, hole_charges = self.getAvgParticleHoleCharges(
                fish.c)
            # append charges to file
            outfile = open("particle_hole_charges.xyz", "a")
            outfile.write("%d\n" % Nat)
            outfile.write(" time: %s fs\n" % (fish.time / fish.fs_to_au))
            tmp = fish.au_to_ang * coordinates
            for i in range(0, Nat):
                outfile.write("%s  %20.12f  %20.12f  %20.12f      %5.7f %5.7f\n" \
                              %(symbols[i],tmp[i][0],tmp[i][1],tmp[i][2],particle_charges[i], hole_charges[i]))
            outfile.close()
        if "particle-hole charges current" in time_series:
            # ph charges of the current electronic state
            if fish.state == 0:
                # ground state
                particle_charges = np.zeros(Nat)
                hole_charges = np.zeros(Nat)
            else:
                # excited state
                particle_charges, hole_charges = self.pes.tddftb.ParticleHoleCharges(
                    fish.state - 1)
            # append charges to file
            outfile = open("particle_hole_charges_current.xyz", "a")
            outfile.write("%d\n" % Nat)
            outfile.write(" time: %s fs\n" % (fish.time / fish.fs_to_au))
            tmp = fish.au_to_ang * coordinates
            for i in range(0, Nat):
                outfile.write("%s  %20.12f  %20.12f  %20.12f      %5.7f %5.7f\n" \
                              %(symbols[i],tmp[i][0],tmp[i][1],tmp[i][2],particle_charges[i], hole_charges[i]))
            outfile.close()
        if "Lambda2 current" in time_series:
            if fish.state == 0:
                # ground state, Lambda2 descriptor is only defined for excited states
                Lambda2 = -1.0
            else:
                # excited state
                Lambda2 = self.pes.tddftb.Lambda2[fish.state - 1]

            if not os.path.isfile("lambda2_current.dat"):
                outfile = open("lambda2_current.dat", "w")
                # write header
                print >> outfile, "# TIME / fs       LAMBDA2 "
                print >> outfile, "#              0  -  charge transfer state"
                print >> outfile, "#              1  -  local excitation     "
                print >> outfile, "#             -1  -  ground state (Lambda2 undefined)"
            else:
                outfile = open("lambda2_current.dat", "a")

            print >> outfile, "%14.8f    %+7.4f" % (fish.time / fish.fs_to_au,
                                                    Lambda2)
            outfile.close()
        if "Lambda2" in time_series:
            # Lambda2 descriptors for all excited states
            if not os.path.isfile("lambda2.dat"):
                outfile = open("lambda2.dat", "w")
                # write header
                print >> outfile, "# TIME / fs       LAMBDA2's OF EXCITED STATES"
                print >> outfile, "#              0  -  charge transfer state"
                print >> outfile, "#              1  -  local excitation     "
                print >> outfile, "#             -1  -  ground state (Lambda2 undefined)"
            else:
                outfile = open("lambda2.dat", "a")

            print >> outfile, "%14.8f    " % (fish.time / fish.fs_to_au),
            for I in range(1, self.nstates):
                print >> outfile, "  %+7.4f" % (self.pes.tddftb.Lambda2[I -
                                                                        1]),
            print >> outfile, ""

            outfile.close()
        if "transition charges current" in time_series:
            # transition charges for S0 -> current electronic state S_n
            if fish.state == 0:
                # ground state
                transition_charges = np.zeros(Nat)
            else:
                # excited state
                transition_charges = self.pes.tddftb.TransitionChargesState(
                    fish.state - 1)
            # append charges to file
            outfile = open("transition_charges_current.xyz", "a")
            outfile.write("%d\n" % Nat)
            outfile.write(" time: %s fs\n" % (fish.time / fish.fs_to_au))
            tmp = fish.au_to_ang * coordinates
            for i in range(0, Nat):
                outfile.write("%s  %20.12f  %20.12f  %20.12f      %5.7f\n" \
                              %(symbols[i],tmp[i][0],tmp[i][1],tmp[i][2], transition_charges[i]))
            outfile.close()

    def getAvgParticleHoleCharges(self, C):
        """
        This function computes the state-averaged particle and hole charges. 
        The density difference between and excited state and the ground state can be divided into 
        particle charges rho_p and hole charges rho_h so that
           d rho^I = rho^I - rho^0 = rho^I_p + rho^I_h
        The state averaged densities are
             ___
           d rho = sum_I |C_I|^2 * d rho^I

        and 
            ___
            rho_p/h = sum_I |C_I|^2 * d rho^I_p/h

        In tight-binding DFT The particle and hole charges are represented as spherical charge fluctuations
        around the atoms, so that it is enough to give the p-h charges on each atom
   
        Parameters:
        ===========
        C: coefficients of electronic wavefunction

        Returns:
        ========
        particle_charges, hole_charges: lists with the charges for each atom
        """
        # particle hole charges can only be calculated for QM atoms, not for
        # MM atoms. In a QM/MM calculation self.Nat is the total number of atoms,
        # but we need the number of QM atoms.
        atomlist = self.pes.tddftb.dftb2.getGeometry()
        Nat = len(atomlist)  # number of QM atoms
        particle_charges = np.zeros(Nat)
        hole_charges = np.zeros(Nat)
        for I in range(1, self.nstates):
            # I = 0 is ground state
            dqI_p, dqI_h = self.pes.tddftb.ParticleHoleCharges(I - 1)
            # weight of charges is probability to be on that state
            wI = abs(C[I])**2
            particle_charges += wI * dqI_p
            hole_charges += wI * dqI_h
        # charges should sum to 0
        assert abs(np.sum(particle_charges + hole_charges)) < 1.0e-10

        return particle_charges, hole_charges
Example #5
0
    """ % basename(sys.argv[0])

    args = sys.argv[1:]

    if len(args) < 3:
        print(usage)
        exit(-1)

    xyz_file = args[0]  # path to xyz-file
    state1 = int(args[1])  # index of lower electronic state
    state2 = int(args[2])  # index of upper electronic state

    # load initial geometry, we take the last geometry, so it is
    # easier to restart a previous MECI calculation.
    atomlist0 = XYZ.read_xyz(xyz_file)[-1]
    # read the charge of the molecule from the comment line in the xyz-file
    kwds = XYZ.extract_keywords_xyz(xyz_file)
    # initialize the TD-DFTB calculator
    pes = PotentialEnergySurfaces(atomlist0, Nst=state2 + 1, **kwds)

    meci = MECI(
        atomlist0,
        pes,
        #coord_system='internal',
        coord_system='cartesian',
        state1=state1,
        state2=state2)

    meci.opt(step_size=0.1, gtol=0.001)
Example #6
0
        print "  (including ground state) to the dat-file"
        print "  type --help to see all options"
        print "  to reduce the amount of output add the option --verbose=0"
        exit(-1)

    #
    xyz_file = sys.argv[1]  # path to xyz-file
    Nst = int(sys.argv[2])
    dat_file = sys.argv[3]  # output file
    # Read the geometry from the xyz-file
    atomlists = XYZ.read_xyz(xyz_file)
    atomlist = atomlists[0]
    # read the charge of the molecule from the comment line in the xyz-file
    kwds = XYZ.extract_keywords_xyz(xyz_file)
    # initialize the TD-DFTB calculator
    pes = PotentialEnergySurfaces(atomlist, Nst=Nst, **kwds)

    fh = open(dat_file, "w")
    for i, atomlist in enumerate(atomlists):
        print "SCAN geometry %d of %d" % (i + 1, len(atomlists))
        # convert geometry to a vector
        x = XYZ.atomlist2vector(atomlist)
        try:
            if Nst == 1:
                ens = pes.getEnergy_S0(x)
            else:
                ens = pes.getEnergies(x)
        except ExcitedStatesError as e:
            print "WARNING: %s" % e
            print "%d-th point is skipped" % i
            continue
Example #7
0
    #
    xyz_file = sys.argv[1]  # path to xyz-file
    I = int(sys.argv[2])  # index of electronic state
    # Should the Hessian be calculated as well?
    calc_hessian = False
    if len(sys.argv) > 3:
        # optional 3rd argument
        if sys.argv[3].upper() == "H":
            calc_hessian = True
    # Read the geometry from the xyz-file
    atomlist = XYZ.read_xyz(xyz_file)[0]
    # read the charge of the molecule from the comment line in the xyz-file
    kwds = XYZ.extract_keywords_xyz(xyz_file)
    # initialize the TD-DFTB calculator
    pes = PotentialEnergySurfaces(atomlist, Nst=max(I + 1, 2), **kwds)

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

    # FIND ENERGY MINIMUM
    # f is the objective function that should be minimized
    # it returns (f(x), f'(x))
    def f(x):
        #
        if I == 0 and type(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 = pes.getEnergyAndGradient_S0(x)
Example #8
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)