if len(args) < 2: print usage exit(-1) xyz_file = args[0] hess_file = args[1] # should be options # optimized geometry atomlist = XYZ.read_xyz(xyz_file)[-1] xopt = XYZ.atomlist2vector(atomlist) # load hessian hess = np.loadtxt(hess_file) masses = AtomicData.atomlist2masses(atomlist) vib_freq, vib_modes = HarmonicApproximation.vibrational_analysis(xopt, hess, masses, \ zero_threshold=opts.zero_threshold, is_molecule=True) # SAMPLE INITIAL CONDITIONS FROM WIGNER DISTRIBUTION qs, ps = HarmonicApproximation.initial_conditions_wigner( xopt, hess, masses, Nsample=opts.Nsample, zero_threshold=opts.zero_threshold) # make hydrogens slower for i in range(0, opts.Nsample): for A, (Z, pos) in enumerate(atomlist): if Z == 1: ps[3 * A:3 * (A + 1), i] *= 0.001 # # STATISTICS
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 """