Exemple #1
0
 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
Exemple #2
0
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
Exemple #3
0
 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
Exemple #4
0
    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))
Exemple #5
0
    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()
Exemple #6
0
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