def rotational_constants_validator(cls, value, values): """Species.rotational_constants validator""" label = f' for species "{values["label"]}"' if 'label' in values and values['label'] is not None else '' if value is None and common.get_number_of_atoms(values) > 1: raise ValueError(f'No rotational constants specified{label}.') if value is not None: if common.get_number_of_atoms(values) == 1: raise ValueError(f'Rotational constants were specified for a monoatomic species{label} ({value}).') if 'coordinates' in values and 'coords' in values['coordinates']: linear = is_linear(coordinates=np.array(values['coordinates']['coords'])) if len(value) != 1 and linear: raise ValueError(f'More than one rotational constant was specified for a linear species{label} ' f'({value}).') if len(value) != 3 and not linear: raise ValueError(f'The number of rotational constants for a non-linear species{label} must be 3.\n' f'Got {len(value)} rotational constants: {value}.') return value
def generate_geom_info(spc, xyz_file=None): if not 'geom' in spc: xyz_file = xyz_file or os.path.join(spc['directory'], 'xyz.txt') try: spc['geom'] = parse_xyz_from_file(xyz_file) except: return try: mol = xyz_to_mol(spc['geom']) except: return spc['smiles'] = mol.to_smiles() spc['mol'] = mol.to_adjacency_list() spc['bond_dict'] = enumerate_bonds(mol) spc['atom_dict'] = mol.get_element_count() spc['linear'] = is_linear(coordinates=np.array(spc['geom']['coords'])) spc['external_symmetry'], spc['optical_isomers'] = determine_symmetry( spc['geom']) return spc
def frequencies_validator(cls, value, values): """Species.frequencies validator""" label = f' for species "{values["label"]}"' if 'label' in values and values['label'] is not None else '' if value is None and common.get_number_of_atoms(values) > 1: raise ValueError(f'Frequencies were not given{label}. Frequencies must be specified for polyatomic species.') if value is not None: if any(i == 0 for i in value): raise ValueError(f'A frequency cannot be zero, got {value}{label}.') if values['coordinates'] is not None and value is not None: linear = is_linear(coordinates=np.array(values['coordinates']['coords'])) num_atoms = common.get_number_of_atoms(values) if num_atoms is not None: expected_num_freqs = 3 * num_atoms - (6 - int(linear)) # 3N-6 for non linear, 3N-5 for linear if len(value) != expected_num_freqs: linear_txt = 'linear' if linear else 'non-linear' raise ValueError(f'Expected {expected_num_freqs} frequencies for a {linear_txt} molecule, ' f'got {len(value)} frequencies{label}.') if 'is_ts' in values and values['is_ts'] and all(freq > 0 for freq in value): raise ValueError(f'An imaginary frequency must be present for a TS species. ' f'Got all real frequencies{label}.') return value
def test_is_linear(self): """Test that we can determine the linearity of a molecule from it's coordinates""" xyz1 = np.array([[0.000000, 0.000000, 0.000000], [0.000000, 0.000000, 1.159076], [0.000000, 0.000000, -1.159076]]) # a trivial case xyz2 = np.array([[-0.06618943, -0.12360663, -0.07631983], [-0.79539707, 0.86755487, 1.02675668], [-0.68919931, 0.25421823, -1.34830853], [0.01546439, -1.54297548, 0.44580391], [1.94428095, 0.40772394, 1.03719428], [2.20318015, -0.14715186, -0.64755729], [1.59252246, 1.51178950, -0.33908352], [-0.87856890, -2.02453514, 0.38494433], [-1.34135876, 1.49608206, 0.53295071]]) # a non-linear multi-atom molecule xyz3 = np.array([[0.0000000000, 0.0000000000, 0.3146069129], [-1.0906813653, 0.0000000000, -0.1376405244], [1.0906813653, 0.0000000000, -0.1376405244]]) # NO2, a non-linear 3-atom molecule xyz4 = np.array([[0.0000000000, 0.0000000000, 0.1413439534], [-0.8031792912, 0.0000000000, -0.4947038368], [0.8031792912, 0.0000000000, -0.4947038368]]) # NH2, a non-linear 3-atom molecule xyz5 = np.array([[-0.5417345330, 0.8208150346, 0.0000000000], [0.9206183692, 1.6432038228, 0.0000000000], [-1.2739176462, 1.9692549926, 0.0000000000]]) # HSO, a non-linear 3-atom molecule xyz6 = np.array([[1.18784533, 0.98526702, 0.00000000], [0.04124533, 0.98526702, 0.00000000], [-1.02875467, 0.98526702, 0.00000000]]) # HCN, a linear 3-atom molecule xyz7 = np.array([[-4.02394116, 0.56169428, 0.00000000], [-5.09394116, 0.56169428, 0.00000000], [-2.82274116, 0.56169428, 0.00000000], [-1.75274116, 0.56169428, 0.00000000]]) # C2H2, a linear 4-atom molecule xyz8 = np.array([ [-1.02600933, 2.12845307, 0.00000000], [-0.77966935, 0.95278385, 0.00000000], [-1.23666197, 3.17751246, 0.00000000], [-0.56023545, -0.09447399, 0.00000000] ]) # C2H2, just 0.5 degree off from linearity, so NOT linear xyz9 = np.array( [[-1.1998, 0.1610, 0.0275], [-1.4021, 0.6223, -0.8489], [-1.48302, 0.80682, -1.19946]] ) # just 3 points in space on a straight line (not a physical molecule) xyz10 = np.array([[-1.1998, 0.1610, 0.0275]]) # mono-atomic species, non-linear xyz11 = np.array([[1.06026500, -0.07706800, 0.03372800], [3.37340700, -0.07706800, 0.03372800], [2.21683600, -0.07706800, 0.03372800]]) # CO2 at wb97xd/6-311+g(d,p), linear xyz12 = np.array([[1.05503600, -0.00335000, 0.09823600], [2.42816800, -0.00335000, 0.09823600], [-0.14726400, -0.00335000, 0.09823600], [3.63046800, -0.00335000, 0.09823600], [-1.21103500, -0.00335000, 0.09823600], [4.69423900, -0.00335000, 0.09823600] ]) # C#CC#C at wb97xd/6-311+g(d,p), linear self.assertTrue(is_linear(xyz1)) self.assertTrue(is_linear(xyz6)) self.assertTrue(is_linear(xyz7)) self.assertTrue(is_linear(xyz9)) self.assertTrue(is_linear(xyz11)) self.assertTrue(is_linear(xyz12)) self.assertFalse(is_linear(xyz2)) self.assertFalse(is_linear(xyz3)) self.assertFalse(is_linear(xyz4)) self.assertFalse(is_linear(xyz5)) self.assertFalse(is_linear(xyz8)) self.assertFalse(is_linear(xyz10))
def parse(self): """ Parse QChem output file and crate the variables the sampling job needed. """ Log = QChemLog(self.input_file) # Load force constant matrix self.hessian = Log.load_force_constant_matrix() # Load cartesian coordinate coordinates, number, mass = Log.load_geometry() # Create conformer class self.conformer, unscaled_frequencies = Log.load_conformer() # Define the sampling protocol if self.protocol is None: self.protocol = 'UMN' # Extract spin miltiplicity and number of optical isomers if self.spin_multiplicity is None: self.spin_multiplicity = self.conformer.spin_multiplicity # Extract net charge from QChem output file if self.charge is None: self.charge = Log.charge # Determine wheteher `UNRESTRICTED` variable should be used or not in QChem calculation self.unrestricted = Log.is_unrestricted() # Log some information related to QM/MM system self.is_QM_MM_INTERFACE = Log.is_QM_MM_INTERFACE() if self.is_QM_MM_INTERFACE: self.QM_ATOMS = Log.get_QM_ATOMS() self.ISOTOPES = Log.get_ISOTOPES() self.nHcap = len(self.ISOTOPES) self.force_field_params = Log.get_force_field_params() self.opt = Log.get_opt() self.fixed_molecule_string = Log.get_fixed_molecule() self.QM_USER_CONNECT = Log.get_QM_USER_CONNECT() self.QM_mass = Log.QM_mass self.QM_coord = Log.QM_coord self.natom = len(self.QM_ATOMS) + len(self.ISOTOPES) self.symbols = Log.QM_atom self.cart_coords = self.QM_coord.reshape(-1, ) self.conformer.coordinates = (self.QM_coord, "angstroms") self.conformer.number = number[:self.natom] self.conformer.mass = (self.QM_mass, "amu") xyz = '' for i in range(len(self.QM_ATOMS)): if self.QM_USER_CONNECT[i].endswith('0 0 0 0'): xyz += '{}\t{}\t\t{}\t\t{}'.format( self.symbols[i], self.cart_coords[3 * i], self.cart_coords[3 * i + 1], self.cart_coords[3 * i + 2]) if i != self.natom - 1: xyz += '\n' self.xyz = xyz if self.ncpus is None: raise InputError('Lack of defining the number of cpu used.') self.zpe = Log.load_zero_point_energy() if self.addcart is None and self.addtr is None and self.add_interfragment_bonds is None: self.addtr = True else: self.nHcap = 0 self.natom = Log.get_number_of_atoms() self.symbols = [symbol_by_number[i] for i in number] self.cart_coords = coordinates.reshape(-1, ) self.conformer.coordinates = (coordinates, "angstroms") self.conformer.number = number self.conformer.mass = (mass, "amu") self.xyz = getXYZ(self.symbols, self.cart_coords) if self.ncpus is None: raise InputError('Lack of defining the number of cpu used.') self.zpe = Log.load_zero_point_energy() # Determine whether or not the species is linear from its 3D coordinates self.linearity = is_linear(self.conformer.coordinates.value) # Determine hindered rotors information if self.protocol == 'UMVT': if self.rotors is not None: logging.info('Find user defined rotors...') self.rotors_dict = self.rotors else: logging.info('Determing the rotors of {}...'.format( self.label)) self.rotors_dict = self.get_rotors_dict() if self.rotors_dict == {}: logging.info( 'No internal rotations are found for {label}'.format( label=self.label)) else: logging.info('==========================================') logging.info(' Hindered Rotors ') logging.info('==========================================') logging.info(prettify(str(self.rotors_dict))) logging.info('-----------------------------------------\n') self.n_rotors = len(self.rotors_dict) elif self.protocol == 'UMN': self.rotors_dict = [] self.n_rotors = 0 else: raise InputError( 'The protocol of {protocol} is invalid. Please use UMVT or UMN.' .format(protocol=self.protocol)) # Determine whether this system is QM/MM system if self.is_QM_MM_INTERFACE: self.nmode = 3 * len(Log.get_QM_ATOMS()) - (1 if self.is_ts else 0) self.n_vib = 3 * len(Log.get_QM_ATOMS()) - self.n_rotors - ( 1 if self.is_ts else 0) else: self.nmode = 3 * self.natom - (5 if self.linearity else 6) - (1 if self.is_ts else 0) self.n_vib = 3 * self.natom - ( 5 if self.linearity else 6) - self.n_rotors - (1 if self.is_ts else 0) # Create RedundantCoords object if self.is_ts and self.addcart is None and self.addtr is None and self.add_interfragment_bonds is None: self.addcart = True self.internal = get_RedundantCoords( self.label, self.symbols, self.cart_coords / BOHR2ANG, self.bond_factor, nHcap=self.nHcap, add_hrdrogen_bonds=False, addcart=self.addcart, addtr=self.addtr, add_interfragment_bonds=self.add_interfragment_bonds) # Create RedundantCoords object for torsional mode if self.protocol == 'UMVT': self.torsion_internal = get_RedundantCoords( self.label, self.symbols, self.cart_coords / BOHR2ANG, self.bond_factor, self.rotors_dict, self.nHcap, add_hrdrogen_bonds=False, addcart=False, addtr=False, add_interfragment_bonds=True) # Extract imaginary frequency from transition state if self.is_ts: self.imaginary_frequency = Log.load_negative_frequency() # Determine max_loop if self.nnl is not None: self.max_nloop = int(1 / self.step_size_factor) * self.nnl else: self.max_nloop = 200 # The basis used in freq job, which will be used in OptVib job self.freq_basis, self.freq_gen_basis = Log.get_basis()
def diagonalize_projected_hessian(conformer, hessian, linear, n_vib, rotors=[], get_projected_out_freqs=False, get_mass_weighted_hessian=False, get_weighted_vectors=False, label=None): """ For a given `conformer` with associated force constant matrix `hessian`, lists of rotor information `rotors`, `pivots`, and `top1`, and the linearity of the molecule `linear`, project out the nonvibrational modes from the force constant matrix and use this to determine the vibrational frequencies. The list of vibrational frequencies is returned in cm^-1. The list of directional vectors, in cartesian coordinates, is returned. Refer to Gaussian whitepaper (http://gaussian.com/vib/) for procedure to calculate harmonic oscillator vibrational frequencies using the force constant matrix. """ n_rotors = 0 for rotor in rotors: if len(rotor) == 8: n_rotors += 2 elif len(rotor) == 5 and isinstance(rotor[1][0], list): n_rotors += len(rotor[1]) else: n_rotors += 1 mass = conformer.mass.value_si coordinates = conformer.coordinates.value if linear is None: linear = is_linear(coordinates) if linear: logging.info('Determined species {0} to be linear.'.format(label)) n_atoms = len(conformer.mass.value) # Put origin in center of mass xm = 0.0 ym = 0.0 zm = 0.0 totmass = 0.0 for i in range(n_atoms): xm += mass[i] * coordinates[i, 0] ym += mass[i] * coordinates[i, 1] zm += mass[i] * coordinates[i, 2] totmass += mass[i] xm /= totmass ym /= totmass zm /= totmass for i in range(n_atoms): coordinates[i, 0] -= xm coordinates[i, 1] -= ym coordinates[i, 2] -= zm # Make vector with the root of the mass in amu for each atom amass = np.sqrt(mass / constants.amu) # Rotation matrix inertia = conformer.get_moment_of_inertia_tensor() inertia_xyz = np.linalg.eigh(inertia)[1] external = 6 if linear: external = 5 d = np.zeros((n_atoms * 3, external), np.float64) # Transform the coordinates to the principal axes p = np.dot(coordinates, inertia_xyz) for i in range(n_atoms): # Projection vectors for translation d[3 * i + 0, 0] = amass[i] d[3 * i + 1, 1] = amass[i] d[3 * i + 2, 2] = amass[i] # Construction of the projection vectors for external rotation for i in range(n_atoms): d[3 * i, 3] = (p[i, 1] * inertia_xyz[0, 2] - p[i, 2] * inertia_xyz[0, 1]) * amass[i] d[3 * i + 1, 3] = (p[i, 1] * inertia_xyz[1, 2] - p[i, 2] * inertia_xyz[1, 1]) * amass[i] d[3 * i + 2, 3] = (p[i, 1] * inertia_xyz[2, 2] - p[i, 2] * inertia_xyz[2, 1]) * amass[i] d[3 * i, 4] = (p[i, 2] * inertia_xyz[0, 0] - p[i, 0] * inertia_xyz[0, 2]) * amass[i] d[3 * i + 1, 4] = (p[i, 2] * inertia_xyz[1, 0] - p[i, 0] * inertia_xyz[1, 2]) * amass[i] d[3 * i + 2, 4] = (p[i, 2] * inertia_xyz[2, 0] - p[i, 0] * inertia_xyz[2, 2]) * amass[i] if not linear: d[3 * i, 5] = (p[i, 0] * inertia_xyz[0, 1] - p[i, 1] * inertia_xyz[0, 0]) * amass[i] d[3 * i + 1, 5] = (p[i, 0] * inertia_xyz[1, 1] - p[i, 1] * inertia_xyz[1, 0]) * amass[i] d[3 * i + 2, 5] = (p[i, 0] * inertia_xyz[2, 1] - p[i, 1] * inertia_xyz[2, 0]) * amass[i] # Make sure projection matrix is orthonormal inertia = np.identity(n_atoms * 3, np.float64) p = np.zeros((n_atoms * 3, 3 * n_atoms + external), np.float64) p[:, 0:external] = d[:, 0:external] p[:, external:external + 3 * n_atoms] = inertia[:, 0:3 * n_atoms] for i in range(3 * n_atoms + external): norm = 0.0 for j in range(3 * n_atoms): norm += p[j, i] * p[j, i] for j in range(3 * n_atoms): if norm > 1E-15: p[j, i] /= np.sqrt(norm) else: p[j, i] = 0.0 for j in range(i + 1, 3 * n_atoms + external): proj = 0.0 for k in range(3 * n_atoms): proj += p[k, i] * p[k, j] for k in range(3 * n_atoms): p[k, j] -= proj * p[k, i] # Order p, there will be vectors that are 0.0 i = 0 while i < 3 * n_atoms: norm = 0.0 for j in range(3 * n_atoms): norm += p[j, i] * p[j, i] if norm < 0.5: p[:, i:3 * n_atoms + external - 1] = p[:, i + 1:3 * n_atoms + external] else: i += 1 # T is the transformation vector from cartesian to internal coordinates T = np.zeros((n_atoms * 3, 3 * n_atoms - external), np.float64) T[:, 0:3 * n_atoms - external] = p[:, external:3 * n_atoms] # Generate mass-weighted force constant matrix # This converts the axes to mass-weighted Cartesian axes # Units of Fm are J/m^2*kg = 1/s^2 weighted_hessian = hessian.copy() for i in range(n_atoms): for j in range(n_atoms): for u in range(3): for v in range(3): weighted_hessian[3 * i + u, 3 * j + v] /= math.sqrt(mass[i] * mass[j]) hessian_int = np.dot(T.T, np.dot(weighted_hessian, T)) # Get eigenvalues of internal force constant matrix, V = 3N-6 * 3N-6 eig, v = np.linalg.eigh(hessian_int) # logging.debug('Frequencies from internal Hessian') # for i in range(3 * n_atoms - external): # with np.warnings.catch_warnings(): # np.warnings.filterwarnings('ignore', r'invalid value encountered in sqrt') # logging.debug(np.sqrt(eig[i]) / (2 * math.pi * constants.c * 100)) # Now we can start thinking about projecting out the internal rotations d_int = np.zeros((3 * n_atoms, n_rotors), np.float64) counter = 0 for i, rotor in enumerate(rotors): if len(rotor) == 5 and isinstance(rotor[1][0], list): scan_dir, pivots_list, tops, sigmas, semiclassical = rotor elif len(rotor) == 5: scanLog, pivots, top, symmetry, fit = rotor pivots_list = [pivots] tops = [top] elif len(rotor) == 2: pivots, top = rotor pivots_list = [pivots] tops = [top] elif len(rotor) == 3: pivots, top, symmetry = rotor pivots_list = [pivots] tops = [top] elif len(rotor) == 8: scan_dir, pivots1, top1, symmetry1, pivots2, top2, symmetry2, symmetry = rotor pivots_list = [pivots1, pivots2] tops = [top1, top2] else: raise ValueError("{} not a proper rotor format".format(rotor)) for k in range(len(tops)): top = tops[k] pivots = pivots_list[k] # Determine pivot atom if pivots[0] in top: pivot1 = pivots[0] pivot2 = pivots[1] elif pivots[1] in top: pivot1 = pivots[1] pivot2 = pivots[0] else: raise ValueError( 'Could not determine pivot atom for rotor {}.'.format( label)) # Projection vectors for internal rotation e12 = coordinates[pivot1 - 1, :] - coordinates[pivot2 - 1, :] for j in range(n_atoms): atom = j + 1 if atom in top: e31 = coordinates[atom - 1, :] - coordinates[pivot1 - 1, :] d_int[3 * (atom - 1):3 * (atom - 1) + 3, counter] = np.cross(e31, e12) * amass[atom - 1] else: e31 = coordinates[atom - 1, :] - coordinates[pivot2 - 1, :] d_int[3 * (atom - 1):3 * (atom - 1) + 3, counter] = np.cross(e31, -e12) * amass[atom - 1] counter += 1 # Normal modes in mass weighted cartesian coordinates vmw = np.dot(T, v) eigm = np.zeros((3 * n_atoms - external, 3 * n_atoms - external), np.float64) for i in range(3 * n_atoms - external): eigm[i, i] = eig[i] fm = np.dot(vmw, np.dot(eigm, vmw.T)) # Internal rotations are not normal modes => project them on the normal modes and orthogonalize # d_int_proj = (3N-6) x (3N) x (3N) x (Nrotors) d_int_proj = np.dot(vmw.T, d_int) # Reconstruct d_int for i in range(n_rotors): for j in range(3 * n_atoms): d_int[j, i] = 0 for k in range(3 * n_atoms - external): d_int[j, i] += d_int_proj[k, i] * vmw[j, k] # Ortho normalize for i in range(n_rotors): norm = 0.0 for j in range(3 * n_atoms): norm += d_int[j, i] * d_int[j, i] for j in range(3 * n_atoms): d_int[j, i] /= np.sqrt(norm) for j in range(i + 1, n_rotors): proj = 0.0 for k in range(3 * n_atoms): proj += d_int[k, i] * d_int[k, j] for k in range(3 * n_atoms): d_int[k, j] -= proj * d_int[k, i] # Calculate the frequencies corresponding to the internal rotors int_proj = np.dot(fm, d_int) kmus = np.array( [np.linalg.norm(int_proj[:, i]) for i in range(int_proj.shape[1])]) int_rotor_freqs = np.sqrt(kmus) / (2.0 * math.pi * constants.c * 100.0) if get_projected_out_freqs: return int_rotor_freqs # Do the projection d_int_proj = np.dot(vmw.T, d_int) proj = np.dot(d_int, d_int.T) inertia = np.identity(n_atoms * 3, np.float64) proj = inertia - proj fm = np.dot(proj, np.dot(fm, proj)) # For linear molecule # TODO It seems that the above code cannot handle linear molecules properly if linear: mass_3N_array = np.array([i for i in mass for j in range(3)]) mass_mat = np.diag(mass_3N_array) inv_sq_mass_mat = np.linalg.inv(mass_mat**0.5) fm = inv_sq_mass_mat.dot(hessian.dot(inv_sq_mass_mat)) if get_mass_weighted_hessian: return fm # Get eigenvalues of mass-weighted force constant matrix eig, v = np.linalg.eigh(fm) eig.sort() if get_weighted_vectors: return v.T[-n_vib:] # Convert eigenvalues to vibrational frequenciㄋes in cm^-1 # Only keep the modes that don't correspond to translation, rotation, or internal rotation # logging.debug('Frequencies from projected Hessian') # for i in range(3 * n_atoms): # with np.warnings.catch_warnings(): # np.warnings.filterwarnings('ignore', r'invalid value encountered in sqrt') # logging.debug(np.sqrt(eig[i]) / (2 * math.pi * constants.c * 100)) # Convert eigenvalues to vibrational frequencies in cm^-1 vib_freq = np.sqrt(eig[-n_vib:]) / (2 * np.pi * constants.c * 100) # Transforme directional vectors of normal modes from mass-weighted coordinates into cartesian coordinates mass_3N_array = np.array([i for i in mass for j in range(3)]) mass_mat = np.diag(mass_3N_array) inv_sq_mass_mat = np.linalg.inv(mass_mat**0.5) unweighted_v = np.matmul(inv_sq_mass_mat, v).T[-n_vib:] return vib_freq, unweighted_v