def expected_zero_modes(x0, masses): """ How many eigen values of the Hessian should be zero because of the structure of the molecule (atom, linear, polyatomic)? """ # assume that coordinate vector with 3*N components belongs to a molecule # with N atoms assert len(x0) % 3 == 0 Nat = len(x0) / 3 # shift the origin to the center of mass x0_shift = MolCo.shift_to_com(x0, masses) # diagonalize the tensor of inertia to obtain the principal moments and # the normalized eigenvectors of I Inert = MolCo.inertial_tensor(masses, x0_shift) principle_moments, X = la.eigh(Inert) # check that the number of rotational and translational modes # is what is expected: # single atom => 3 translational modes only = 3 # dimer or linear molecule => 3 translational and 2 rotational modes = 5 # otherwise => 3 translational and 3 rotational modes = 6 # If the molecule is linear two of the principle moments of inertia # will be zero Ntrans = 3 # number of translational Nrot = 3 # and rotational modes which should be very close to zero is_linear = False pmom_abs = np.sort(abs(principle_moments)) # In a linear triatomic molecule we have Icc = Ibb > Iaa = 0 if abs(pmom_abs[2] - pmom_abs[1]) < 1.0e-6 and abs(pmom_abs[0]) < 1.0e-6: is_linear = True print("Molecule is linear") if Nat == 1: Nrot = 0 elif is_linear == True or Nat == 2: Nrot = 2 else: Nrot = 3 return (Ntrans + Nrot)
def transform_gradient(self, x, g_cart, max_iter=200): """ transform cartesian gradient g_cart = dE/dx to redundant internal coordinates according to B^T g_intern = g_cart The underdetermined system of linear equations is solved in a least square sense. """ if self.verbose > 0: print("transform gradient to internal coordinates") print(" dE/dx -> dE/dq") # Since the energy of a molecule depends only on the internal # coordinates q it should be possible to transform the cartesian # gradient exactly into internal coordinates according to # dE/dx(i) = sum_j dE/dq(j) dq(j)/dx(i) # = sum_j (B^T)_{i,j} dE/dq(j) # # cartesian force f_cart = -g_cart # cartesian accelerations a_cart = f_cart / self.masses # cartesian velocity dt = 1.0 vel = a_cart * dt # shift position to center of mass x = MolCo.shift_to_com(x, self.masses) # eliminate rotation and translation from a_cart I = MolCo.inertial_tensor(self.masses, x) L = MolCo.angular_momentum(self.masses, x, vel) P = MolCo.linear_momentum(self.masses, vel) omega = MolCo.angular_velocity(L, I) vel = MolCo.eliminate_translation(self.masses, vel, P) vel = MolCo.eliminate_rotation(vel, omega, x) # Check that the total linear momentum and angular momentum # indeed vanish. assert la.norm(MolCo.linear_momentum(self.masses, vel)) < 1.0e-14 assert la.norm(MolCo.angular_momentum(self.masses, x, vel)) < 1.0e-14 # go back from velocities to gradient g_cart = -vel / dt * self.masses # solve B^T . g_intern = g_cart by linear least squares. ret = sla.lstsq(self.B0.transpose(), g_cart, cond=self.cond_threshold) g_intern = ret[0] if self.verbose > 1: print(self._table_gradients(g_cart, g_intern)) # check solution err = la.norm(np.dot(self.B0.transpose(), g_intern) - g_cart) assert err < 1.0e-10, "|B^T.g_intern - g_cart|= %e" % err # Abort if gradients are not reasonable gnorm = la.norm(g_intern) if gnorm > 1.0e5: raise RuntimeError( "ERROR: Internal gradient is too large |grad|= %e" % gnorm) # Components of gradient belonging to frozen internal # coordinates are zeroed to avoid changing them. g_intern[self.frozen_internals] = 0.0 # Simply setting some components of the gradient to zero # will most likely lead to inconsistencies if the coordinates # are coupled (Maybe it's impossible to change internal coordinate X # without changing coordinate Y at the same time). These # inconsistencies are removed by applying the projection # operator repeatedly until the components of the frozen # internal coordinates have converged to 0. if self.verbose > 0: print(" apply projector P=G.G- to internal gradient") # The projector is updated everytime cartesian2internal(...) # is called. proj = self.P0 for i in range(0, max_iter): g_intern = np.dot(proj, g_intern) # gradient along frozen coordinates should be zero gnorm_frozen = la.norm(g_intern[self.frozen_internals]) if self.verbose > 0: print(" Iteration= %4.1d |grad(frozen)|= %s" % (i, gnorm_frozen)) if gnorm_frozen < 1.0e-10: break else: g_intern[self.frozen_internals] = 0.0 else: if gnorm_frozen < 1.0e-5: print( "WARNING: Projection of gradient vector in internal coordinates did not converge!" ) print( " But |grad(frozen)|= %e is not too large, so let's continue anyway." % gnorm_frozen) else: raise RuntimeError( "ERROR: Projection of gradient vector in internal coordinates did not converge! |grad(frozen)|= %e" % gnorm_frozen) if self.verbose > 1: print( "gradients after applying projector (only internal gradient changes)" ) print(self._table_gradients(g_cart, g_intern)) return g_intern
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)