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
""" % 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)
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
# 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)
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)