def __init__(self, atomlist): # convert the list of atoms into a list of 3d points nat = len(atomlist) pos = XYZ.atomlist2vector(atomlist) points = [] for i in range(0, nat): points.append(pos[3 * i:3 * (i + 1)]) # QHull needs at least 4 point to construct the initial # simplex. if nat < 4: # If there are less than 4 atoms, # we add the center of mass as an additional point masses = AtomicData.atomlist2masses(atomlist) com = MolCo.center_of_mass(masses, pos) points.append(com) if nat < 3: # If there are less than 3 atoms we add # an arbitrary point on the x-axis points.append(com + np.array([0.0005, 0.0, 0.0])) if nat < 2: # If there is only one atom, we add another arbitrary # point on the y-axis points.append(com + np.array([0.0, 0.0005, 0.0])) # We add small random numbers to the input coordinates, so that # we get a 3D convex hull even if the molecule is planar points = np.array(points) + 0.0001 * np.random.rand(len(points), 3) # find the convex hull using the qhull code hull = ConvexHull(points, qhull_options="QbB Qt") # call the constructor of the parent class (MinimalEnclosingBox) super(MoleculeBox, self).__init__(hull)
def partition_kinetic_energy(atomlists, dt): Nt = len(atomlists) # determine the indeces of the molecular fragments at the first # time-step. fragments_graph = MolecularGraph.atomlist2graph(atomlists[0]) fragments_indeces = [ MolecularGraph.graph2indeces(g) for g in fragments_graph ] fragment_atomlists = [[[atomlist[ii] for ii in I] for atomlist in atomlists] for I in fragments_indeces] # compute vibrational and center of mass kinetic energy for each fragment Nfrag = len(fragment_atomlists) ekin_com = np.zeros((Nt, Nfrag)) ekin_tot = np.zeros((Nt, Nfrag)) for f in range(0, Nfrag): masses = AtomicData.atomlist2masses(fragment_atomlists[f][0]) print((fragment_atomlists[f][0])) # total mass M = np.sum(masses) / 3.0 positions, velocities = velocities_finite_diff(fragment_atomlists[f], dt) for i in range(0, Nt): vm = velocities[i] * masses vel_com = np.array( [np.sum(vm[0::3]), np.sum(vm[1::3]), np.sum(vm[2::3])]) / M ekin_com_f = 1.0 / 2.0 * M * np.sum(vel_com**2) ekin_tot_f = 1.0 / 2.0 * np.sum(masses * velocities[i]**2) ekin_com[i, f] += ekin_com_f ekin_tot[i, f] += ekin_tot_f return ekin_com, ekin_tot
def molecular_frame_transformation(atomlist): """ The molecule is shifted to the center of mass and its principle axes of inertia are aligned with the coordinate axes. This standard orientation defines the molecular frame. The translation vector and Euler angles needed to transform the geometry from the molecular frame to the original frame are also returned. Returns: ======== atomlist_std: molecular geometry in standard orientation (a,b,g): Euler angles in z-y-z convention cm: 3D vector with center of mass """ pos = XYZ.atomlist2vector(atomlist) masses = AtomicData.atomlist2masses(atomlist) # shift center of mass to origin Nat = len(atomlist) pos_std = np.zeros(pos.shape) cm = center_of_mass(masses, pos) for i in range(0, Nat): pos_std[3*i:3*i+3] = pos[3*i:3*i+3] - cm (a,b,g) = euler_angles_inertia(masses, pos_std) R = EulerAngles2Rotation(a,b,g) for i in range(0, Nat): pos_std[3*i:3*i+3] = np.dot(R, pos_std[3*i:3*i+3]) atomlist_std = XYZ.vector2atomlist(pos_std, atomlist) # The MolecularCoord module uses a strange convention for Euler angles. # Therefore we extract the Euler angles in z-y-z convention directly # from the rotation matrix, that rotates the molecule from the standard # orientation into the original orientation Rinv = R.transpose() a,b,g = rotation2EulerAngles(Rinv, convention="z-y-z") return atomlist_std, (a,b,g), cm
def wigner_from_G09_hessian(g09_file, Nsample=100, zero_threshold=1.0e-9): """ create Wigner ensemble based on hessian matrix from Gaussian 09 calculation """ suffix = g09_file.split(".")[-1] if suffix in ["out", "log"]: print("Reading Gaussian 09 log file %s" % g09_file) atomlist = Gaussian.read_geometry(g09_file) forces = Gaussian.read_forces(g09_file) hess = Gaussian.read_force_constants(g09_file) elif suffix in ["fchk"]: print("Reading formatted Gaussian 09 checkpoint file %s" % g09_file) Data = Checkpoint.parseCheckpointFile(g09_file) # cartesian coordinates pos = Data["_Current_cartesian_coordinates"] atnos = Data["_Atomic_numbers"] # forces frc = -Data["_Cartesian_Gradient"] atomlist = [] forces = [] for i, Zi in enumerate(atnos): atomlist.append((Zi, tuple(pos[3 * i:3 * (i + 1)]))) forces.append((Zi, tuple(frc[3 * i:3 * (i + 1)]))) # Hessian hess = Data["_Cartesian_Force_Constants"] masses = np.array(AtomicData.atomlist2masses(atomlist)) x0 = XYZ.atomlist2vector(atomlist) x0 = shift_to_com(x0, masses) grad = -XYZ.atomlist2vector(forces) grad_nrm = la.norm(grad) print(" gradient norm = %s" % grad_nrm) # assert grad_nrm < 1.0e-3, "Gradient norm too large for minimum!" vib_freq, vib_modes = vibrational_analysis(hess, masses, zero_threshold=zero_threshold) Aw, Bw = wigner_distribution(x0, hess, masses, zero_threshold=zero_threshold) gw = GaussianWavepacket.Gaussian(Aw, Bw) qs, ps = gw.sample(Nsample) mx = np.outer(masses, np.ones(Nsample)) * qs avg_com = np.mean(np.sum(mx[::3, :], axis=0)), np.mean( np.sum(mx[1::3, :], axis=0)), np.mean(np.sum(mx[2::3, :], axis=0)) print(avg_com) geometries = [ XYZ.vector2atomlist(qs[:, i], atomlist) for i in range(0, Nsample) ] return geometries
def dynamics_in_format(atomlist, q, p, fname): atomlist_q = XYZ.vector2atomlist(q, atomlist) masses = AtomicData.atomlist2masses(atomlist) Nat = len(atomlist_q) txt = "%d\n" % Nat # positions in bohr for Zi, (x, y, z) in atomlist_q: atname = AtomicData.atom_names[Zi - 1] txt += "%4s %+10.15f %+10.15f %+10.15f\n" % (atname, x, y, z) # velocities in bohr/s vel = p / masses for i in range(0, Nat): vx, vy, vz = vel[3 * i:3 * (i + 1)] txt += " %+10.15f %+10.15f %+10.15f\n" % (vx, vy, vz) return txt
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 cut_sphere(atomlist, R=20.0): """ remove all atoms outside sphere Parameters: =========== atomlist R: radius of sphere in bohr Returns: ======== atomlist with atoms inside sphere """ # shift to center of mass masses = AtomicData.atomlist2masses(atomlist) pos = XYZ.atomlist2vector(atomlist) pos_shifted = MolecularCoords.shift_to_com(pos, masses) atomlist = XYZ.vector2atomlist(pos_shifted, atomlist) Con = connectivity(atomlist) print("recursively remove connected atoms...") removed = [] # list of removed indeces for i,(Zi,posi) in enumerate(atomlist): print("i = %s" % i) if la.norm(posi) > R: if not (i in removed): print("remove %s%d" % (AtomicData.atom_names[Zi-1], i)) removed.append(i) # remove connect atoms connected = find_connected(Con, i) print("and connected atoms %s" % connected) removed += connected removed = set(removed) cut_atomlist = [] for i,(Zi,posi) in enumerate(atomlist): if i in removed: pass else: cut_atomlist.append( (Zi, posi) ) return cut_atomlist
def qmf3_in_format(atomlist, q, p, fname): """ Save initial conditions in the format expected by Matthias' QMF3 program """ atomlist_q = XYZ.vector2atomlist(q, atomlist) masses = AtomicData.atomlist2masses(atomlist) Nat = len(atomlist_q) txt = "$coordinates [\n" # positions in bohr for Zi, (x, y, z) in atomlist_q: atname = AtomicData.atom_names[Zi - 1] txt += "%4s %+10.15f %+10.15f %+10.15f\n" % (atname, x, y, z) txt += "]\n" # velocities in bohr/s vel = p / masses txt += "$velocities [\n" for i in range(0, Nat): vx, vy, vz = vel[3 * i:3 * (i + 1)] txt += " %+10.15f %+10.15f %+10.15f\n" % (vx, vy, vz) txt += "]\n" txt += "$end\n" return txt
(opts, args) = parser.parse_args() 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
def __init__(self, atomlist, freeze=[], explicit_bonds=[], verbose=0): """ setup system of internal coordinates using valence bonds, angles and dihedrals Parameters ---------- atomlist : list of tuples (Z,[x,y,z]) with molecular geometry, connectivity defines the valence coordinates Optional -------- freeze : list of tuples of atom indices (starting at 0) corresponding to internal coordinates that should be frozen explicit_bonds : list of pairs of atom indices (starting at 0) between which artificial bonds should be inserted, i.e. [(0,1), (10,20)]. This allows to connect separate fragments. verbose : write out additional information if > 0 """ self.verbose = verbose self.atomlist = atomlist self.masses = AtomicData.atomlist2masses(self.atomlist) # Bonds, angles and torsions are constructed by the force field. # Atom types, partial charges and lattice vectors # all don't matter, so we assign atom type 6 (C_R, carbon in resonance) # to all atoms. atomtypes = [6 for atom in atomlist] partial_charges = [0.0 for atom in atomlist] lattice_vectors = [[0.0, 0.0, 0.0], [0.0, 0.0, 0.0], [0.0, 0.0, 0.0]] # But since the covalent radii are wrong, we have to provide # the connectivity matrix conmat = XYZ.connectivity_matrix(atomlist, hydrogen_bonds=True) # insert artificial bonds for (I, J) in explicit_bonds: print("explicit bond between atoms %d-%d" % (I + 1, J + 1)) conmat[I, J] = 1 # Internal coordinates only work if the molecule does not # contain disconnected fragments, since there is no way how the # interfragment distance could be expressed in terms of internal coordinates. # We need to check that there is only a single fragment. fragment_graphs = MolecularGraph.atomlist2graph(self.atomlist, conmat=conmat) nr_fragments = len(fragment_graphs) error_msg = "The molecule consists of %d disconnected fragments.\n" % nr_fragments error_msg += "Internal coordinates only work if all atoms in the molecular graph are connected.\n" error_msg += "Disconnected fragments may be joined via an artificial bond using the\n" error_msg += "`explicit_bonds` option.\n" assert nr_fragments == 1, error_msg # Frozen degrees of freedom do not necessarily correspond to physical bonds # or angles. For instance we can freeze the H-H distance in water although there # is no bond between the hydrogens. To allow the definition of such 'unphysical' # internal coordinates, we have to modify the connectivity matrix and introduce # artificial bonds. for IJKL in freeze: if len(IJKL) == 2: I, J = IJKL # create artificial bond between atoms I and J conmat[I, J] = 1 elif len(IJKL) == 3: I, J, K = IJKL # create artifical bonds I-J and J-K so that the valence angle I-J-K exists conmat[I, J] = 1 conmat[J, K] = 1 elif len(IJKL) == 4: I, J, K, L = IJKL # create artifical bonds I-J, J-K and K-L so that the dihedral angle I-J-K-L # exists conmat[I, J] = 1 conmat[J, K] = 1 conmat[K, L] = 1 # cutoff for small singular values when solving the # linear system of equations B.dx = dq in a least square # sense. self.cond_threshold = 1.0e-10 self.force_field = PeriodicForceField(atomlist, atomtypes, partial_charges, lattice_vectors, [], connectivity_matrix=conmat, verbose=1) x0 = XYZ.atomlist2vector(atomlist) # shift molecule to center of mass self.x0 = MolCo.shift_to_com(x0, self.masses) self._selectActiveInternals(freeze=freeze)
def __init__(self, xyz_file, dyson_file=None): super(Main, self).__init__() self.settings = Settings({ "Continuum Orbital": { "Ionization transitions": [0, ["only intra-atomic", "inter-atomic"]] }, "Averaging": { "Euler angle grid points": 5, "polar angle grid points": 1000, "sphere radius Rmax": 300.0, }, "Scan": { "nr. points": 20 }, "Cube": { "extra space / bohr": 15.0, "points per bohr": 3.0 } }) # perform DFTB calculation # BOUND ORBITAL = H**O self.atomlist = XYZ.read_xyz(xyz_file)[0] # shift molecule to center of mass print "shift molecule to center of mass" pos = XYZ.atomlist2vector(self.atomlist) masses = AtomicData.atomlist2masses(self.atomlist) pos_com = MolCo.shift_to_com(pos, masses) self.atomlist = XYZ.vector2atomlist(pos_com, self.atomlist) self.tddftb = LR_TDDFTB(self.atomlist) self.tddftb.setGeometry(self.atomlist, charge=0) options = {"nstates": 1} try: self.tddftb.getEnergies(**options) except DFTB.Solver.ExcitedStatesNotConverged: pass self.valorbs, radial_val = load_pseudo_atoms(self.atomlist) if dyson_file == None: # Kohn-Sham orbitals are taken as Dyson orbitals self.H**O, self.LUMO = self.tddftb.dftb2.getFrontierOrbitals() self.bound_orbs = self.tddftb.dftb2.getKSCoefficients() self.orbe = self.tddftb.dftb2.getKSEnergies() orbital_names = [] norb = len(self.orbe) for o in range(0, norb): if o < self.H**O: name = "occup." elif o == self.H**O: name = "H**O" elif o == self.LUMO: name = "LUMO " else: name = "virtual" name = name + " " + str(o).rjust(4) + ( " %+10.3f eV" % (self.orbe[o] * 27.211)) orbital_names.append(name) initially_selected = self.H**O else: # load coefficients of Dyson orbitals from file names, ionization_energies, self.bound_orbs = load_dyson_orbitals( dyson_file) self.orbe = np.array(ionization_energies) / 27.211 orbital_names = [] norb = len(self.orbe) for o in range(0, norb): name = names[o] + " " + str(o).rjust(4) + ( " %4.2f eV" % (self.orbe[o] * 27.211)) orbital_names.append(name) initially_selected = 0 self.photo_kinetic_energy = slako_tables_scattering.energies[0] self.epol = np.array([15.0, 0.0, 0.0]) # Build Graphical User Interface main = QtGui.QWidget() mainLayout = QtGui.QHBoxLayout(main) # selectionFrame = QtGui.QFrame() selectionFrame.setSizePolicy(QtGui.QSizePolicy.Fixed, QtGui.QSizePolicy.Preferred) mainLayout.addWidget(selectionFrame) selectionLayout = QtGui.QVBoxLayout(selectionFrame) # label = QtGui.QLabel(selectionFrame) label.setText("Select bound MO:") selectionLayout.addWidget(label) # bound orbitals self.orbitalSelection = QtGui.QListWidget(selectionFrame) self.orbitalSelection.itemSelectionChanged.connect( self.selectBoundOrbital) norb = len(self.orbe) self.orbital_dict = {} for o in range(0, norb): name = orbital_names[o] self.orbital_dict[name] = o item = QtGui.QListWidgetItem(name, self.orbitalSelection) if o == initially_selected: selected_orbital_item = item selectionLayout.addWidget(self.orbitalSelection) ### VIEWS center = QtGui.QWidget() mainLayout.addWidget(center) centerLayout = QtGui.QGridLayout(center) # boundFrame = QtGui.QFrame() centerLayout.addWidget(boundFrame, 1, 1) boundLayout = QtGui.QVBoxLayout(boundFrame) # "Bound Orbital" label = QtGui.QLabel(boundFrame) label.setText("Bound Orbital") boundLayout.addWidget(label) # self.boundOrbitalViewer = QCubeViewerWidget(boundFrame) boundLayout.addWidget(self.boundOrbitalViewer) # continuum orbital continuumFrame = QtGui.QFrame() centerLayout.addWidget(continuumFrame, 1, 2) continuumLayout = QtGui.QVBoxLayout(continuumFrame) # "Dipole-Prepared Continuum Orbital" label = QtGui.QLabel(continuumFrame) label.setText("Dipole-Prepared Continuum Orbital") continuumLayout.addWidget(label) self.continuumOrbitalViewer = QCubeViewerWidget(continuumFrame) continuumLayout.addWidget(self.continuumOrbitalViewer) self.efield_objects = [] self.efield_actors = [] self.selected = None # picker self.picker = self.continuumOrbitalViewer.visualization.scene.mayavi_scene.on_mouse_pick( self.picker_callback) self.picker.tolerance = 0.01 # PHOTO KINETIC ENERGY sliderFrame = QtGui.QFrame(continuumFrame) continuumLayout.addWidget(sliderFrame) sliderLayout = QtGui.QHBoxLayout(sliderFrame) # label self.pke_label = QtGui.QLabel() self.pke_label.setText("PKE: %6.4f eV" % (self.photo_kinetic_energy * 27.211)) sliderLayout.addWidget(self.pke_label) # Slider for changing the PKE self.pke_slider = QtGui.QSlider(QtCore.Qt.Horizontal) self.pke_slider.setMinimum(0) self.pke_slider.setMaximum(len(slako_tables_scattering.energies) - 1) self.pke_slider.setValue(0) self.pke_slider.sliderReleased.connect(self.changePKE) self.pke_slider.valueChanged.connect(self.searchPKE) sliderLayout.addWidget(self.pke_slider) # # molecular frame photoangular distribution mfpadFrame = QtGui.QFrame() centerLayout.addWidget(mfpadFrame, 2, 1) mfpadLayout = QtGui.QVBoxLayout(mfpadFrame) mfpadLayout.addWidget(QtGui.QLabel("Molecular Frame PAD")) mfpadTabs = QtGui.QTabWidget() mfpadLayout.addWidget(mfpadTabs) # 2D map mfpadFrame2D = QtGui.QFrame() mfpadTabs.addTab(mfpadFrame2D, "2D") mfpadLayout2D = QtGui.QVBoxLayout(mfpadFrame2D) self.MFPADfig2D = Figure() self.MFPADCanvas2D = FigureCanvas(self.MFPADfig2D) mfpadLayout2D.addWidget(self.MFPADCanvas2D) self.MFPADCanvas2D.draw() NavigationToolbar(self.MFPADCanvas2D, mfpadFrame2D, coordinates=True) # 3D mfpadFrame3D = QtGui.QFrame() mfpadTabs.addTab(mfpadFrame3D, "3D") mfpadLayout3D = QtGui.QVBoxLayout(mfpadFrame3D) self.MFPADfig3D = Figure() self.MFPADCanvas3D = FigureCanvas(self.MFPADfig3D) mfpadLayout3D.addWidget(self.MFPADCanvas3D) self.MFPADCanvas3D.draw() NavigationToolbar(self.MFPADCanvas3D, mfpadFrame3D, coordinates=True) # orientation averaged photoangular distribution avgpadFrame = QtGui.QFrame() centerLayout.addWidget(avgpadFrame, 2, 2) avgpadLayout = QtGui.QVBoxLayout(avgpadFrame) self.activate_average = QtGui.QCheckBox("Orientation Averaged PAD") self.activate_average.setToolTip( "Check this box to start averaging of the molecular frame PADs over all orientations. This can take a while." ) self.activate_average.setCheckState(QtCore.Qt.Unchecked) self.activate_average.stateChanged.connect(self.activateAveragedPAD) avgpadLayout.addWidget(self.activate_average) avgpadTabs = QtGui.QTabWidget() avgpadLayout.addWidget(avgpadTabs) # 1D map avgpadFrame1D = QtGui.QFrame() avgpadTabs.addTab(avgpadFrame1D, "1D") avgpadLayout1D = QtGui.QVBoxLayout(avgpadFrame1D) self.AvgPADfig1D = Figure() self.AvgPADCanvas1D = FigureCanvas(self.AvgPADfig1D) avgpadLayout1D.addWidget(self.AvgPADCanvas1D) self.AvgPADCanvas1D.draw() NavigationToolbar(self.AvgPADCanvas1D, avgpadFrame1D, coordinates=True) # 2D map avgpadFrame2D = QtGui.QFrame() avgpadFrame2D.setToolTip( "The averaged PAD should have no phi-dependence anymore. A phi-dependence is a sign of incomplete averaging." ) avgpadTabs.addTab(avgpadFrame2D, "2D") avgpadLayout2D = QtGui.QVBoxLayout(avgpadFrame2D) self.AvgPADfig2D = Figure() self.AvgPADCanvas2D = FigureCanvas(self.AvgPADfig2D) avgpadLayout2D.addWidget(self.AvgPADCanvas2D) self.AvgPADCanvas2D.draw() NavigationToolbar(self.AvgPADCanvas2D, avgpadFrame2D, coordinates=True) # Table avgpadFrameTable = QtGui.QFrame() avgpadTabs.addTab(avgpadFrameTable, "Table") avgpadLayoutTable = QtGui.QVBoxLayout(avgpadFrameTable) self.avgpadTable = QtGui.QTableWidget(0, 6) self.avgpadTable.setToolTip( "Activate averaging and move the PKE slider above to add a new row with beta values. After collecting betas for different energies you can save the table or plot a curve beta(PKE) for the selected orbital." ) self.avgpadTable.setHorizontalHeaderLabels( ["PKE / eV", "sigma", "beta1", "beta2", "beta3", "beta4"]) avgpadLayoutTable.addWidget(self.avgpadTable) # Buttons buttonFrame = QtGui.QFrame() avgpadLayoutTable.addWidget(buttonFrame) buttonLayout = QtGui.QHBoxLayout(buttonFrame) deleteButton = QtGui.QPushButton("Delete") deleteButton.setToolTip("clear table") deleteButton.clicked.connect(self.deletePADTable) buttonLayout.addWidget(deleteButton) buttonLayout.addSpacing(3) scanButton = QtGui.QPushButton("Scan") scanButton.setToolTip( "fill table by scanning automatically through all PKE values") scanButton.clicked.connect(self.scanPADTable) buttonLayout.addWidget(scanButton) saveButton = QtGui.QPushButton("Save") saveButton.setToolTip("save table as a text file") saveButton.clicked.connect(self.savePADTable) buttonLayout.addWidget(saveButton) plotButton = QtGui.QPushButton("Plot") plotButton.setToolTip("plot beta2 column as a function of PKE") plotButton.clicked.connect(self.plotPADTable) buttonLayout.addWidget(plotButton) """ # DOCKS self.setDockOptions(QtGui.QMainWindow.AnimatedDocks | QtGui.QMainWindow.AllowNestedDocks) # selectionDock = QtGui.QDockWidget(self) selectionDock.setWidget(selectionFrame) selectionDock.setFeatures(QtGui.QDockWidget.DockWidgetFloatable | QtGui.QDockWidget.DockWidgetMovable) self.addDockWidget(QtCore.Qt.DockWidgetArea(1), selectionDock) # boundDock = QtGui.QDockWidget(self) boundDock.setWidget(boundFrame) boundDock.setFeatures(QtGui.QDockWidget.DockWidgetFloatable | QtGui.QDockWidget.DockWidgetMovable) boundDock.setSizePolicy(QtGui.QSizePolicy.Preferred, QtGui.QSizePolicy.Preferred) self.addDockWidget(QtCore.Qt.DockWidgetArea(2), boundDock) # continuumDock = QtGui.QDockWidget(self) continuumDock.setWidget(continuumFrame) continuumDock.setFeatures(QtGui.QDockWidget.DockWidgetFloatable | QtGui.QDockWidget.DockWidgetMovable) continuumDock.setSizePolicy(QtGui.QSizePolicy.Preferred, QtGui.QSizePolicy.Preferred) self.addDockWidget(QtCore.Qt.DockWidgetArea(2), continuumDock) """ self.setCentralWidget(main) self.status_bar = QtGui.QStatusBar(main) self.setStatusBar(self.status_bar) self.default_message = "Click on the tip of the green arrow in the top right figure to change the orientation of the E-field" self.statusBar().showMessage(self.default_message) # Menu bar menubar = self.menuBar() exitAction = QtGui.QAction('&Exit', self) exitAction.setShortcut('Ctrl+Q') exitAction.setStatusTip('Exit program') exitAction.triggered.connect(exit) fileMenu = menubar.addMenu('&File') fileMenu.addAction(exitAction) settingsMenu = menubar.addMenu('&Edit') settingsAction = QtGui.QAction('&Settings...', self) settingsAction.setStatusTip('Edit settings') settingsAction.triggered.connect(self.editSettings) settingsMenu.addAction(settingsAction) self.loadContinuum() # select H**O selected_orbital_item.setSelected(True)
def __init__(self, atomlist, E0, vib_freq, symmetry_group, pressure=AtomicData.atm_pressure, temperature=AtomicData.satp_temperature): """ temperature in Kelvin """ self.E0 = E0 self.P = pressure self.T = temperature self.vib_freq = vib_freq self.symmetry_group = symmetry_group self.atomlist = atomlist Nat = len(atomlist) self.masses = AtomicData.atomlist2masses(self.atomlist) # compute rotational constants from tensor of inertia x0 = XYZ.atomlist2vector(atomlist) # shift the origin to the center of mass x0_shift = MolCo.shift_to_com(x0, self.masses) # diagonalize the tensor of inertia to obtain the principal moments and # the normalized eigenvectors of I Inert = MolCo.inertial_tensor(self.masses, x0_shift) principle_moments, X = la.eigh(Inert) Iaa, Ibb, Icc = np.sort(abs(principle_moments)) print("principle moments of inertia (in a.u.): %s %s %s" % (Iaa, Ibb, Icc)) # In a linear triatomic molecule we have Icc = Ibb > Iaa = 0 self.is_linear = False if abs(Icc - Ibb) / abs(Icc) < 1.0e-6 and abs(Iaa) / abs(Icc) < 1.0e-6: self.is_linear = True print("Molecule is linear") # Determine the type of rotor if self.is_linear == True: self.rotor_type = "linear rotor" else: if abs(Icc - Ibb) / abs(Icc) < 1.0e-4 and abs(Icc - Iaa) / abs( Icc) < 1.0e-4: # three equal moments of inertia self.rotor_type = "spherical rotor" elif abs(Icc - Ibb) / abs(Icc) < 1.0e-4 or abs(Ibb - Iaa) / abs( Icc) < 1.0e-4: # two equal moments of inertia self.rotor_type = "symmetric rotor" else: self.rotor_type = "asymmetric rotor" # rotational constants self.rotational_constants = 1.0 / ( 2.0 * principle_moments + 1.0e-20 ) # avoid division by zero error for a linear molecule, the invalid values are not actually used # symmetry number if symmetry_group == None: print( "Symmetry group unknown, setting rotational symmetry number to 1" ) self.sigma_rot = 1 else: self.sigma_rot = symmetry_group.rotational_symmetry_number() # beta = 1/(kB*T) kB = AtomicData.kBoltzmann self.beta = 1.0 / (kB * self.T)
def _update_visualization(self, atomlist, charges, fragments): """ only change the underlying data but do not recreate visualization. This is faster and avoids jerky animations. """ mlab = self.scene.mlab if self.show_flags["charges"] == True: i = 0 for q,(Z,pos) in zip(charges, atomlist): x,y,z = pos if q < 0.0: color = (0.,0.,1.) elif q > 0.0: color = (1.,0.,0.) else: color = (1.,1.,1.) # color does not work so far txt = "%+2.3f" % q if self.show_flags["labels"] == True: # maybe charges should not overlap with labels txt = "%s" % txt # update label position and text label = self.charge_labels[i] label.remove() label = mlab.text(x,y, txt, z=z, figure=self.scene.mayavi_scene) self.charge_labels[i] = label label.actor.set(text_scale_mode='none', width=0.05, height=0.1) label.property.set(justification='centered', vertical_justification='centered') i += 1 if self.show_flags["frag. charges"] == True: i = 0 for ifrag,(fragment_indeces, fragment_atomlist) in enumerate(fragments): # compute the charges on fragment qfrag = np.sum(charges[fragment_indeces]) print "Fragment charges: %s" % charges[fragment_indeces] # compute the center of the molecule, pos_frag = XYZ.atomlist2vector(fragment_atomlist) masses_frag = AtomicData.atomlist2masses(fragment_atomlist) com = MolCo.center_of_mass(masses_frag, pos_frag) # print "Fragment %d charge = %s" % (ifrag, qfrag) txt = "%+2.3f" % qfrag label = self.frag_charge_labels[i] label.remove() label = mlab.text(com[0],com[1], txt, z=com[2], line_width=0.8, figure=self.scene.mayavi_scene) self.frag_charge_labels[i] = label label.actor.set(text_scale_mode='none', width=0.05, height=0.1) label.property.set(justification='centered', vertical_justification='centered') i += 1 if self.show_flags["charge clouds"] == True: vec = XYZ.atomlist2vector(atomlist) x, y, z = vec[::3], vec[1::3], vec[2::3] s = abs(charges) # The charge clouds represent surfaces of equal charge density around each atoms. # In DFTB the charge fluctuations are modelled by a Gaussian: # F(r) = 1/(2*pi*sA^2)^(3/2) * exp(-r^2/(2*sA^2)) # The radii of the charge clouds are scaled by the charge on the atom: # r = q * r0 # The radius r0 belongs to a charge cloud containing exactly 1 electron, it depends # on the hubbard parameter through sA and on the isoValue: # F(r0) = F(0) * isoValue # r0s = self.charge_cloud_radii.getRadii(atomlist) s *= r0s self.cloud.mlab_source.set(x=x,y=y,z=z,u=s,v=s,w=s, scalars=charges, scale_factor=1.0) # atoms are coloured by their atomic number self.cloud.glyph.color_mode = "color_by_scalar" self.cloud.glyph.glyph_source.glyph_source.center = [0,0,0] self.cloud.module_manager.scalar_lut_manager.lut.table = self.lut self.cloud.module_manager.scalar_lut_manager.data_range = (-1.0, 1.0)
def _create_visualization(self, atomlist, charges, fragments): mlab = self.scene.mlab if self.show_flags["charges"] == True and type(charges) != type(None): self.charge_labels = [] for q,(Z,pos) in zip(charges, atomlist): x,y,z = pos if q < 0.0: color = (0.,0.,1.) elif q > 0.0: color = (1.,0.,0.) else: color = (1.,1.,1.) # color does not work so far txt = "%+2.3f" % q if self.show_flags["labels"] == True: # maybe charges should not overlap with labels txt = "%s" % txt label = mlab.text(x,y, txt, z=z, figure=self.scene.mayavi_scene) label.actor.set(text_scale_mode='none', width=0.05, height=0.1) label.property.set(justification='centered', vertical_justification='centered') self.charge_labels.append( label ) self.shown_charges = True else: self.shown_charges = False if self.show_flags["frag. charges"] == True and type(charges) != type(None): self.frag_charge_labels = [] for ifrag,(fragment_indeces, fragment_atomlist, fragment_box) in enumerate(fragments): # compute the charges on fragment qfrag = np.sum(charges[fragment_indeces]) print "Fragment charges: %s" % charges[fragment_indeces] # compute the center of the molecule, pos_frag = XYZ.atomlist2vector(fragment_atomlist) masses_frag = AtomicData.atomlist2masses(fragment_atomlist) com = MolCo.center_of_mass(masses_frag, pos_frag) # print "Fragment %d charge = %s" % (ifrag, qfrag) txt = "%+2.3f" % qfrag label = mlab.text(com[0],com[1], txt, z=com[2], line_width=0.8, figure=self.scene.mayavi_scene) label.actor.set(text_scale_mode='none', width=0.05, height=0.1) label.property.set(justification='centered', vertical_justification='centered') self.frag_charge_labels.append( label ) self.shown_frag_charges = True else: self.shown_frag_charges = False if self.show_flags["charge clouds"] == True and type(charges) != type(None): self.charge_clouds = [] vec = XYZ.atomlist2vector(atomlist) x, y, z = vec[::3], vec[1::3], vec[2::3] s = abs(charges) # The charge clouds represent surfaces of equal charge density around each atoms. # In DFTB the charge fluctuations are modelled by a Gaussian: # F(r) = 1/(2*pi*sA^2)^(3/2) * exp(-r^2/(2*sA^2)) # The radii of the charge clouds are scaled by the charge on the atom: # r = q * r0 # The radius r0 belongs to a charge cloud containing exactly 1 electron, it depends # on the hubbard parameter through sA and on the isoValue: # F(r0) = F(0) * isoValue # r0s = self.charge_cloud_radii.getRadii(atomlist) s *= r0s cloud = mlab.quiver3d(x,y,z,s,s,s, scalars=charges, mode="sphere", scale_factor=1.0, resolution=20, opacity = 0.4, figure=self.scene.mayavi_scene) # atoms are coloured by their atomic number cloud.glyph.color_mode = "color_by_scalar" cloud.glyph.glyph_source.glyph_source.center = [0,0,0] self.lut = cloud.module_manager.scalar_lut_manager.lut.table.to_array() red = np.array((255.0, 0.0, 0.0)).astype('uint8') blue = np.array((0.0, 0.0, 255.0)).astype('uint8') for i in range(0, 255): if i < 128: color = blue else: color = red self.lut[i,0:3] = color cloud.module_manager.scalar_lut_manager.lut.table = self.lut cloud.module_manager.scalar_lut_manager.data_range = (-1.0, 1.0) self.cloud = cloud self.charge_clouds.append( cloud ) self.shown_charge_clouds = True else: self.shown_charge_clouds = False if self.show_flags["dipole moment"] == True and type(charges) != type(None): self.dipole_vectors = [] # The dipole vector is placed at the center of mass pos = XYZ.atomlist2vector(atomlist) masses = AtomicData.atomlist2masses(atomlist) com = MolCo.center_of_mass(masses, pos) # compute dipole moment from charge distribution dipole = np.zeros(3) for i,q in enumerate(charges): dipole += q * pos[3*i:3*(i+1)] print "Dipole moment D = %s a.u." % dipole # For plotting the dipole vector is converted to Debye dipole *= AtomicData.ebohr_to_debye print "Dipole moment D = %s Debye" % dipole print "Length of dipole moment |D| = %s Debye" % la.norm(dipole) quiver3d = mlab.quiver3d(com[0],com[1],com[2], dipole[0], dipole[1], dipole[2], line_width=5.0, scale_mode='vector', color=(0,0,1), scale_factor=1.0) self.dipole_vectors.append(quiver3d) else: self.shown_dipole_moment = False if self.show_flags["enclosing box"] == True: self.frag_enclosing_boxes = [] for ifrag,(fragment_indeces, fragment_atomlist, fragment_box) in enumerate(fragments): box = fragment_box # plot edges of the enclosing box for edge in box.edges: l = mlab.plot3d(box.vertices[edge,0], box.vertices[edge,1], box.vertices[edge,2], color=(1,0,0), figure=self.scene.mayavi_scene) self.frag_enclosing_boxes.append(l) # plot axes for axis, color in zip(box.axes, [(1.,0.,0.),(0.,1.,0.), (0.,0.,1.)]): ax = mlab.quiver3d(float(box.center[0]), float(box.center[1]), float(box.center[2]), float(axis[0]), float(axis[1]), float(axis[2]), color=color, scale_factor=3.0, mode='arrow', resolution=20, figure=self.scene.mayavi_scene) self.frag_enclosing_boxes.append(ax) self.shown_enclosing_boxes = True else: self.shown_enclosing_boxes = False if self.show_flags["screening charges (COSMO)"] == True: # read parameter for solvent model from command line # or configuration file parser = OptionParserFuncWrapper([ImplicitSolvent.SolventCavity.__init__], "", unknown_options="ignore") # extract only arguments for constructor of solvent cavity (solvent_options, args) = parser.parse_args(ImplicitSolvent.SolventCavity.__init__) # cavity = ImplicitSolvent.SolventCavity(**solvent_options) cavity.constructSAS(atomlist) area = cavity.getSurfaceArea() print "solvent accessible surface area: %8.6f bohr^2 %8.6f Ang^2" % (area, area*AtomicData.bohr_to_angs**2) points = cavity.getSurfacePoints() x,y,z = points[:,0], points[:,1], points[:,2] if type(charges) != type(None): # If there are Mulliken charges we can compute the # induced charges on the surface of the cavity # according to COSMO cavity.constructCOSMO() induced_charges = cavity.getInducedCharges(charges) screening_energy = cavity.getScreeningEnergy(charges) print "screening energy: %10.6f Hartree %10.6f kcal/mol" \ % (screening_energy, screening_energy * AtomicData.hartree_to_kcalmol) # The surface points are colored and scaled # according to their charge # negative -> blue positive -> red points3d = mlab.points3d(x,y,z,induced_charges, colormap="blue-red", mode='2dcross', scale_factor=10) # mode='2dvertex') points3d.glyph.color_mode = "color_by_scalar" else: # points3d = mlab.points3d(x,y,z,color=(0.0,0.0,0.8), mode='2dvertex') self.sas_points.append( points3d ) else: self.shown_solvent_screening_charges = False if self.show_flags["non-adiab. coupling vectors"] == True: vec = XYZ.atomlist2vector(atomlist) x, y, z = vec[::3], vec[1::3], vec[2::3] # assume that charges are transition charges transition_charges = charges # Because we don't know the energy here, the energy difference # in the denominator is set to 1, so only the direction and relative # lengths are correct. nac = NACsApprox.coupling_vector(atomlist, transition_charges, 1.0) # directions of NAC vectors u,v,w = nac[0,:], nac[1,:], nac[2,:] # Add a NAC vector at each atom quiver3d = mlab.quiver3d(x,y,z, u,v,w, line_width=5.0) self.nac_vectors.append(quiver3d) else: self.shown_nacs = False
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 """
def averaged_pad_scan(xyz_file, dyson_file, selected_orbitals, npts_euler, npts_theta, nskip, inter_atomic, sphere_radius): molecule_name = os.path.basename(xyz_file).replace(".xyz", "") atomlist = XYZ.read_xyz(xyz_file)[-1] # shift molecule to center of mass print "shift molecule to center of mass" pos = XYZ.atomlist2vector(atomlist) masses = AtomicData.atomlist2masses(atomlist) pos_com = MolCo.shift_to_com(pos, masses) atomlist = XYZ.vector2atomlist(pos_com, atomlist) # compute molecular orbitals with DFTB tddftb = LR_TDDFTB(atomlist) tddftb.setGeometry(atomlist, charge=0) options={"nstates": 1} try: tddftb.getEnergies(**options) except DFTB.Solver.ExcitedStatesNotConverged: pass valorbs, radial_val = load_pseudo_atoms(atomlist) if dyson_file == None: print "tight-binding Kohn-Sham orbitals are taken as Dyson orbitals" H**O, LUMO = tddftb.dftb2.getFrontierOrbitals() bound_orbs = tddftb.dftb2.getKSCoefficients() if selected_orbitals == None: # all orbitals selected_orbitals = range(0,bound_orbs.shape[1]) else: selected_orbitals = eval(selected_orbitals, {}, {"H**O": H**O+1, "LUMO": LUMO+1}) print "Indeces of selected orbitals (counting from 1): %s" % selected_orbitals orbital_names = ["orb_%s" % o for o in selected_orbitals] selected_orbitals = np.array(selected_orbitals, dtype=int)-1 # counting from 0 dyson_orbs = bound_orbs[:,selected_orbitals] ionization_energies = -tddftb.dftb2.getKSEnergies()[selected_orbitals] else: print "coeffients for Dyson orbitals are read from '%s'" % dyson_file orbital_names, ionization_energies, dyson_orbs = load_dyson_orbitals(dyson_file) ionization_energies = np.array(ionization_energies) / AtomicData.hartree_to_eV print "" print "*******************************************" print "* PHOTOELECTRON ANGULAR DISTRIBUTIONS *" print "*******************************************" print "" # determine the radius of the sphere where the angular distribution is calculated. It should be # much larger than the extent of the molecule (xmin,xmax),(ymin,ymax),(zmin,zmax) = Cube.get_bbox(atomlist, dbuff=0.0) dx,dy,dz = xmax-xmin,ymax-ymin,zmax-zmin Rmax = max([dx,dy,dz]) + sphere_radius Npts = max(int(Rmax),1) * 50 print "Radius of sphere around molecule, Rmax = %s bohr" % Rmax print "Points on radial grid, Npts = %d" % Npts nr_dyson_orbs = len(orbital_names) # compute PADs for all selected orbitals for iorb in range(0, nr_dyson_orbs): print "computing photoangular distribution for orbital %s" % orbital_names[iorb] data_file = "betas_" + molecule_name + "_" + orbital_names[iorb] + ".dat" pad_data = [] print " SCAN" nskip = max(1, nskip) # save table fh = open(data_file, "w") print " Writing table with betas to %s" % data_file print>>fh, "# ionization from orbital %s IE = %6.3f eV" % (orbital_names[iorb], ionization_energies[iorb]*AtomicData.hartree_to_eV) print>>fh, "# inter_atomic: %s npts_euler: %s npts_theta: %s rmax: %s" % (inter_atomic, npts_euler, npts_theta, Rmax) print>>fh, "# PKE/eV sigma beta1 beta2 beta3 beta4" for i,E in enumerate(slako_tables_scattering.energies): if i % nskip != 0: continue print " PKE = %6.6f Hartree (%4.4f eV)" % (E, E*AtomicData.hartree_to_eV) k = np.sqrt(2*E) wavelength = 2.0 * np.pi/k bs_free = AtomicScatteringBasisSet(atomlist, E, rmin=0.0, rmax=Rmax+2*wavelength, Npts=Npts) SKT_bf, SKT_ff = load_slako_scattering(atomlist, E) Dipole = ScatteringDipoleMatrix(atomlist, valorbs, SKT_bf, inter_atomic=inter_atomic).real orientation_averaging = PAD.OrientationAveraging_small_memory(Dipole, bs_free, Rmax, E, npts_euler=npts_euler, npts_theta=npts_theta) pad,betasE = orientation_averaging.averaged_pad(dyson_orbs[:,iorb]) pad_data.append( [E*AtomicData.hartree_to_eV] + list(betasE) ) # save PAD for this energy print>>fh, "%10.6f %10.6e %+10.6e %+10.6f %+10.6e %+10.6e" % tuple(pad_data[-1]) fh.flush() fh.close()
xyz_file = args[0] # ... Hessian of energy hessian_file = args[1] # output file state_file = args[2] # load minimum geometry and Hessian atomlist = XYZ.read_xyz(xyz_file)[-1] hess = np.loadtxt(hessian_file) print "optimized geometry read from '%s'" % xyz_file print "Hessian read from '%s'" % hessian_file # compute normal modes and frequencies xopt = XYZ.atomlist2vector(atomlist) masses = AtomicData.atomlist2masses(atomlist) # setting zero_threshold to -0.1 makes sure that all frequencies are returned # even if some of them are imaginary vib_freq, vib_modes = HarmonicApproximation.vibrational_analysis(xopt, hess, masses, \ zero_threshold=-0.1, is_molecule=True) # sort frequencies in ascending order vib_freq = vib_freq.real sort_index = np.argsort(abs(vib_freq)**2) vib_freq = vib_freq[sort_index] vib_modes = vib_modes[:, sort_index] # remove lowest 6 vibrations (3 translation + 3 rotation) assuming the molecule # is not linear nat = len(atomlist) nvib = 3 * nat - 6