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, 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
# 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
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)
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()
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