def setUp(self): """ A function run before each unit test in this class. """ self.ethylene = Conformer( E0 = (0.0,"kJ/mol"), modes = [ IdealGasTranslation(mass=(28.03,"amu")), NonlinearRotor(inertia=([3.41526,16.6498,20.065],"amu*angstrom^2"), symmetry=4), HarmonicOscillator(frequencies=([828.397,970.652,977.223,1052.93,1233.55,1367.56,1465.09,1672.25,3098.46,3111.7,3165.79,3193.54],"cm^-1")), ], spinMultiplicity = 1, opticalIsomers = 1, ) self.oxygen = Conformer( E0 = (0.0,"kJ/mol"), modes = [ IdealGasTranslation(mass=(31.99,"amu")), LinearRotor(inertia=(11.6056,"amu*angstrom^2"), symmetry=2), HarmonicOscillator(frequencies=([1621.54],"cm^-1")), ], spinMultiplicity = 3, opticalIsomers = 1, ) # The following data is for ethane at the CBS-QB3 level self.coordinates = numpy.array([ [ 0.0000, 0.0000, 0.0000], [ -0.0000, -0.0000, 1.0936], [ 1.0430, -0.0000, -0.3288], [ -0.4484, 0.9417, -0.3288], [ -0.7609, -1.2051, -0.5580], [ -0.7609, -1.2051, -1.6516], [ -0.3125, -2.1468, -0.2292], [ -1.8039, -1.2051, -0.2293], ]) self.number = numpy.array([6, 1, 1, 1, 6, 1, 1, 1]) self.mass = numpy.array([12, 1.007825, 1.007825, 1.007825, 12, 1.007825, 1.007825, 1.007825]) self.E0 = -93.5097 self.conformer = Conformer( E0 = (self.E0,"kJ/mol"), modes = [ IdealGasTranslation(mass=(30.0469,"amu")), NonlinearRotor(inertia=([6.27071,25.3832,25.3833],"amu*angstrom^2"), symmetry=6), HarmonicOscillator(frequencies=([818.917,819.479,987.099,1206.76,1207.05,1396,1411.35,1489.73,1489.95,1492.49,1492.66,2995.36,2996.06,3040.77,3041,3065.86,3066.02],"cm^-1")), HinderedRotor(inertia=(1.56768,"amu*angstrom^2"), symmetry=3, barrier=(2.69401,"kcal/mol"), quantum=False, semiclassical=False), ], spinMultiplicity = 1, opticalIsomers = 1, coordinates = (self.coordinates,"angstrom"), number = self.number, mass = (self.mass,"amu"), )
def setUp(self): """ A method that is run before each unit test in this class. """ self.species = Species( index=1, label='C2H4', thermo=ThermoData( Tdata=([300.0, 400.0, 500.0, 600.0, 800.0, 1000.0, 1500.0], 'K'), Cpdata=([3.0, 4.0, 5.0, 6.0, 8.0, 10.0, 15.0], 'cal/(mol*K)'), H298=(-20.0, 'kcal/mol'), S298=(50.0, 'cal/(mol*K)'), Tmin=(300.0, 'K'), Tmax=(2000.0, 'K'), ), conformer=Conformer( E0=(0.0, 'kJ/mol'), modes=[ IdealGasTranslation(mass=(28.03, 'amu')), NonlinearRotor( inertia=([5.6952e-47, 2.7758e-46, 3.3454e-46], 'kg*m^2'), symmetry=1), HarmonicOscillator(frequencies=([ 834.50, 973.31, 975.37, 1067.1, 1238.5, 1379.5, 1472.3, 1691.3, 3121.6, 3136.7, 3192.5, 3221.0 ], 'cm^-1')), ], spinMultiplicity=1, opticalIsomers=1, ), molecule=[Molecule().fromSMILES('C=C')], transportData=TransportData(sigma=(1, 'angstrom'), epsilon=(100, 'K')), molecularWeight=(28.03, 'amu'), reactive=True, )
class TestConformer(unittest.TestCase): """ Contains unit tests of the :class:`Conformer` class. """ def setUp(self): """ A function run before each unit test in this class. """ self.ethylene = Conformer( E0=(0.0, "kJ/mol"), modes=[ IdealGasTranslation(mass=(28.03, "amu")), NonlinearRotor(inertia=([3.41526, 16.6498, 20.065], "amu*angstrom^2"), symmetry=4), HarmonicOscillator(frequencies=([ 828.397, 970.652, 977.223, 1052.93, 1233.55, 1367.56, 1465.09, 1672.25, 3098.46, 3111.7, 3165.79, 3193.54 ], "cm^-1")), ], spin_multiplicity=1, optical_isomers=1, ) self.oxygen = Conformer( E0=(0.0, "kJ/mol"), modes=[ IdealGasTranslation(mass=(31.99, "amu")), LinearRotor(inertia=(11.6056, "amu*angstrom^2"), symmetry=2), HarmonicOscillator(frequencies=([1621.54], "cm^-1")), ], spin_multiplicity=3, optical_isomers=1, ) # The following data is for ethane at the CBS-QB3 level self.coordinates = np.array([ [0.0000, 0.0000, 0.0000], [-0.0000, -0.0000, 1.0936], [1.0430, -0.0000, -0.3288], [-0.4484, 0.9417, -0.3288], [-0.7609, -1.2051, -0.5580], [-0.7609, -1.2051, -1.6516], [-0.3125, -2.1468, -0.2292], [-1.8039, -1.2051, -0.2293], ]) self.number = np.array([6, 1, 1, 1, 6, 1, 1, 1]) self.mass = np.array([ 12, 1.007825, 1.007825, 1.007825, 12, 1.007825, 1.007825, 1.007825 ]) self.E0 = -93.5097 self.conformer = Conformer( E0=(self.E0, "kJ/mol"), modes=[ IdealGasTranslation(mass=(30.0469, "amu")), NonlinearRotor(inertia=([6.27071, 25.3832, 25.3833], "amu*angstrom^2"), symmetry=6), HarmonicOscillator(frequencies=([ 818.917, 819.479, 987.099, 1206.76, 1207.05, 1396, 1411.35, 1489.73, 1489.95, 1492.49, 1492.66, 2995.36, 2996.06, 3040.77, 3041, 3065.86, 3066.02 ], "cm^-1")), HinderedRotor(inertia=(1.56768, "amu*angstrom^2"), symmetry=3, barrier=(2.69401, "kcal/mol"), quantum=False, semiclassical=False), ], spin_multiplicity=1, optical_isomers=1, coordinates=(self.coordinates, "angstrom"), number=self.number, mass=(self.mass, "amu"), ) def test_get_partition_function_ethylene(self): """ Test the StatMech.get_partition_function() method for ethylene. """ t_list = np.array([300, 500, 1000, 1500, 2000]) q_exp_list = np.array( [4.05311e+09, 4.19728e+10, 2.82309e+12, 7.51135e+13, 1.16538e+15]) for temperature, q_exp in zip(t_list, q_exp_list): q_act = self.ethylene.get_partition_function(temperature) self.assertAlmostEqual(q_exp, q_act, delta=1e-4 * q_exp) def test_get_heat_capacity_ethylene(self): """ Test the StatMech.get_heat_capacity() method for ethylene. """ t_list = np.array([300, 500, 1000, 1500, 2000]) cv_exp_list = np.array([5.11186, 7.40447, 11.1659, 13.1221, 14.1617 ]) * constants.R for temperature, cv_exp in zip(t_list, cv_exp_list): cv_act = self.ethylene.get_heat_capacity(temperature) self.assertAlmostEqual(cv_exp, cv_act, 3) def test_get_enthalpy_ethylene(self): """ Test the StatMech.get_enthalpy() method for ethylene. """ t_list = np.array([300, 500, 1000, 1500, 2000]) h_exp_list = np.array([4.23129, 5.04826, 7.27337, 8.93167, 10.1223 ]) * constants.R * t_list for temperature, h_exp in zip(t_list, h_exp_list): h_act = self.ethylene.get_enthalpy(temperature) self.assertAlmostEqual(h_exp, h_act, delta=1e-4 * h_exp) def test_get_entropy_ethylene(self): """ Test the StatMech.get_entropy() method for ethylene. """ t_list = np.array([300, 500, 1000, 1500, 2000]) s_exp_list = np.array([26.3540, 29.5085, 35.9422, 40.8817, 44.8142 ]) * constants.R for temperature, s_exp in zip(t_list, s_exp_list): s_act = self.ethylene.get_entropy(temperature) self.assertAlmostEqual(s_exp, s_act, 3) def test_get_sum_of_states_ethylene(self): """ Test the StatMech.get_sum_of_states() method for ethylene. """ e_list = np.arange(0, 5000 * 11.96, 2 * 11.96) sum_states = self.ethylene.get_sum_of_states(e_list) dens_states = self.ethylene.get_density_of_states(e_list) for n in range(10, len(e_list)): self.assertTrue( 0.8 < np.sum(dens_states[0:n + 1]) / sum_states[n] < 1.25, '{0} != {1}'.format(np.sum(dens_states[0:n + 1]), sum_states[n])) def test_get_density_of_states_ethylene(self): """ Test the StatMech.get_density_of_states() method for ethylene. """ e_list = np.arange(0, 5000 * 11.96, 2 * 11.96) dens_states = self.ethylene.get_density_of_states(e_list) temperature = 100 q_act = np.sum(dens_states * np.exp(-e_list / constants.R / temperature)) q_exp = self.ethylene.get_partition_function(temperature) self.assertAlmostEqual(q_exp, q_act, delta=1e-1 * q_exp) def test_get_partition_function_oxygen(self): """ Test the StatMech.get_partition_function() method for oxygen. """ t_list = np.array([300, 500, 1000, 1500, 2000]) q_exp_list = np.array( [1.55584e+09, 9.38339e+09, 1.16459e+11, 5.51016e+11, 1.72794e+12]) for temperature, q_exp in zip(t_list, q_exp_list): q_act = self.oxygen.get_partition_function(temperature) self.assertAlmostEqual(q_exp, q_act, delta=1e-4 * q_exp) def test_get_heat_capacity_oxygen(self): """ Test the StatMech.get_heat_capacity() method for oxygen. """ t_list = np.array([300, 500, 1000, 1500, 2000]) cv_exp_list = np.array([3.52538, 3.70877, 4.14751, 4.32063, 4.39392 ]) * constants.R for temperature, Cv_exp in zip(t_list, cv_exp_list): cv_act = self.oxygen.get_heat_capacity(temperature) self.assertAlmostEqual(Cv_exp, cv_act, 3) def test_get_enthalpy_oxygen(self): """ Test the StatMech.get_enthalpy() method for oxygen. """ t_list = np.array([300, 500, 1000, 1500, 2000]) h_exp_list = np.array([3.50326, 3.54432, 3.75062, 3.91623, 4.02765 ]) * constants.R * t_list for temperature, h_exp in zip(t_list, h_exp_list): h_act = self.oxygen.get_enthalpy(temperature) self.assertAlmostEqual(h_exp, h_act, delta=1e-4 * h_exp) def test_get_entropy_oxygen(self): """ Test the StatMech.get_entropy() method for oxygen. """ t_list = np.array([300, 500, 1000, 1500, 2000]) s_exp_list = np.array([24.6685, 26.5065, 29.2314, 30.9513, 32.2056 ]) * constants.R for temperature, s_exp in zip(t_list, s_exp_list): s_act = self.oxygen.get_entropy(temperature) self.assertAlmostEqual(s_exp, s_act, 3) def test_get_sum_of_states_oxygen(self): """ Test the StatMech.get_sum_of_states() method for oxygen. """ e_list = np.arange(0, 5000 * 11.96, 2 * 11.96) sum_states = self.oxygen.get_sum_of_states(e_list) dens_states = self.oxygen.get_density_of_states(e_list) for n in range(10, len(e_list)): self.assertTrue( 0.8 < np.sum(dens_states[0:n + 1]) / sum_states[n] < 1.25, '{0} != {1}'.format(np.sum(dens_states[0:n + 1]), sum_states[n])) def test_get_density_of_states_oxygen(self): """ Test the StatMech.get_density_of_states() method for oxygen. """ e_list = np.arange(0, 5000 * 11.96, 2 * 11.96) dens_states = self.oxygen.get_density_of_states(e_list) temperature = 100 q_act = np.sum(dens_states * np.exp(-e_list / constants.R / temperature)) q_exp = self.oxygen.get_partition_function(temperature) self.assertAlmostEqual(q_exp, q_act, delta=1e-1 * q_exp) def test_get_total_mass(self): """ Test the Conformer.get_total_mass() method. """ self.assertAlmostEqual( self.conformer.get_total_mass() * constants.Na * 1000., np.sum(self.mass), 6) def test_get_center_of_mass(self): """ Test the Conformer.get_center_of_mass() method. """ cm = self.conformer.get_center_of_mass() self.assertAlmostEqual(cm[0] * 1e10, -0.38045, 4) self.assertAlmostEqual(cm[1] * 1e10, -0.60255, 4) self.assertAlmostEqual(cm[2] * 1e10, -0.27900, 4) def test_get_moment_of_inertia_tensor(self): """ Test the Conformer.get_moment_of_inertia_tensor() method. """ inertia = self.conformer.get_moment_of_inertia_tensor() self.assertAlmostEqual(inertia[0, 0] * constants.Na * 1e23, 20.65968, 4) self.assertAlmostEqual(inertia[0, 1] * constants.Na * 1e23, -7.48115, 4) self.assertAlmostEqual(inertia[0, 2] * constants.Na * 1e23, -3.46416, 4) self.assertAlmostEqual(inertia[1, 0] * constants.Na * 1e23, -7.48115, 4) self.assertAlmostEqual(inertia[1, 1] * constants.Na * 1e23, 13.53472, 4) self.assertAlmostEqual(inertia[1, 2] * constants.Na * 1e23, -5.48630, 4) self.assertAlmostEqual(inertia[2, 0] * constants.Na * 1e23, -3.46416, 4) self.assertAlmostEqual(inertia[2, 1] * constants.Na * 1e23, -5.48630, 4) self.assertAlmostEqual(inertia[2, 2] * constants.Na * 1e23, 22.84296, 4) def test_get_principal_moments_of_inertia(self): """ Test the Conformer.get_principal_moments_of_inertia() method. """ inertia, axes = self.conformer.get_principal_moments_of_inertia() self.assertAlmostEqual(inertia[0] * constants.Na * 1e23, 6.27074, 4) self.assertAlmostEqual(inertia[1] * constants.Na * 1e23, 25.38321, 3) self.assertAlmostEqual(inertia[2] * constants.Na * 1e23, 25.38341, 3) # For some reason the axes seem to jump around (positioning and signs change) # but the absolute values should be the same as we expect expected = sorted([ 0.497140, 0.610114, 0.616938, 0.787360, 0.018454, 0.616218, 0.364578, 0.792099, 0.489554 ]) result = sorted(abs(axes).flat) for i, j in zip(expected, result): self.assertAlmostEqual(i, j, 4) return def test_get_internal_reduced_moment_of_inertia(self): """ Test the Conformer.get_internal_reduced_moment_of_inertia() method. """ inertia = self.conformer.get_internal_reduced_moment_of_inertia( pivots=[1, 5], top1=[1, 2, 3, 4]) self.assertAlmostEqual(inertia * constants.Na * 1e23, 1.56768, 4) def test_get_number_degrees_of_freedom(self): """ Test the Conformer.get_number_degrees_of_freedom() method. """ # this is for ethane: number_degrees_of_freedom = self.conformer.get_number_degrees_of_freedom( ) self.assertEqual(number_degrees_of_freedom, 24) # this is for ethylene: # It doesn't check against 3 * n_atoms, because n_atoms is not declared. number_degrees_of_freedom = self.ethylene.get_number_degrees_of_freedom( ) self.assertEqual(number_degrees_of_freedom, 18) # this is for CO # It doesn't check against 3 * n_atoms, because n_atoms is not declared. number_degrees_of_freedom = self.oxygen.get_number_degrees_of_freedom() self.assertEqual(number_degrees_of_freedom, 6)
def process_thermo_data(spc, thermo0, thermo_class=NASA, solvent_name=''): """ Converts via Wilhoit into required `thermo_class` and sets `E0`. Resulting thermo is returned. """ thermo = None # Always convert to Wilhoit so we can compute E0 if isinstance(thermo0, Wilhoit): wilhoit = thermo0 elif isinstance(thermo0, ThermoData): wilhoit = thermo0.to_wilhoit(B=1000.) else: wilhoit = thermo0.to_wilhoit() # Add on solvation correction solvation_database = get_db('solvation') if not solvent_name or solvation_database is None: logging.debug( 'Solvent database or solvent_name not found. Solvent effect was not utilized' ) solvent_data = None else: solvent_data = solvation_database.get_solvent_data(solvent_name) if solvent_data and not "Liquid thermo library" in thermo0.comment: solvation_database = get_db('solvation') solute_data = solvation_database.get_solute_data(spc) solvation_correction = solvation_database.get_solvation_correction( solute_data, solvent_data) # correction is added to the entropy and enthalpy wilhoit.S0.value_si = (wilhoit.S0.value_si + solvation_correction.entropy) wilhoit.H0.value_si = (wilhoit.H0.value_si + solvation_correction.enthalpy) # Compute E0 by extrapolation to 0 K if spc.conformer is None: spc.conformer = Conformer() spc.conformer.E0 = wilhoit.E0 # Convert to desired thermo class if thermo_class is Wilhoit: thermo = wilhoit elif thermo_class is NASA: if solvent_data: # If liquid phase simulation keep the nasa polynomial if it comes from a liquid phase thermoLibrary. # Otherwise convert wilhoit to NASA if "Liquid thermo library" in thermo0.comment and isinstance( thermo0, NASA): thermo = thermo0 if thermo.E0 is None: thermo.E0 = wilhoit.E0 else: thermo = wilhoit.to_nasa(Tmin=100.0, Tmax=5000.0, Tint=1000.0) else: # gas phase with species matching thermo library keep the NASA from library or convert if group additivity if "Thermo library" in thermo0.comment and isinstance( thermo0, NASA): thermo = thermo0 if thermo.E0 is None: thermo.E0 = wilhoit.E0 else: thermo = wilhoit.to_nasa(Tmin=100.0, Tmax=5000.0, Tint=1000.0) else: raise Exception( 'thermo_class neither NASA nor Wilhoit. Cannot process thermo data.' ) return thermo
def load_conformer(self, symmetry=None, spin_multiplicity=0, optical_isomers=None, label=''): """ Load the molecular degree of freedom data from a log file created as the result of a MolPro "Freq" quantum chemistry calculation with the thermo printed. """ modes = [] unscaled_frequencies = [] e0 = 0.0 if optical_isomers is None or symmetry is None: _optical_isomers, _symmetry, _ = self.get_symmetry_properties() if optical_isomers is None: optical_isomers = _optical_isomers if symmetry is None: symmetry = _symmetry with open(self.path, 'r') as f: line = f.readline() while line != '': # Read the spin multiplicity if not explicitly given if spin_multiplicity == 0 and 'spin' in line: splits = line.replace('=', ' ').replace(',', ' ').split(' ') for i, s in enumerate(splits): if 'spin' in s: spin_multiplicity = int(splits[i + 1]) + 1 logging.debug( 'Conformer {0} is assigned a spin multiplicity of {1}' .format(label, spin_multiplicity)) break if spin_multiplicity == 0 and 'SPIN SYMMETRY' in line: spin_symmetry = line.split()[-1] if spin_symmetry == 'Singlet': spin_multiplicity = 1 elif spin_symmetry == 'Doublet': spin_multiplicity = 2 elif spin_symmetry == 'Triplet': spin_multiplicity = 3 elif spin_symmetry == 'Quartet': spin_multiplicity = 4 elif spin_symmetry == 'Quintet': spin_multiplicity = 5 elif spin_symmetry == 'Sextet': spin_multiplicity = 6 if spin_multiplicity: logging.debug( 'Conformer {0} is assigned a spin multiplicity of {1}' .format(label, spin_multiplicity)) break # The data we want is in the Thermochemistry section of the output if 'THERMODYNAMICAL' in line: modes = [] line = f.readline() while line != '': # This marks the end of the thermochemistry section if '*************************************************' in line: break # Read molecular mass for external translational modes elif 'Molecular Mass:' in line: mass = float(line.split()[2]) translation = IdealGasTranslation(mass=(mass, "amu")) modes.append(translation) # Read moments of inertia for external rotational modes elif 'Rotational Constants' in line and line.split( )[-1] == '[GHz]': inertia = [float(d) for d in line.split()[-4:-1]] for i in range(3): inertia[i] = constants.h / (8 * constants.pi * constants.pi * inertia[i] * 1e9) \ * constants.Na * 1e23 rotation = NonlinearRotor( inertia=(inertia, "amu*angstrom^2"), symmetry=symmetry) modes.append(rotation) elif 'Rotational Constant' in line and line.split( )[3] == '[GHz]': inertia = float(line.split()[2]) inertia = constants.h / (8 * constants.pi * constants.pi * inertia * 1e9) \ * constants.Na * 1e23 rotation = LinearRotor(inertia=(inertia, "amu*angstrom^2"), symmetry=symmetry) modes.append(rotation) # Read vibrational modes elif 'Vibrational Temperatures' in line: frequencies = [] frequencies.extend( [float(d) for d in line.split()[3:]]) line = f.readline() while line.strip() != '': frequencies.extend( [float(d) for d in line.split()]) line = f.readline() # Convert from K to cm^-1 if len(frequencies) > 0: frequencies = [ freq * 0.695039 for freq in frequencies ] # kB = 0.695039 cm^-1/K unscaled_frequencies = frequencies vibration = HarmonicOscillator( frequencies=(frequencies, "cm^-1")) modes.append(vibration) # Read the next line in the file line = f.readline() # Read the next line in the file line = f.readline() return Conformer(E0=(e0 * 0.001, "kJ/mol"), modes=modes, spin_multiplicity=spin_multiplicity, optical_isomers=optical_isomers), unscaled_frequencies
def process_thermo_data(spc, thermo0, thermo_class=NASA, solvent_name=''): """ Converts via Wilhoit into required `thermo_class` and sets `E0`. Resulting thermo is returned. """ thermo = None # Always convert to Wilhoit so we can compute E0 if isinstance(thermo0, Wilhoit): wilhoit = thermo0 elif isinstance(thermo0, ThermoData): wilhoit = thermo0.to_wilhoit(B=1000.) else: wilhoit = thermo0.to_wilhoit() # Add on solvation correction solvation_database = get_db('solvation') if not solvent_name or solvation_database is None: logging.debug('Solvent database or solvent_name not found. Solvent effect was not utilized') solvent_data = None else: solvent_data = solvation_database.get_solvent_data(solvent_name) if solvent_data and not "Liquid thermo library" in thermo0.comment: solvation_database = get_db('solvation') solute_data = solvation_database.get_solute_data(spc) solvation_correction = solvation_database.get_solvation_correction(solute_data, solvent_data) # correction is added to the entropy and enthalpy wilhoit.S0.value_si = (wilhoit.S0.value_si + solvation_correction.entropy) wilhoit.H0.value_si = (wilhoit.H0.value_si + solvation_correction.enthalpy) # Compute E0 by extrapolation to 0 K if spc.conformer is None: spc.conformer = Conformer() spc.conformer.E0 = wilhoit.E0 # Convert to desired thermo class if thermo_class is Wilhoit: thermo = wilhoit elif thermo_class is NASA: if solvent_data: # If liquid phase simulation keep the nasa polynomial if it comes from a liquid phase thermoLibrary. # Otherwise convert wilhoit to NASA if "Liquid thermo library" in thermo0.comment and isinstance(thermo0, NASA): thermo = thermo0 if thermo.E0 is None: thermo.E0 = wilhoit.E0 else: thermo = wilhoit.to_nasa(Tmin=100.0, Tmax=5000.0, Tint=1000.0) else: # gas phase with species matching thermo library keep the NASA from library or convert if group additivity if "Thermo library" in thermo0.comment and isinstance(thermo0, NASA): thermo = thermo0 if thermo.E0 is None: thermo.E0 = wilhoit.E0 else: thermo = wilhoit.to_nasa(Tmin=100.0, Tmax=5000.0, Tint=1000.0) else: raise Exception('thermo_class neither NASA nor Wilhoit. Cannot process thermo data.') if thermo.__class__ != thermo0.__class__: # Compute RMS error of overall transformation Tlist = np.array([300.0, 400.0, 500.0, 600.0, 800.0, 1000.0, 1500.0], np.float64) err = 0.0 for T in Tlist: err += (thermo.get_heat_capacity(T) - thermo0.get_heat_capacity(T)) ** 2 err = math.sqrt(err / len(Tlist)) / constants.R # logging.log(logging.WARNING if err > 0.2 else 0, 'Average RMS error in heat capacity fit to {0} = {1:g}*R'.format(spc, err)) return thermo
def loadConformer(self, symmetry=None, spinMultiplicity=None, opticalIsomers=1): """ Load the molecular degree of freedom data from a log file created as the result of a Qchem "Freq" calculation. As Qchem's guess of the external symmetry number is not always correct, you can use the `symmetry` parameter to substitute your own value; if not provided, the value in the Qchem output file will be adopted. """ modes = [] freq = [] mmass = [] rot = [] E0 = 0.0 f = open(self.path, 'r') line = f.readline() while line != '': # The data we want is in the Thermochemistry section of the output if 'VIBRATIONAL ANALYSIS' in line: modes = [] inPartitionFunctions = False line = f.readline() while line != '': # This marks the end of the thermochemistry section if 'Thank you very much for using Q-Chem.' in line: break # Read vibrational modes elif 'VIBRATIONAL FREQUENCIES (CM**-1)' in line: frequencies = [] while 'STANDARD THERMODYNAMIC QUANTITIES AT' not in line: if ' Frequency:' in line: frequencies.extend( [float(d) for d in line.split()[-3:]]) line = f.readline() line = f.readline() # If there is an imaginary frequency, remove it if frequencies[0] < 0.0: frequencies = frequencies[1:] vibration = HarmonicOscillator( frequencies=(frequencies, "cm^-1")) #modes.append(vibration) freq.append(vibration) # Read molecular mass for external translational modes elif 'Molecular Mass:' in line: mass = float(line.split()[2]) translation = IdealGasTranslation(mass=(mass, "amu")) #modes.append(translation) mmass.append(translation) # Read moments of inertia for external rotational modes, given in atomic units elif 'Eigenvalues --' in line: inertia = [float(d) for d in line.split()[-3:]] # If the first eigenvalue is 0, the rotor is linear if inertia[0] == 0.0: inertia.remove(0.0) for i in range(2): inertia[i] *= (constants.a0 / 1e-10)**2 rotation = LinearRotor( inertia=(inertia, "amu*angstrom^2"), symmetry=symmetry) #modes.append(rotation) rot.append(rotation) else: for i in range(3): inertia[i] *= (constants.a0 / 1e-10)**2 rotation = NonlinearRotor( inertia=(inertia, "amu*angstrom^2"), symmetry=symmetry) #modes.append(rotation) rot.append(rotation) # Read Qchem's estimate of the external rotational symmetry number, which may very well be incorrect elif 'Rotational Symmetry Number is' in line and symmetry is None: symmetry = int(float(line.split()[4])) elif 'Final energy is' in line: E0 = float( line.split()[3]) * constants.E_h * constants.Na print 'energy is' + str(E0) # Read ZPE and add to ground-state energy # NEED TO MULTIPLY ZPE BY scaling factor! elif 'Zero point vibrational energy:' in line: ZPE = float(line.split()[4]) * 4184 E0 = E0 + ZPE # Read spin multiplicity if not explicitly given # elif 'Electronic' in line and inPartitionFunctions and spinMultiplicity is None: # spinMultiplicity = int(float(line.split()[1].replace('D', 'E'))) # elif 'Log10(Q)' in line: # inPartitionFunctions = True # Read the next line in the file line = f.readline() # Read the next line in the file line = f.readline() # Close file when finished f.close() modes = mmass + rot + freq #modes.append(mmass), modes.append(rot), modes.append(freq) return Conformer(E0=(E0 * 0.001, "kJ/mol"), modes=modes, spinMultiplicity=spinMultiplicity, opticalIsomers=opticalIsomers)
def load_conformer(self, symmetry=None, spin_multiplicity=0, optical_isomers=None, label=''): """ Load the molecular degree of freedom data from a log file created as the result of a Gaussian "Freq" quantum chemistry calculation. As Gaussian's guess of the external symmetry number is not always correct, you can use the `symmetry` parameter to substitute your own value; if not provided, the value in the Gaussian log file will be adopted. In a log file with multiple Thermochemistry sections, only the last one will be kept. """ modes = [] unscaled_frequencies = [] e0 = 0.0 if optical_isomers is None or symmetry is None: _optical_isomers, _symmetry, _ = self.get_symmetry_properties() if optical_isomers is None: optical_isomers = _optical_isomers if symmetry is None: symmetry = _symmetry with open(self.path, 'r') as f: line = f.readline() while line != '': # Read the spin multiplicity if not explicitly given if spin_multiplicity == 0 and 'Multiplicity =' in line: spin_multiplicity = int(line.split()[-1]) logging.debug('Conformer {0} is assigned a spin multiplicity of {1}' .format(label, spin_multiplicity)) # The data we want is in the Thermochemistry section of the output if '- Thermochemistry -' in line: modes = [] in_partition_functions = False line = f.readline() while line != '': # This marks the end of the thermochemistry section if '-------------------------------------------------------------------' in line: break # Read molecular mass for external translational modes elif 'Molecular mass:' in line: mass = float(line.split()[2]) translation = IdealGasTranslation(mass=(mass, "amu")) modes.append(translation) # Read moments of inertia for external rotational modes elif 'Rotational constants (GHZ):' in line: inertia = [float(d) for d in line.split()[-3:]] for i in range(3): inertia[i] = constants.h / (8 * constants.pi * constants.pi * inertia[i] * 1e9) \ * constants.Na * 1e23 rotation = NonlinearRotor(inertia=(inertia, "amu*angstrom^2"), symmetry=symmetry) modes.append(rotation) elif 'Rotational constant (GHZ):' in line: inertia = [float(line.split()[3])] inertia[0] = constants.h / (8 * constants.pi * constants.pi * inertia[0] * 1e9) \ * constants.Na * 1e23 rotation = LinearRotor(inertia=(inertia[0], "amu*angstrom^2"), symmetry=symmetry) modes.append(rotation) # Read vibrational modes elif 'Vibrational temperatures:' in line: frequencies = [] frequencies.extend([float(d) for d in line.split()[2:]]) line = f.readline() frequencies.extend([float(d) for d in line.split()[1:]]) line = f.readline() while line.strip() != '': frequencies.extend([float(d) for d in line.split()]) line = f.readline() # Convert from K to cm^-1 if len(frequencies) > 0: frequencies = [freq * 0.695039 for freq in frequencies] # kB = 0.695039 cm^-1/K unscaled_frequencies = frequencies vibration = HarmonicOscillator(frequencies=(frequencies, "cm^-1")) modes.append(vibration) # Read ground-state energy elif 'Sum of electronic and zero-point Energies=' in line: e0 = float(line.split()[6]) * 4.35974394e-18 * constants.Na # Read spin multiplicity if above method was unsuccessful elif 'Electronic' in line and in_partition_functions and spin_multiplicity == 0: spin_multiplicity = int(float(line.split()[1].replace('D', 'E'))) elif 'Log10(Q)' in line: in_partition_functions = True # Read the next line in the file line = f.readline() if 'Error termination' in line: raise LogError(f'The Gaussian job in {self.path} did not converge.') # Read the next line in the file line = f.readline() return Conformer(E0=(e0 * 0.001, "kJ/mol"), modes=modes, spin_multiplicity=spin_multiplicity, optical_isomers=optical_isomers), unscaled_frequencies
def getStatmechData(self, molecule, thermoModel): """ Use the previously-loaded frequency database to generate a set of characteristic group frequencies corresponding to the speficied `molecule`. The provided thermo data in `thermoModel` is used to fit some frequencies and all hindered rotors to heat capacity data. """ conformer = Conformer() # Compute spin multiplicity # For closed-shell molecule the spin multiplicity is 1 # For monoradicals the spin multiplicity is 2 # For higher-order radicals the highest allowed spin multiplicity is assumed conformer.spinMultiplicity = molecule.getRadicalCount() + 1 # No need to determine rotational and vibrational modes for single atoms if len(molecule.atoms) < 2: return (conformer, None, None) linear = molecule.isLinear() numRotors = molecule.countInternalRotors() numVibrations = 3 * len(molecule.atoms) - (5 if linear else 6) - numRotors # Get characteristic frequency groups and the associated frequencies groupCount = self.getFrequencyGroups(molecule) frequencies = [] for entry, count in groupCount.iteritems(): if count != 0 and entry.data is not None: frequencies.extend(entry.data.generateFrequencies(count)) # Check that we have the right number of degrees of freedom specified if len(frequencies) > numVibrations: # We have too many vibrational modes difference = len(frequencies) - numVibrations # First try to remove hindered rotor modes until the proper number of modes remain if numRotors > difference: numRotors -= difference numVibrations = len(frequencies) logging.warning('For {0}, more characteristic frequencies were generated than vibrational modes allowed. Removed {1:d} internal rotors to compensate.'.format(molecule, difference)) # If that won't work, turn off functional groups until the problem is underspecified again else: groupsRemoved = 0 freqsRemoved = 0 freqCount = len(frequencies) while freqCount > numVibrations: minDegeneracy, minEntry = min([(entry.data.symmetry, entry) for entry in groupCount if groupCount[entry] > 0]) if groupCount[minEntry] > 1: groupCount[minEntry] -= 1 else: del groupCount[minEntry] groupsRemoved += 1 freqsRemoved += minDegeneracy freqCount -= minDegeneracy # Log warning logging.warning('For {0}, more characteristic frequencies were generated than vibrational modes allowed. Removed {1:d} groups ({2:d} frequencies) to compensate.'.format(molecule, groupsRemoved, freqsRemoved)) # Regenerate characteristic frequencies frequencies = [] for entry, count in groupCount.iteritems(): if count != 0: frequencies.extend(entry.data.generateFrequencies(count)) # Subtract out contributions to heat capacity from the group frequencies Tlist = numpy.arange(300.0, 1501.0, 100.0, numpy.float64) Cv = numpy.array([thermoModel.getHeatCapacity(T) / constants.R for T in Tlist], numpy.float64) ho = HarmonicOscillator(frequencies=(frequencies,"cm^-1")) for i in range(Tlist.shape[0]): Cv[i] -= ho.getHeatCapacity(Tlist[i]) / constants.R # Subtract out translational modes Cv -= 1.5 # Subtract out external rotational modes Cv -= (1.5 if not linear else 1.0) # Subtract out PV term (Cp -> Cv) Cv -= 1.0 # Fit remaining frequencies and hindered rotors to the heat capacity data from statmechfit import fitStatmechToHeatCapacity modes = fitStatmechToHeatCapacity(Tlist, Cv, numVibrations - len(frequencies), numRotors, molecule) for mode in modes: if isinstance(mode, HarmonicOscillator): uncertainties = [0 for f in frequencies] # probably shouldn't be zero frequencies.extend(mode.frequencies.value_si) uncertainties.extend(mode.frequencies.uncertainty) mode.frequencies.value_si = numpy.array(frequencies, numpy.float) mode.frequencies.uncertainty = numpy.array(uncertainties, numpy.float) break else: modes.insert(0, HarmonicOscillator(frequencies=(frequencies,"cm^-1"))) conformer.modes = modes return (conformer, None, None)
def update(self, reaction_model, pdep_settings): """ Regenerate the :math:`k(T,P)` values for this partial network if the network is marked as invalid. """ from rmgpy.kinetics import Arrhenius, KineticsData, MultiArrhenius # Get the parameters for the pressure dependence calculation job = pdep_settings job.network = self output_directory = pdep_settings.output_file Tmin = job.Tmin.value_si Tmax = job.Tmax.value_si Pmin = job.Pmin.value_si Pmax = job.Pmax.value_si Tlist = job.Tlist.value_si Plist = job.Plist.value_si maximum_grain_size = job.maximum_grain_size.value_si if job.maximum_grain_size is not None else 0.0 minimum_grain_count = job.minimum_grain_count method = job.method interpolation_model = job.interpolation_model active_j_rotor = job.active_j_rotor active_k_rotor = job.active_k_rotor rmgmode = job.rmgmode # Figure out which configurations are isomers, reactant channels, and product channels self.update_configurations(reaction_model) # Make sure we have high-P kinetics for all path reactions for rxn in self.path_reactions: if rxn.kinetics is None and rxn.reverse.kinetics is None: raise PressureDependenceError( 'Path reaction {0} with no high-pressure-limit kinetics encountered in ' 'PDepNetwork #{1:d}.'.format(rxn, self.index)) elif rxn.kinetics is not None and rxn.kinetics.is_pressure_dependent( ) and rxn.network_kinetics is None: raise PressureDependenceError( 'Pressure-dependent kinetics encountered for path reaction {0} in ' 'PDepNetwork #{1:d}.'.format(rxn, self.index)) # Do nothing if the network is already valid if self.valid: return # Do nothing if there are no explored wells if len(self.explored) == 0 and len(self.source) > 1: return # Log the network being updated logging.info("Updating {0!s}".format(self)) # Generate states data for unimolecular isomers and reactants if necessary for isomer in self.isomers: spec = isomer.species[0] if not spec.has_statmech(): spec.generate_statmech() for reactants in self.reactants: for spec in reactants.species: if not spec.has_statmech(): spec.generate_statmech() # Also generate states data for any path reaction reactants, so we can # always apply the ILT method in the direction the kinetics are known for reaction in self.path_reactions: for spec in reaction.reactants: if not spec.has_statmech(): spec.generate_statmech() # While we don't need the frequencies for product channels, we do need # the E0, so create a conformer object with the E0 for the product # channel species if necessary for products in self.products: for spec in products.species: if spec.conformer is None: spec.conformer = Conformer(E0=spec.get_thermo_data().E0) # Determine transition state energies on potential energy surface # In the absence of any better information, we simply set it to # be the reactant ground-state energy + the activation energy # Note that we need Arrhenius kinetics in order to do this for rxn in self.path_reactions: if rxn.kinetics is None: raise Exception( 'Path reaction "{0}" in PDepNetwork #{1:d} has no kinetics!' .format(rxn, self.index)) elif isinstance(rxn.kinetics, KineticsData): if len(rxn.reactants) == 1: kunits = 's^-1' elif len(rxn.reactants) == 2: kunits = 'm^3/(mol*s)' elif len(rxn.reactants) == 3: kunits = 'm^6/(mol^2*s)' else: kunits = '' rxn.kinetics = Arrhenius().fit_to_data( Tlist=rxn.kinetics.Tdata.value_si, klist=rxn.kinetics.kdata.value_si, kunits=kunits) elif isinstance(rxn.kinetics, MultiArrhenius): logging.info( 'Converting multiple kinetics to a single Arrhenius expression for reaction {rxn}' .format(rxn=rxn)) rxn.kinetics = rxn.kinetics.to_arrhenius(Tmin=Tmin, Tmax=Tmax) elif not isinstance(rxn.kinetics, Arrhenius) and rxn.network_kinetics is None: raise Exception( 'Path reaction "{0}" in PDepNetwork #{1:d} has invalid kinetics ' 'type "{2!s}".'.format(rxn, self.index, rxn.kinetics.__class__)) rxn.fix_barrier_height(force_positive=True) if rxn.network_kinetics is None: E0 = sum( [spec.conformer.E0.value_si for spec in rxn.reactants]) + rxn.kinetics.Ea.value_si else: E0 = sum([ spec.conformer.E0.value_si for spec in rxn.reactants ]) + rxn.network_kinetics.Ea.value_si rxn.transition_state = rmgpy.species.TransitionState( conformer=Conformer(E0=(E0 * 0.001, "kJ/mol"))) # Set collision model bath_gas = [ spec for spec in reaction_model.core.species if not spec.reactive ] assert len( bath_gas) > 0, 'No unreactive species to identify as bath gas' self.bath_gas = {} for spec in bath_gas: # is this really the only/best way to weight them? self.bath_gas[spec] = 1.0 / len(bath_gas) # Save input file if not self.label: self.label = str(self.index) if output_directory: job.save_input_file( os.path.join( output_directory, 'pdep', 'network{0:d}_{1:d}.py'.format(self.index, len(self.isomers)))) self.log_summary(level=logging.INFO) # Calculate the rate coefficients self.initialize(Tmin, Tmax, Pmin, Pmax, maximum_grain_size, minimum_grain_count, active_j_rotor, active_k_rotor, rmgmode) K = self.calculate_rate_coefficients(Tlist, Plist, method) # Generate PDepReaction objects configurations = [] configurations.extend([isom.species[:] for isom in self.isomers]) configurations.extend( [reactant.species[:] for reactant in self.reactants]) configurations.extend( [product.species[:] for product in self.products]) j = configurations.index(self.source) for i in range(K.shape[2]): if i != j: # Find the path reaction net_reaction = None for r in self.net_reactions: if r.has_template(configurations[j], configurations[i]): net_reaction = r # If net reaction does not already exist, make a new one if net_reaction is None: net_reaction = PDepReaction(reactants=configurations[j], products=configurations[i], network=self, kinetics=None) net_reaction = reaction_model.make_new_pdep_reaction( net_reaction) self.net_reactions.append(net_reaction) # Place the net reaction in the core or edge if necessary # Note that leak reactions are not placed in the edge if all([s in reaction_model.core.species for s in net_reaction.reactants]) \ and all([s in reaction_model.core.species for s in net_reaction.products]): # Check whether netReaction already exists in the core as a LibraryReaction for rxn in reaction_model.core.reactions: if isinstance(rxn, LibraryReaction) \ and rxn.is_isomorphic(net_reaction, either_direction=True) \ and not rxn.allow_pdep_route and not rxn.elementary_high_p: logging.info( 'Network reaction {0} matched an existing core reaction {1}' ' from the {2} library, and was not added to the model' .format(str(net_reaction), str(rxn), rxn.library)) break else: reaction_model.add_reaction_to_core(net_reaction) else: # Check whether netReaction already exists in the edge as a LibraryReaction for rxn in reaction_model.edge.reactions: if isinstance(rxn, LibraryReaction) \ and rxn.is_isomorphic(net_reaction, either_direction=True) \ and not rxn.allow_pdep_route and not rxn.elementary_high_p: logging.info( 'Network reaction {0} matched an existing edge reaction {1}' ' from the {2} library, and was not added to the model' .format(str(net_reaction), str(rxn), rxn.library)) break else: reaction_model.add_reaction_to_edge(net_reaction) # Set/update the net reaction kinetics using interpolation model kdata = K[:, :, i, j].copy() order = len(net_reaction.reactants) kdata *= 1e6**(order - 1) kunits = { 1: 's^-1', 2: 'cm^3/(mol*s)', 3: 'cm^6/(mol^2*s)' }[order] net_reaction.kinetics = job.fit_interpolation_model( Tlist, Plist, kdata, kunits) # Check: For each net reaction that has a path reaction, make # sure the k(T,P) values for the net reaction do not exceed # the k(T) values of the path reaction # Only check the k(T,P) value at the highest P and lowest T, # as this is the one most likely to be in the high-pressure # limit t = 0 p = len(Plist) - 1 for pathReaction in self.path_reactions: if pathReaction.is_isomerization(): # Don't check isomerization reactions, since their # k(T,P) values potentially contain both direct and # well-skipping contributions, and therefore could be # significantly larger than the direct k(T) value # (This can also happen for association/dissociation # reactions, but the effect is generally not too large) continue if pathReaction.reactants == net_reaction.reactants and pathReaction.products == net_reaction.products: if pathReaction.network_kinetics is not None: kinf = pathReaction.network_kinetics.get_rate_coefficient( Tlist[t]) else: kinf = pathReaction.kinetics.get_rate_coefficient( Tlist[t]) if K[t, p, i, j] > 2 * kinf: # To allow for a small discretization error logging.warning( 'k(T,P) for net reaction {0} exceeds high-P k(T) by {1:g} at {2:g} K, ' '{3:g} bar'.format(net_reaction, K[t, p, i, j] / kinf, Tlist[t], Plist[p] / 1e5)) logging.info( ' k(T,P) = {0:9.2e} k(T) = {1:9.2e}'. format(K[t, p, i, j], kinf)) break elif pathReaction.products == net_reaction.reactants and pathReaction.reactants == net_reaction.products: if pathReaction.network_kinetics is not None: kinf = pathReaction.network_kinetics.get_rate_coefficient( Tlist[t] ) / pathReaction.get_equilibrium_constant(Tlist[t]) else: kinf = pathReaction.kinetics.get_rate_coefficient( Tlist[t] ) / pathReaction.get_equilibrium_constant(Tlist[t]) if K[t, p, i, j] > 2 * kinf: # To allow for a small discretization error logging.warning( 'k(T,P) for net reaction {0} exceeds high-P k(T) by {1:g} at {2:g} K, ' '{3:g} bar'.format(net_reaction, K[t, p, i, j] / kinf, Tlist[t], Plist[p] / 1e5)) logging.info( ' k(T,P) = {0:9.2e} k(T) = {1:9.2e}'. format(K[t, p, i, j], kinf)) break # Delete intermediate arrays to conserve memory self.cleanup() # We're done processing this network, so mark it as valid self.valid = True
class TestConformer(unittest.TestCase): """ Contains unit tests of the :class:`Conformer` class. """ def setUp(self): """ A function run before each unit test in this class. """ self.ethylene = Conformer( E0=(0.0, "kJ/mol"), modes=[ IdealGasTranslation(mass=(28.03, "amu")), NonlinearRotor(inertia=([3.41526, 16.6498, 20.065], "amu*angstrom^2"), symmetry=4), HarmonicOscillator(frequencies=([ 828.397, 970.652, 977.223, 1052.93, 1233.55, 1367.56, 1465.09, 1672.25, 3098.46, 3111.7, 3165.79, 3193.54 ], "cm^-1")), ], spinMultiplicity=1, opticalIsomers=1, ) self.oxygen = Conformer( E0=(0.0, "kJ/mol"), modes=[ IdealGasTranslation(mass=(31.99, "amu")), LinearRotor(inertia=(11.6056, "amu*angstrom^2"), symmetry=2), HarmonicOscillator(frequencies=([1621.54], "cm^-1")), ], spinMultiplicity=3, opticalIsomers=1, ) # The following data is for ethane at the CBS-QB3 level self.coordinates = numpy.array([ [0.0000, 0.0000, 0.0000], [-0.0000, -0.0000, 1.0936], [1.0430, -0.0000, -0.3288], [-0.4484, 0.9417, -0.3288], [-0.7609, -1.2051, -0.5580], [-0.7609, -1.2051, -1.6516], [-0.3125, -2.1468, -0.2292], [-1.8039, -1.2051, -0.2293], ]) self.number = numpy.array([6, 1, 1, 1, 6, 1, 1, 1]) self.mass = numpy.array([ 12, 1.007825, 1.007825, 1.007825, 12, 1.007825, 1.007825, 1.007825 ]) self.E0 = -93.5097 self.conformer = Conformer( E0=(self.E0, "kJ/mol"), modes=[ IdealGasTranslation(mass=(30.0469, "amu")), NonlinearRotor(inertia=([6.27071, 25.3832, 25.3833], "amu*angstrom^2"), symmetry=6), HarmonicOscillator(frequencies=([ 818.917, 819.479, 987.099, 1206.76, 1207.05, 1396, 1411.35, 1489.73, 1489.95, 1492.49, 1492.66, 2995.36, 2996.06, 3040.77, 3041, 3065.86, 3066.02 ], "cm^-1")), HinderedRotor(inertia=(1.56768, "amu*angstrom^2"), symmetry=3, barrier=(2.69401, "kcal/mol"), quantum=False, semiclassical=False), ], spinMultiplicity=1, opticalIsomers=1, coordinates=(self.coordinates, "angstrom"), number=self.number, mass=(self.mass, "amu"), ) def test_getPartitionFunction_ethylene(self): """ Test the StatMech.getPartitionFunction() method for ethylene. """ Tlist = numpy.array([300, 500, 1000, 1500, 2000]) Qexplist = numpy.array( [4.05311e+09, 4.19728e+10, 2.82309e+12, 7.51135e+13, 1.16538e+15]) for T, Qexp in zip(Tlist, Qexplist): Qact = self.ethylene.getPartitionFunction(T) self.assertAlmostEqual(Qexp, Qact, delta=1e-4 * Qexp) def test_getHeatCapacity_ethylene(self): """ Test the StatMech.getHeatCapacity() method for ethylene. """ Tlist = numpy.array([300, 500, 1000, 1500, 2000]) Cvexplist = numpy.array([5.11186, 7.40447, 11.1659, 13.1221, 14.1617 ]) * constants.R for T, Cvexp in zip(Tlist, Cvexplist): Cvact = self.ethylene.getHeatCapacity(T) self.assertAlmostEqual(Cvexp, Cvact, 3) def test_getEnthalpy_ethylene(self): """ Test the StatMech.getEnthalpy() method for ethylene. """ Tlist = numpy.array([300, 500, 1000, 1500, 2000]) Hexplist = numpy.array([4.23129, 5.04826, 7.27337, 8.93167, 10.1223 ]) * constants.R * Tlist for T, Hexp in zip(Tlist, Hexplist): Hact = self.ethylene.getEnthalpy(T) self.assertAlmostEqual(Hexp, Hact, delta=1e-4 * Hexp) def test_getEntropy_ethylene(self): """ Test the StatMech.getEntropy() method for ethylene. """ Tlist = numpy.array([300, 500, 1000, 1500, 2000]) Sexplist = numpy.array([26.3540, 29.5085, 35.9422, 40.8817, 44.8142 ]) * constants.R for T, Sexp in zip(Tlist, Sexplist): Sact = self.ethylene.getEntropy(T) self.assertAlmostEqual(Sexp, Sact, 3) def test_getSumOfStates_ethylene(self): """ Test the StatMech.getSumOfStates() method for ethylene. """ Elist = numpy.arange(0, 5000 * 11.96, 2 * 11.96) sumStates = self.ethylene.getSumOfStates(Elist) densStates = self.ethylene.getDensityOfStates(Elist) for n in range(10, len(Elist)): self.assertTrue( 0.8 < numpy.sum(densStates[0:n + 1]) / sumStates[n] < 1.25, '{0} != {1}'.format(numpy.sum(densStates[0:n + 1]), sumStates[n])) def test_getDensityOfStates_ethylene(self): """ Test the StatMech.getDensityOfStates() method for ethylene. """ Elist = numpy.arange(0, 5000 * 11.96, 2 * 11.96) densStates = self.ethylene.getDensityOfStates(Elist) T = 100 Qact = numpy.sum(densStates * numpy.exp(-Elist / constants.R / T)) Qexp = self.ethylene.getPartitionFunction(T) self.assertAlmostEqual(Qexp, Qact, delta=1e-1 * Qexp) def test_getPartitionFunction_oxygen(self): """ Test the StatMech.getPartitionFunction() method for oxygen. """ Tlist = numpy.array([300, 500, 1000, 1500, 2000]) Qexplist = numpy.array( [1.55584e+09, 9.38339e+09, 1.16459e+11, 5.51016e+11, 1.72794e+12]) for T, Qexp in zip(Tlist, Qexplist): Qact = self.oxygen.getPartitionFunction(T) self.assertAlmostEqual(Qexp, Qact, delta=1e-4 * Qexp) def test_getHeatCapacity_oxygen(self): """ Test the StatMech.getHeatCapacity() method for oxygen. """ Tlist = numpy.array([300, 500, 1000, 1500, 2000]) Cvexplist = numpy.array([3.52538, 3.70877, 4.14751, 4.32063, 4.39392 ]) * constants.R for T, Cvexp in zip(Tlist, Cvexplist): Cvact = self.oxygen.getHeatCapacity(T) self.assertAlmostEqual(Cvexp, Cvact, 3) def test_getEnthalpy_oxygen(self): """ Test the StatMech.getEnthalpy() method for oxygen. """ Tlist = numpy.array([300, 500, 1000, 1500, 2000]) Hexplist = numpy.array([3.50326, 3.54432, 3.75062, 3.91623, 4.02765 ]) * constants.R * Tlist for T, Hexp in zip(Tlist, Hexplist): Hact = self.oxygen.getEnthalpy(T) self.assertAlmostEqual(Hexp, Hact, delta=1e-4 * Hexp) def test_getEntropy_oxygen(self): """ Test the StatMech.getEntropy() method for oxygen. """ Tlist = numpy.array([300, 500, 1000, 1500, 2000]) Sexplist = numpy.array([24.6685, 26.5065, 29.2314, 30.9513, 32.2056 ]) * constants.R for T, Sexp in zip(Tlist, Sexplist): Sact = self.oxygen.getEntropy(T) self.assertAlmostEqual(Sexp, Sact, 3) def test_getSumOfStates_oxygen(self): """ Test the StatMech.getSumOfStates() method for oxygen. """ Elist = numpy.arange(0, 5000 * 11.96, 2 * 11.96) sumStates = self.oxygen.getSumOfStates(Elist) densStates = self.oxygen.getDensityOfStates(Elist) for n in range(10, len(Elist)): self.assertTrue( 0.8 < numpy.sum(densStates[0:n + 1]) / sumStates[n] < 1.25, '{0} != {1}'.format(numpy.sum(densStates[0:n + 1]), sumStates[n])) def test_getDensityOfStates_oxygen(self): """ Test the StatMech.getDensityOfStates() method for oxygen. """ Elist = numpy.arange(0, 5000 * 11.96, 2 * 11.96) densStates = self.oxygen.getDensityOfStates(Elist) T = 100 Qact = numpy.sum(densStates * numpy.exp(-Elist / constants.R / T)) Qexp = self.oxygen.getPartitionFunction(T) self.assertAlmostEqual(Qexp, Qact, delta=1e-1 * Qexp) def test_getTotalMass(self): """ Test the Conformer.getTotalMass() method. """ self.assertAlmostEqual( self.conformer.getTotalMass() * constants.Na * 1000., numpy.sum(self.mass), 6) def test_getCenterOfMass(self): """ Test the Conformer.getCenterOfMass() method. """ cm = self.conformer.getCenterOfMass() self.assertAlmostEqual(cm[0] * 1e10, -0.38045, 4) self.assertAlmostEqual(cm[1] * 1e10, -0.60255, 4) self.assertAlmostEqual(cm[2] * 1e10, -0.27900, 4) def test_getMomentOfInertiaTensor(self): """ Test the Conformer.getMomentOfInertiaTensor() method. """ I = self.conformer.getMomentOfInertiaTensor() self.assertAlmostEqual(I[0, 0] * constants.Na * 1e23, 20.65968, 4) self.assertAlmostEqual(I[0, 1] * constants.Na * 1e23, -7.48115, 4) self.assertAlmostEqual(I[0, 2] * constants.Na * 1e23, -3.46416, 4) self.assertAlmostEqual(I[1, 0] * constants.Na * 1e23, -7.48115, 4) self.assertAlmostEqual(I[1, 1] * constants.Na * 1e23, 13.53472, 4) self.assertAlmostEqual(I[1, 2] * constants.Na * 1e23, -5.48630, 4) self.assertAlmostEqual(I[2, 0] * constants.Na * 1e23, -3.46416, 4) self.assertAlmostEqual(I[2, 1] * constants.Na * 1e23, -5.48630, 4) self.assertAlmostEqual(I[2, 2] * constants.Na * 1e23, 22.84296, 4) def test_getPrincipalMomentsOfInertia(self): """ Test the Conformer.getPrincipalMomentsOfInertia() method. """ I, V = self.conformer.getPrincipalMomentsOfInertia() self.assertAlmostEqual(I[0] * constants.Na * 1e23, 6.27074, 4) self.assertAlmostEqual(I[1] * constants.Na * 1e23, 25.38321, 3) self.assertAlmostEqual(I[2] * constants.Na * 1e23, 25.38341, 3) #print V # For some reason the axes seem to jump around (positioning and signs change) # but the absolute values should be the same as we expect expected = sorted([ 0.497140, 0.610114, 0.616938, 0.787360, 0.018454, 0.616218, 0.364578, 0.792099, 0.489554 ]) result = sorted(abs(V).flat) for i, j in zip(expected, result): self.assertAlmostEqual(i, j, 4) return # now because the following often fails: self.assertAlmostEqual(V[0, 0], 0.497140, 4) self.assertAlmostEqual(V[0, 1], -0.610114, 4) self.assertAlmostEqual(V[0, 2], -0.616938, 4) self.assertAlmostEqual(V[1, 0], 0.787360, 4) self.assertAlmostEqual(V[1, 1], 0.018454, 4) self.assertAlmostEqual(V[1, 2], 0.616218, 4) self.assertAlmostEqual(V[2, 0], 0.364578, 4) self.assertAlmostEqual(V[2, 1], 0.792099, 4) self.assertAlmostEqual(V[2, 2], -0.489554, 4) def test_getInternalReducedMomentOfInertia(self): """ Test the Conformer.getInternalReducedMomentOfInertia() method. """ I = self.conformer.getInternalReducedMomentOfInertia(pivots=[1, 5], top1=[1, 2, 3, 4]) self.assertAlmostEqual(I * constants.Na * 1e23, 1.56768, 4) def test_getNumberDegreesOfFreedom(self): """ Test the Conformer.getNumberDegreesOfFreedom() method. """ #this is for ethane: numberDegreesOfFreedom = self.conformer.getNumberDegreesOfFreedom() self.assertEqual(numberDegreesOfFreedom, 24) #this is for ethylene: # It doesn't check aganist 3*Natoms, because Natoms is not declared. numberDegreesOfFreedom = self.ethylene.getNumberDegreesOfFreedom() self.assertEqual(numberDegreesOfFreedom, 18) #this is for CO # It doesn't check aganist 3*Natoms, because Natoms is not declared. numberDegreesOfFreedom = self.oxygen.getNumberDegreesOfFreedom() self.assertEqual(numberDegreesOfFreedom, 6)
def loadConformer(self, symmetry=None, spinMultiplicity=None, opticalIsomers=1): """ Load the molecular degree of freedom data from a log file created as the result of a Gaussian "Freq" quantum chemistry calculation. As Gaussian's guess of the external symmetry number is not always correct, you can use the `symmetry` parameter to substitute your own value; if not provided, the value in the Gaussian log file will be adopted. In a log file with multiple Thermochemistry sections, only the last one will be kept. """ modes = [] E0 = 0.0 f = open(self.path, 'r') line = f.readline() while line != '': # The data we want is in the Thermochemistry section of the output if '- Thermochemistry -' in line: modes = [] inPartitionFunctions = False line = f.readline() while line != '': # This marks the end of the thermochemistry section if '-------------------------------------------------------------------' in line: break # Read molecular mass for external translational modes elif 'Molecular mass:' in line: mass = float(line.split()[2]) translation = IdealGasTranslation(mass=(mass,"amu")) modes.append(translation) # Read Gaussian's estimate of the external symmetry number elif 'Rotational symmetry number' in line and symmetry is None: symmetry = int(float(line.split()[3])) # Read moments of inertia for external rotational modes elif 'Rotational constants (GHZ):' in line: inertia = [float(d) for d in line.split()[-3:]] for i in range(3): inertia[i] = constants.h / (8 * constants.pi * constants.pi * inertia[i] * 1e9) *constants.Na*1e23 rotation = NonlinearRotor(inertia=(inertia,"amu*angstrom^2"), symmetry=symmetry) modes.append(rotation) elif 'Rotational constant (GHZ):' in line: inertia = [float(line.split()[3])] inertia[0] = constants.h / (8 * constants.pi * constants.pi * inertia[0] * 1e9) *constants.Na*1e23 rotation = LinearRotor(inertia=(inertia[0],"amu*angstrom^2"), symmetry=symmetry) modes.append(rotation) # Read vibrational modes elif 'Vibrational temperatures:' in line: frequencies = [] frequencies.extend([float(d) for d in line.split()[2:]]) line = f.readline() frequencies.extend([float(d) for d in line.split()[1:]]) line = f.readline() while line.strip() != '': frequencies.extend([float(d) for d in line.split()]) line = f.readline() # Convert from K to cm^-1 if len(frequencies) > 0: frequencies = [freq * 0.695039 for freq in frequencies] # kB = 0.695039 cm^-1/K vibration = HarmonicOscillator(frequencies=(frequencies,"cm^-1")) modes.append(vibration) # Read ground-state energy elif 'Sum of electronic and zero-point Energies=' in line: E0 = float(line.split()[6]) * 4.35974394e-18 * constants.Na # Read spin multiplicity if not explicitly given elif 'Electronic' in line and inPartitionFunctions and spinMultiplicity is None: spinMultiplicity = int(float(line.split()[1].replace('D', 'E'))) elif 'Log10(Q)' in line: inPartitionFunctions = True # Read the next line in the file line = f.readline() # Read the next line in the file line = f.readline() # Close file when finished f.close() return Conformer(E0=(E0*0.001,"kJ/mol"), modes=modes, spinMultiplicity=spinMultiplicity, opticalIsomers=opticalIsomers)
def get_enthalpy_of_formation(self, freq_scale_factor=1.0, apply_bond_corrections=True): """ Calculate the enthalpy of formation at 298.15 K. Apply bond energy corrections if desired and if model chemistry is compatible. """ temperature = 298.15 mol = pybel.readstring('smi', self.smiles) # Use OBMol to extract bond types # Use RMG molecule to determine if it's linear # Assume it's not linear if we can't parse SMILES with RMG rmg_mol = None try: rmg_mol = Molecule().fromSMILES(self.smiles) except AtomTypeError: try: rmg_mol = Molecule().fromSMILES(self.smiles2) except AtomTypeError: try: rmg_mol = Molecule().fromInChI(self.inchi) except AtomTypeError: warnings.warn( 'Could not determine linearity from RMG molecule in {}' .format(self.file_name)) if rmg_mol is not None: is_linear = rmg_mol.isLinear() else: is_linear = False # Translation translation = IdealGasTranslation() # Rotation if is_linear: rotation = LinearRotor() else: rotation = NonlinearRotor( rotationalConstant=(self.rotational_consts, 'GHz')) # Vibration freqs = [f * freq_scale_factor for f in self.freqs] # Apply scale factor vibration = HarmonicOscillator(frequencies=(freqs, 'cm^-1')) # Group modes modes = [translation, rotation, vibration] conformer = Conformer(modes=modes) # Energy e0 = self.e0 * constants.E_h * constants.Na zpe = self.zpe * constants.E_h * constants.Na * freq_scale_factor # Bring energy to gas phase reference state at 298.15K atom_energies = energy_data.atom_energies[self.model_chemistry] for element in self.elements: e0 -= atom_energies[element] * constants.E_h * constants.Na e0 += self.enthalpy_corrections[element] * 4184.0 if apply_bond_corrections: bond_energies = energy_data.bond_energy_corrections[ self.model_chemistry] for bond in pybel.ob.OBMolBondIter(mol.OBMol): bond_symbol_split = [ self.atomic_num_dict[bond.GetBeginAtom().GetAtomicNum()], self.bond_symbols[bond.GetBondOrder()], self.atomic_num_dict[bond.GetEndAtom().GetAtomicNum()] ] try: bond_energy = bond_energies[''.join(bond_symbol_split)] except KeyError: bond_energy = bond_energies[''.join( bond_symbol_split[::-1])] # Try reverse order e0 += bond_energy * 4184.0 conformer.E0 = (e0 + zpe, 'J/mol') self.hf298 = conformer.getEnthalpy(temperature) + conformer.E0.value_si return self.hf298
def setUp(self): """ A function run before each unit test in this class. """ self.ethylene = Conformer( E0=(0.0, "kJ/mol"), modes=[ IdealGasTranslation(mass=(28.03, "amu")), NonlinearRotor(inertia=([3.41526, 16.6498, 20.065], "amu*angstrom^2"), symmetry=4), HarmonicOscillator(frequencies=([ 828.397, 970.652, 977.223, 1052.93, 1233.55, 1367.56, 1465.09, 1672.25, 3098.46, 3111.7, 3165.79, 3193.54 ], "cm^-1")), ], spin_multiplicity=1, optical_isomers=1, ) self.oxygen = Conformer( E0=(0.0, "kJ/mol"), modes=[ IdealGasTranslation(mass=(31.99, "amu")), LinearRotor(inertia=(11.6056, "amu*angstrom^2"), symmetry=2), HarmonicOscillator(frequencies=([1621.54], "cm^-1")), ], spin_multiplicity=3, optical_isomers=1, ) # The following data is for ethane at the CBS-QB3 level self.coordinates = np.array([ [0.0000, 0.0000, 0.0000], [-0.0000, -0.0000, 1.0936], [1.0430, -0.0000, -0.3288], [-0.4484, 0.9417, -0.3288], [-0.7609, -1.2051, -0.5580], [-0.7609, -1.2051, -1.6516], [-0.3125, -2.1468, -0.2292], [-1.8039, -1.2051, -0.2293], ]) self.number = np.array([6, 1, 1, 1, 6, 1, 1, 1]) self.mass = np.array([ 12, 1.007825, 1.007825, 1.007825, 12, 1.007825, 1.007825, 1.007825 ]) self.E0 = -93.5097 self.conformer = Conformer( E0=(self.E0, "kJ/mol"), modes=[ IdealGasTranslation(mass=(30.0469, "amu")), NonlinearRotor(inertia=([6.27071, 25.3832, 25.3833], "amu*angstrom^2"), symmetry=6), HarmonicOscillator(frequencies=([ 818.917, 819.479, 987.099, 1206.76, 1207.05, 1396, 1411.35, 1489.73, 1489.95, 1492.49, 1492.66, 2995.36, 2996.06, 3040.77, 3041, 3065.86, 3066.02 ], "cm^-1")), HinderedRotor(inertia=(1.56768, "amu*angstrom^2"), symmetry=3, barrier=(2.69401, "kcal/mol"), quantum=False, semiclassical=False), ], spin_multiplicity=1, optical_isomers=1, coordinates=(self.coordinates, "angstrom"), number=self.number, mass=(self.mass, "amu"), )
def loadConformer(self, symmetry=None, spinMultiplicity=0, opticalIsomers=None, label=''): """ Load the molecular degree of freedom data from an output file created as the result of a QChem "Freq" calculation. As QChem's guess of the external symmetry number is not always correct, you can use the `symmetry` parameter to substitute your own value; if not provided, the value in the QChem output file will be adopted. """ modes = []; freq = []; mmass = []; rot = []; inertia = [] unscaled_frequencies = [] E0 = 0.0 if opticalIsomers is None or symmetry is None: _opticalIsomers, _symmetry = self.get_optical_isomers_and_symmetry_number() if opticalIsomers is None: opticalIsomers = _opticalIsomers if symmetry is None: symmetry = _symmetry f = open(self.path, 'r') line = f.readline() while line != '': # Read spin multiplicity if not explicitly given if '$molecule' in line and spinMultiplicity == 0: line = f.readline() if len(line.split()) == 2: spinMultiplicity = int(float(line.split()[1])) logging.debug('Conformer {0} is assigned a spin multiplicity of {1}'.format(label,spinMultiplicity)) # The rest of the data we want is in the Thermochemistry section of the output elif 'VIBRATIONAL ANALYSIS' in line: modes = [] line = f.readline() while line != '': # This marks the end of the thermochemistry section if 'Thank you very much for using Q-Chem.' in line: break # Read vibrational modes elif 'VIBRATIONAL FREQUENCIES (CM**-1)' in line: frequencies = [] while 'STANDARD THERMODYNAMIC QUANTITIES AT' not in line: if ' Frequency:' in line: if len(line.split()) == 4: frequencies.extend([float(d) for d in line.split()[-3:]]) elif len(line.split()) == 3: frequencies.extend([float(d) for d in line.split()[-2:]]) elif len(line.split()) == 2: frequencies.extend([float(d) for d in line.split()[-1:]]) line = f.readline() line = f.readline() # If there is an imaginary frequency, remove it if frequencies[0] < 0.0: frequencies = frequencies[1:] unscaled_frequencies = frequencies vibration = HarmonicOscillator(frequencies=(frequencies,"cm^-1")) # modes.append(vibration) freq.append(vibration) # Read molecular mass for external translational modes elif 'Molecular Mass:' in line: mass = float(line.split()[2]) translation = IdealGasTranslation(mass=(mass,"amu")) # modes.append(translation) mmass.append(translation) # Read moments of inertia for external rotational modes, given in atomic units elif 'Eigenvalues --' in line: inertia = [float(d) for d in line.split()[-3:]] # Read the next line in the file line = f.readline() # Read the next line in the file line = f.readline() if len(inertia): if inertia[0] == 0.0: # If the first eigenvalue is 0, the rotor is linear inertia.remove(0.0) logging.debug('inertia is {}'.format(str(inertia))) for i in range(2): inertia[i] *= (constants.a0 / 1e-10) ** 2 inertia = numpy.sqrt(inertia[0] * inertia[1]) rotation = LinearRotor(inertia=(inertia, "amu*angstrom^2"), symmetry=symmetry) rot.append(rotation) else: for i in range(3): inertia[i] *= (constants.a0 / 1e-10) ** 2 rotation = NonlinearRotor(inertia=(inertia, "amu*angstrom^2"), symmetry=symmetry) # modes.append(rotation) rot.append(rotation) inertia = [] # Close file when finished f.close() modes = mmass + rot + freq return Conformer(E0=(E0*0.001,"kJ/mol"), modes=modes, spinMultiplicity=spinMultiplicity, opticalIsomers=opticalIsomers), unscaled_frequencies
class TestConformer(unittest.TestCase): """ Contains unit tests of the :class:`Conformer` class. """ def setUp(self): """ A function run before each unit test in this class. """ self.ethylene = Conformer( E0 = (0.0,"kJ/mol"), modes = [ IdealGasTranslation(mass=(28.03,"amu")), NonlinearRotor(inertia=([3.41526,16.6498,20.065],"amu*angstrom^2"), symmetry=4), HarmonicOscillator(frequencies=([828.397,970.652,977.223,1052.93,1233.55,1367.56,1465.09,1672.25,3098.46,3111.7,3165.79,3193.54],"cm^-1")), ], spinMultiplicity = 1, opticalIsomers = 1, ) self.oxygen = Conformer( E0 = (0.0,"kJ/mol"), modes = [ IdealGasTranslation(mass=(31.99,"amu")), LinearRotor(inertia=(11.6056,"amu*angstrom^2"), symmetry=2), HarmonicOscillator(frequencies=([1621.54],"cm^-1")), ], spinMultiplicity = 3, opticalIsomers = 1, ) # The following data is for ethane at the CBS-QB3 level self.coordinates = numpy.array([ [ 0.0000, 0.0000, 0.0000], [ -0.0000, -0.0000, 1.0936], [ 1.0430, -0.0000, -0.3288], [ -0.4484, 0.9417, -0.3288], [ -0.7609, -1.2051, -0.5580], [ -0.7609, -1.2051, -1.6516], [ -0.3125, -2.1468, -0.2292], [ -1.8039, -1.2051, -0.2293], ]) self.number = numpy.array([6, 1, 1, 1, 6, 1, 1, 1]) self.mass = numpy.array([12, 1.007825, 1.007825, 1.007825, 12, 1.007825, 1.007825, 1.007825]) self.E0 = -93.5097 self.conformer = Conformer( E0 = (self.E0,"kJ/mol"), modes = [ IdealGasTranslation(mass=(30.0469,"amu")), NonlinearRotor(inertia=([6.27071,25.3832,25.3833],"amu*angstrom^2"), symmetry=6), HarmonicOscillator(frequencies=([818.917,819.479,987.099,1206.76,1207.05,1396,1411.35,1489.73,1489.95,1492.49,1492.66,2995.36,2996.06,3040.77,3041,3065.86,3066.02],"cm^-1")), HinderedRotor(inertia=(1.56768,"amu*angstrom^2"), symmetry=3, barrier=(2.69401,"kcal/mol"), quantum=False, semiclassical=False), ], spinMultiplicity = 1, opticalIsomers = 1, coordinates = (self.coordinates,"angstrom"), number = self.number, mass = (self.mass,"amu"), ) def test_getPartitionFunction_ethylene(self): """ Test the StatMech.getPartitionFunction() method for ethylene. """ Tlist = numpy.array([300,500,1000,1500,2000]) Qexplist = numpy.array([4.05311e+09, 4.19728e+10, 2.82309e+12, 7.51135e+13, 1.16538e+15]) for T, Qexp in zip(Tlist, Qexplist): Qact = self.ethylene.getPartitionFunction(T) self.assertAlmostEqual(Qexp, Qact, delta=1e-4*Qexp) def test_getHeatCapacity_ethylene(self): """ Test the StatMech.getHeatCapacity() method for ethylene. """ Tlist = numpy.array([300,500,1000,1500,2000]) Cvexplist = numpy.array([5.11186, 7.40447, 11.1659, 13.1221, 14.1617]) * constants.R for T, Cvexp in zip(Tlist, Cvexplist): Cvact = self.ethylene.getHeatCapacity(T) self.assertAlmostEqual(Cvexp, Cvact, 3) def test_getEnthalpy_ethylene(self): """ Test the StatMech.getEnthalpy() method for ethylene. """ Tlist = numpy.array([300,500,1000,1500,2000]) Hexplist = numpy.array([4.23129, 5.04826, 7.27337, 8.93167, 10.1223]) * constants.R * Tlist for T, Hexp in zip(Tlist, Hexplist): Hact = self.ethylene.getEnthalpy(T) self.assertAlmostEqual(Hexp, Hact, delta=1e-4*Hexp) def test_getEntropy_ethylene(self): """ Test the StatMech.getEntropy() method for ethylene. """ Tlist = numpy.array([300,500,1000,1500,2000]) Sexplist = numpy.array([26.3540, 29.5085, 35.9422, 40.8817, 44.8142]) * constants.R for T, Sexp in zip(Tlist, Sexplist): Sact = self.ethylene.getEntropy(T) self.assertAlmostEqual(Sexp, Sact, 3) def test_getSumOfStates_ethylene(self): """ Test the StatMech.getSumOfStates() method for ethylene. """ Elist = numpy.arange(0, 5000*11.96, 2*11.96) sumStates = self.ethylene.getSumOfStates(Elist) densStates = self.ethylene.getDensityOfStates(Elist) for n in range(10, len(Elist)): self.assertTrue(0.8 < numpy.sum(densStates[0:n+1]) / sumStates[n] < 1.25, '{0} != {1}'.format(numpy.sum(densStates[0:n+1]), sumStates[n])) def test_getDensityOfStates_ethylene(self): """ Test the StatMech.getDensityOfStates() method for ethylene. """ Elist = numpy.arange(0, 5000*11.96, 2*11.96) densStates = self.ethylene.getDensityOfStates(Elist) T = 100 Qact = numpy.sum(densStates * numpy.exp(-Elist / constants.R / T)) Qexp = self.ethylene.getPartitionFunction(T) self.assertAlmostEqual(Qexp, Qact, delta=1e-1*Qexp) def test_getPartitionFunction_oxygen(self): """ Test the StatMech.getPartitionFunction() method for oxygen. """ Tlist = numpy.array([300,500,1000,1500,2000]) Qexplist = numpy.array([1.55584e+09, 9.38339e+09, 1.16459e+11, 5.51016e+11, 1.72794e+12]) for T, Qexp in zip(Tlist, Qexplist): Qact = self.oxygen.getPartitionFunction(T) self.assertAlmostEqual(Qexp, Qact, delta=1e-4*Qexp) def test_getHeatCapacity_oxygen(self): """ Test the StatMech.getHeatCapacity() method for oxygen. """ Tlist = numpy.array([300,500,1000,1500,2000]) Cvexplist = numpy.array([3.52538, 3.70877, 4.14751, 4.32063, 4.39392]) * constants.R for T, Cvexp in zip(Tlist, Cvexplist): Cvact = self.oxygen.getHeatCapacity(T) self.assertAlmostEqual(Cvexp, Cvact, 3) def test_getEnthalpy_oxygen(self): """ Test the StatMech.getEnthalpy() method for oxygen. """ Tlist = numpy.array([300,500,1000,1500,2000]) Hexplist = numpy.array([3.50326, 3.54432, 3.75062, 3.91623, 4.02765]) * constants.R * Tlist for T, Hexp in zip(Tlist, Hexplist): Hact = self.oxygen.getEnthalpy(T) self.assertAlmostEqual(Hexp, Hact, delta=1e-4*Hexp) def test_getEntropy_oxygen(self): """ Test the StatMech.getEntropy() method for oxygen. """ Tlist = numpy.array([300,500,1000,1500,2000]) Sexplist = numpy.array([24.6685, 26.5065, 29.2314, 30.9513, 32.2056]) * constants.R for T, Sexp in zip(Tlist, Sexplist): Sact = self.oxygen.getEntropy(T) self.assertAlmostEqual(Sexp, Sact, 3) def test_getSumOfStates_oxygen(self): """ Test the StatMech.getSumOfStates() method for oxygen. """ Elist = numpy.arange(0, 5000*11.96, 2*11.96) sumStates = self.oxygen.getSumOfStates(Elist) densStates = self.oxygen.getDensityOfStates(Elist) for n in range(10, len(Elist)): self.assertTrue(0.8 < numpy.sum(densStates[0:n+1]) / sumStates[n] < 1.25, '{0} != {1}'.format(numpy.sum(densStates[0:n+1]), sumStates[n])) def test_getDensityOfStates_oxygen(self): """ Test the StatMech.getDensityOfStates() method for oxygen. """ Elist = numpy.arange(0, 5000*11.96, 2*11.96) densStates = self.oxygen.getDensityOfStates(Elist) T = 100 Qact = numpy.sum(densStates * numpy.exp(-Elist / constants.R / T)) Qexp = self.oxygen.getPartitionFunction(T) self.assertAlmostEqual(Qexp, Qact, delta=1e-1*Qexp) def test_getTotalMass(self): """ Test the Conformer.getTotalMass() method. """ self.assertAlmostEqual(self.conformer.getTotalMass()*constants.Na*1000., numpy.sum(self.mass), 6) def test_getCenterOfMass(self): """ Test the Conformer.getCenterOfMass() method. """ cm = self.conformer.getCenterOfMass() self.assertAlmostEqual(cm[0]*1e10, -0.38045, 4) self.assertAlmostEqual(cm[1]*1e10, -0.60255, 4) self.assertAlmostEqual(cm[2]*1e10, -0.27900, 4) def test_getMomentOfInertiaTensor(self): """ Test the Conformer.getMomentOfInertiaTensor() method. """ I = self.conformer.getMomentOfInertiaTensor() self.assertAlmostEqual(I[0,0]*constants.Na*1e23, 20.65968, 4) self.assertAlmostEqual(I[0,1]*constants.Na*1e23, -7.48115, 4) self.assertAlmostEqual(I[0,2]*constants.Na*1e23, -3.46416, 4) self.assertAlmostEqual(I[1,0]*constants.Na*1e23, -7.48115, 4) self.assertAlmostEqual(I[1,1]*constants.Na*1e23, 13.53472, 4) self.assertAlmostEqual(I[1,2]*constants.Na*1e23, -5.48630, 4) self.assertAlmostEqual(I[2,0]*constants.Na*1e23, -3.46416, 4) self.assertAlmostEqual(I[2,1]*constants.Na*1e23, -5.48630, 4) self.assertAlmostEqual(I[2,2]*constants.Na*1e23, 22.84296, 4) def test_getPrincipalMomentsOfInertia(self): """ Test the Conformer.getPrincipalMomentsOfInertia() method. """ I, V = self.conformer.getPrincipalMomentsOfInertia() self.assertAlmostEqual(I[0]*constants.Na*1e23, 6.27074, 4) self.assertAlmostEqual(I[1]*constants.Na*1e23, 25.38321, 3) self.assertAlmostEqual(I[2]*constants.Na*1e23, 25.38341, 3) #print V # For some reason the axes seem to jump around (positioning and signs change) # but the absolute values should be the same as we expect expected = sorted([0.497140, 0.610114, 0.616938, 0.787360, 0.018454, 0.616218, 0.364578, 0.792099, 0.489554]) result = sorted(abs(V).flat) for i,j in zip(expected, result): self.assertAlmostEqual(i, j, 4) return # now because the following often fails: self.assertAlmostEqual(V[0,0], 0.497140, 4) self.assertAlmostEqual(V[0,1], -0.610114, 4) self.assertAlmostEqual(V[0,2], -0.616938, 4) self.assertAlmostEqual(V[1,0], 0.787360, 4) self.assertAlmostEqual(V[1,1], 0.018454, 4) self.assertAlmostEqual(V[1,2], 0.616218, 4) self.assertAlmostEqual(V[2,0], 0.364578, 4) self.assertAlmostEqual(V[2,1], 0.792099, 4) self.assertAlmostEqual(V[2,2], -0.489554, 4) def test_getInternalReducedMomentOfInertia(self): """ Test the Conformer.getInternalReducedMomentOfInertia() method. """ I = self.conformer.getInternalReducedMomentOfInertia(pivots=[1,5], top1=[1,2,3,4]) self.assertAlmostEqual(I*constants.Na*1e23, 1.56768, 4) def test_getNumberDegreesOfFreedom(self): """ Test the Conformer.getNumberDegreesOfFreedom() method. """ #this is for ethane: numberDegreesOfFreedom = self.conformer.getNumberDegreesOfFreedom() self.assertEqual(numberDegreesOfFreedom, 24) #this is for ethylene: # It doesn't check aganist 3*Natoms, because Natoms is not declared. numberDegreesOfFreedom = self.ethylene.getNumberDegreesOfFreedom() self.assertEqual(numberDegreesOfFreedom, 18) #this is for CO # It doesn't check aganist 3*Natoms, because Natoms is not declared. numberDegreesOfFreedom = self.oxygen.getNumberDegreesOfFreedom() self.assertEqual(numberDegreesOfFreedom, 6)
def setUp(self): """ A method that is run before each unit test in this class. """ self.species = Species( index=1, label='C2H4', thermo=ThermoData( Tdata=([300.0, 400.0, 500.0, 600.0, 800.0, 1000.0, 1500.0], 'K'), Cpdata=([3.0, 4.0, 5.0, 6.0, 8.0, 10.0, 15.0], 'cal/(mol*K)'), H298=(-20.0, 'kcal/mol'), S298=(50.0, 'cal/(mol*K)'), Tmin=(300.0, 'K'), Tmax=(2000.0, 'K'), ), conformer=Conformer( E0=(0.0, 'kJ/mol'), modes=[ IdealGasTranslation(mass=(28.03, 'amu')), NonlinearRotor( inertia=([5.6952e-47, 2.7758e-46, 3.3454e-46], 'kg*m^2'), symmetry=1), HarmonicOscillator(frequencies=([ 834.50, 973.31, 975.37, 1067.1, 1238.5, 1379.5, 1472.3, 1691.3, 3121.6, 3136.7, 3192.5, 3221.0 ], 'cm^-1')), ], spin_multiplicity=1, optical_isomers=1, ), molecule=[Molecule().from_smiles('C=C')], transport_data=TransportData(sigma=(1, 'angstrom'), epsilon=(100, 'K')), molecular_weight=(28.03, 'amu'), reactive=True, ) self.species2 = Species().from_adjacency_list(""" 1 C u0 p0 c0 {2,D} {6,S} {7,S} 2 C u0 p0 c0 {1,D} {3,S} {8,S} 3 C u0 p0 c0 {2,S} {4,D} {9,S} 4 C u0 p0 c0 {3,D} {5,S} {10,S} 5 C u0 p0 c0 {4,S} {6,D} {11,S} 6 C u0 p0 c0 {1,S} {5,D} {12,S} 7 H u0 p0 c0 {1,S} 8 H u0 p0 c0 {2,S} 9 H u0 p0 c0 {3,S} 10 H u0 p0 c0 {4,S} 11 H u0 p0 c0 {5,S} 12 H u0 p0 c0 {6,S} """) self.species3 = Species().from_adjacency_list(""" multiplicity 2 1 O u1 p2 c0 {3,S} 2 O u0 p2 c0 {3,D} 3 N u0 p1 c0 {1,S} {2,D} """) self.species4 = Species().from_adjacency_list(""" Propane multiplicity 1 1 C u0 p0 c0 {2,S} {4,S} {5,S} {6,S} 2 C u0 p0 c0 {1,S} {3,S} {7,S} {8,S} 3 C u0 p0 c0 {2,S} {9,S} {10,S} {11,S} 4 H u0 p0 c0 {1,S} 5 H u0 p0 c0 {1,S} 6 H u0 p0 c0 {1,S} 7 H u0 p0 c0 {2,S} 8 H u0 p0 c0 {2,S} 9 H u0 p0 c0 {3,S} 10 H u0 p0 c0 {3,S} 11 H u0 p0 c0 {3,S} """)
def loadFAMEInput(path, moleculeDict=None): """ Load the contents of a FAME input file into the MEASURE object. FAME is an early version of MEASURE written in Fortran and used by RMG-Java. This script enables importing FAME input files into MEASURE so we can use the additional functionality that MEASURE provides. Note that it is mostly designed to load the FAME input files generated automatically by RMG-Java, and may not load hand-crafted FAME input files. If you specify a `moleculeDict`, then this script will use it to associate the species with their structures. """ def readMeaningfulLine(f): line = f.readline() while line != '': line = line.strip() if len(line) > 0 and line[0] != '#': return line else: line = f.readline() return '' moleculeDict = moleculeDict or {} logging.info('Loading file "{0}"...'.format(path)) f = open(path) job = PressureDependenceJob(network=None) # Read method method = readMeaningfulLine(f).lower() if method == 'modifiedstrongcollision': job.method = 'modified strong collision' elif method == 'reservoirstate': job.method = 'reservoir state' # Read temperatures Tcount, Tunits, Tmin, Tmax = readMeaningfulLine(f).split() job.Tmin = Quantity(float(Tmin), Tunits) job.Tmax = Quantity(float(Tmax), Tunits) job.Tcount = int(Tcount) Tlist = [] for i in range(int(Tcount)): Tlist.append(float(readMeaningfulLine(f))) job.Tlist = Quantity(Tlist, Tunits) # Read pressures Pcount, Punits, Pmin, Pmax = readMeaningfulLine(f).split() job.Pmin = Quantity(float(Pmin), Punits) job.Pmax = Quantity(float(Pmax), Punits) job.Pcount = int(Pcount) Plist = [] for i in range(int(Pcount)): Plist.append(float(readMeaningfulLine(f))) job.Plist = Quantity(Plist, Punits) # Read interpolation model model = readMeaningfulLine(f).split() if model[0].lower() == 'chebyshev': job.interpolationModel = ('chebyshev', int(model[1]), int(model[2])) elif model[0].lower() == 'pdeparrhenius': job.interpolationModel = ('pdeparrhenius',) # Read grain size or number of grains job.minimumGrainCount = 0 job.maximumGrainSize = None for i in range(2): data = readMeaningfulLine(f).split() if data[0].lower() == 'numgrains': job.minimumGrainCount = int(data[1]) elif data[0].lower() == 'grainsize': job.maximumGrainSize = (float(data[2]), data[1]) # A FAME file is almost certainly created during an RMG job, so use RMG mode job.rmgmode = True # Create the Network job.network = Network() # Read collision model data = readMeaningfulLine(f) assert data.lower() == 'singleexpdown' alpha0units, alpha0 = readMeaningfulLine(f).split() T0units, T0 = readMeaningfulLine(f).split() n = readMeaningfulLine(f) energyTransferModel = SingleExponentialDown( alpha0 = Quantity(float(alpha0), alpha0units), T0 = Quantity(float(T0), T0units), n = float(n), ) speciesDict = {} # Read bath gas parameters bathGas = Species(label='bath_gas', energyTransferModel=energyTransferModel) molWtunits, molWt = readMeaningfulLine(f).split() if molWtunits == 'u': molWtunits = 'amu' bathGas.molecularWeight = Quantity(float(molWt), molWtunits) sigmaLJunits, sigmaLJ = readMeaningfulLine(f).split() epsilonLJunits, epsilonLJ = readMeaningfulLine(f).split() assert epsilonLJunits == 'J' bathGas.transportData = TransportData( sigma = Quantity(float(sigmaLJ), sigmaLJunits), epsilon = Quantity(float(epsilonLJ) / constants.kB, 'K'), ) job.network.bathGas = {bathGas: 1.0} # Read species data Nspec = int(readMeaningfulLine(f)) for i in range(Nspec): species = Species() species.conformer = Conformer() species.energyTransferModel = energyTransferModel # Read species label species.label = readMeaningfulLine(f) speciesDict[species.label] = species if species.label in moleculeDict: species.molecule = [moleculeDict[species.label]] # Read species E0 E0units, E0 = readMeaningfulLine(f).split() species.conformer.E0 = Quantity(float(E0), E0units) species.conformer.E0.units = 'kJ/mol' # Read species thermo data H298units, H298 = readMeaningfulLine(f).split() S298units, S298 = readMeaningfulLine(f).split() Cpcount, Cpunits = readMeaningfulLine(f).split() Cpdata = [] for i in range(int(Cpcount)): Cpdata.append(float(readMeaningfulLine(f))) if S298units == 'J/mol*K': S298units = 'J/(mol*K)' if Cpunits == 'J/mol*K': Cpunits = 'J/(mol*K)' species.thermo = ThermoData( H298 = Quantity(float(H298), H298units), S298 = Quantity(float(S298), S298units), Tdata = Quantity([300,400,500,600,800,1000,1500], "K"), Cpdata = Quantity(Cpdata, Cpunits), Cp0 = (Cpdata[0], Cpunits), CpInf = (Cpdata[-1], Cpunits), ) # Read species collision parameters molWtunits, molWt = readMeaningfulLine(f).split() if molWtunits == 'u': molWtunits = 'amu' species.molecularWeight = Quantity(float(molWt), molWtunits) sigmaLJunits, sigmaLJ = readMeaningfulLine(f).split() epsilonLJunits, epsilonLJ = readMeaningfulLine(f).split() assert epsilonLJunits == 'J' species.transportData = TransportData( sigma = Quantity(float(sigmaLJ), sigmaLJunits), epsilon = Quantity(float(epsilonLJ) / constants.kB, 'K'), ) # Read species vibrational frequencies freqCount, freqUnits = readMeaningfulLine(f).split() frequencies = [] for j in range(int(freqCount)): frequencies.append(float(readMeaningfulLine(f))) species.conformer.modes.append(HarmonicOscillator( frequencies = Quantity(frequencies, freqUnits), )) # Read species external rotors rotCount, rotUnits = readMeaningfulLine(f).split() if int(rotCount) > 0: raise NotImplementedError('Cannot handle external rotational modes in FAME input.') # Read species internal rotors freqCount, freqUnits = readMeaningfulLine(f).split() frequencies = [] for j in range(int(freqCount)): frequencies.append(float(readMeaningfulLine(f))) barrCount, barrUnits = readMeaningfulLine(f).split() barriers = [] for j in range(int(barrCount)): barriers.append(float(readMeaningfulLine(f))) if barrUnits == 'cm^-1': barrUnits = 'J/mol' barriers = [barr * constants.h * constants.c * constants.Na * 100. for barr in barriers] elif barrUnits in ['Hz', 's^-1']: barrUnits = 'J/mol' barriers = [barr * constants.h * constants.Na for barr in barriers] elif barrUnits != 'J/mol': raise Exception('Unexpected units "{0}" for hindered rotor barrier height.'.format(barrUnits)) inertia = [V0 / 2.0 / (nu * constants.c * 100.)**2 / constants.Na for nu, V0 in zip(frequencies, barriers)] for I, V0 in zip(inertia, barriers): species.conformer.modes.append(HinderedRotor( inertia = Quantity(I,"kg*m^2"), barrier = Quantity(V0,barrUnits), symmetry = 1, semiclassical = False, )) # Read overall symmetry number species.conformer.spinMultiplicity = int(readMeaningfulLine(f)) # Read isomer, reactant channel, and product channel data Nisom = int(readMeaningfulLine(f)) Nreac = int(readMeaningfulLine(f)) Nprod = int(readMeaningfulLine(f)) for i in range(Nisom): data = readMeaningfulLine(f).split() assert data[0] == '1' job.network.isomers.append(speciesDict[data[1]]) for i in range(Nreac): data = readMeaningfulLine(f).split() assert data[0] == '2' job.network.reactants.append([speciesDict[data[1]], speciesDict[data[2]]]) for i in range(Nprod): data = readMeaningfulLine(f).split() if data[0] == '1': job.network.products.append([speciesDict[data[1]]]) elif data[0] == '2': job.network.products.append([speciesDict[data[1]], speciesDict[data[2]]]) # Read path reactions Nrxn = int(readMeaningfulLine(f)) for i in range(Nrxn): # Read and ignore reaction equation equation = readMeaningfulLine(f) reaction = Reaction(transitionState=TransitionState(), reversible=True) job.network.pathReactions.append(reaction) reaction.transitionState.conformer = Conformer() # Read reactant and product indices data = readMeaningfulLine(f).split() reac = int(data[0]) - 1 prod = int(data[1]) - 1 if reac < Nisom: reaction.reactants = [job.network.isomers[reac]] elif reac < Nisom+Nreac: reaction.reactants = job.network.reactants[reac-Nisom] else: reaction.reactants = job.network.products[reac-Nisom-Nreac] if prod < Nisom: reaction.products = [job.network.isomers[prod]] elif prod < Nisom+Nreac: reaction.products = job.network.reactants[prod-Nisom] else: reaction.products = job.network.products[prod-Nisom-Nreac] # Read reaction E0 E0units, E0 = readMeaningfulLine(f).split() reaction.transitionState.conformer.E0 = Quantity(float(E0), E0units) reaction.transitionState.conformer.E0.units = 'kJ/mol' # Read high-pressure limit kinetics data = readMeaningfulLine(f) assert data.lower() == 'arrhenius' Aunits, A = readMeaningfulLine(f).split() if '/' in Aunits: index = Aunits.find('/') Aunits = '{0}/({1})'.format(Aunits[0:index], Aunits[index+1:]) Eaunits, Ea = readMeaningfulLine(f).split() n = readMeaningfulLine(f) reaction.kinetics = Arrhenius( A = Quantity(float(A), Aunits), Ea = Quantity(float(Ea), Eaunits), n = Quantity(float(n)), ) reaction.kinetics.Ea.units = 'kJ/mol' f.close() job.network.isomers = [Configuration(isomer) for isomer in job.network.isomers] job.network.reactants = [Configuration(*reactants) for reactants in job.network.reactants] job.network.products = [Configuration(*products) for products in job.network.products] return job
def processThermoData(spc, thermo0, thermoClass=NASA): """ Converts via Wilhoit into required `thermoClass` and sets `E0`. Resulting thermo is returned. """ # TODO moving this as a global import leads to circular imports. from rmgpy.rmg.model import Species thermo = None # Always convert to Wilhoit so we can compute E0 if isinstance(thermo0, Wilhoit): wilhoit = thermo0 elif isinstance(thermo0, ThermoData): wilhoit = thermo0.toWilhoit(B=1000.) else: wilhoit = thermo0.toWilhoit() # Add on solvation correction if Species.solventData and not "Liquid thermo library" in thermo0.comment: solvationdatabase = getDB('solvation') #logging.info("Making solvent correction for {0}".format(Species.solventName)) soluteData = solvationdatabase.getSoluteData(spc) solvation_correction = solvationdatabase.getSolvationCorrection( soluteData, Species.solventData) # correction is added to the entropy and enthalpy wilhoit.S0.value_si = (wilhoit.S0.value_si + solvation_correction.entropy) wilhoit.H0.value_si = (wilhoit.H0.value_si + solvation_correction.enthalpy) # Compute E0 by extrapolation to 0 K if spc.conformer is None: spc.conformer = Conformer() spc.conformer.E0 = wilhoit.E0 # Convert to desired thermo class if thermoClass is Wilhoit: thermo = wilhoit elif thermoClass is NASA: if Species.solventData: #if liquid phase simulation keep the nasa polynomial if it comes from a liquid phase thermoLibrary. Otherwise convert wilhoit to NASA if "Liquid thermo library" in thermo0.comment and isinstance( thermo0, NASA): thermo = thermo0 if thermo.E0 is None: thermo.E0 = wilhoit.E0 else: thermo = wilhoit.toNASA(Tmin=100.0, Tmax=5000.0, Tint=1000.0) else: #gas phase with species matching thermo library keep the NASA from library or convert if group additivity if "Thermo library" in thermo0.comment and isinstance( thermo0, NASA): thermo = thermo0 if thermo.E0 is None: thermo.E0 = wilhoit.E0 else: thermo = wilhoit.toNASA(Tmin=100.0, Tmax=5000.0, Tint=1000.0) else: raise Exception( 'thermoClass neither NASA nor Wilhoit. Cannot process thermo data.' ) if thermo.__class__ != thermo0.__class__: # Compute RMS error of overall transformation Tlist = numpy.array( [300.0, 400.0, 500.0, 600.0, 800.0, 1000.0, 1500.0], numpy.float64) err = 0.0 for T in Tlist: err += (thermo.getHeatCapacity(T) - thermo0.getHeatCapacity(T))**2 err = math.sqrt(err / len(Tlist)) / constants.R # logging.log(logging.WARNING if err > 0.2 else 0, 'Average RMS error in heat capacity fit to {0} = {1:g}*R'.format(spc, err)) return thermo
def getStatmechData(self, molecule, thermoModel): """ Use the previously-loaded frequency database to generate a set of characteristic group frequencies corresponding to the speficied `molecule`. The provided thermo data in `thermoModel` is used to fit some frequencies and all hindered rotors to heat capacity data. """ conformer = Conformer() # Compute spin multiplicity # For closed-shell molecule the spin multiplicity is 1 # For monoradicals the spin multiplicity is 2 # For higher-order radicals the highest allowed spin multiplicity is assumed conformer.spinMultiplicity = molecule.getRadicalCount() + 1 # No need to determine rotational and vibrational modes for single atoms if len(molecule.atoms) < 2: return (conformer, None, None) linear = molecule.isLinear() numRotors = molecule.countInternalRotors() numVibrations = 3 * len(molecule.atoms) - (5 if linear else 6) - numRotors # Get characteristic frequency groups and the associated frequencies groupCount = self.getFrequencyGroups(molecule) frequencies = [] for entry, count in groupCount.iteritems(): if count != 0 and entry.data is not None: frequencies.extend(entry.data.generateFrequencies(count)) # Check that we have the right number of degrees of freedom specified if len(frequencies) > numVibrations: # We have too many vibrational modes difference = len(frequencies) - numVibrations # First try to remove hindered rotor modes until the proper number of modes remain if numRotors > difference: numRotors -= difference numVibrations = len(frequencies) logging.warning( 'For {0}, more characteristic frequencies were generated than vibrational modes allowed. Removed {1:d} internal rotors to compensate.' .format(molecule, difference)) # If that won't work, turn off functional groups until the problem is underspecified again else: groupsRemoved = 0 freqsRemoved = 0 freqCount = len(frequencies) while freqCount > numVibrations: minDegeneracy, minEntry = min([(entry.data.symmetry, entry) for entry in groupCount if groupCount[entry] > 0]) if groupCount[minEntry] > 1: groupCount[minEntry] -= 1 else: del groupCount[minEntry] groupsRemoved += 1 freqsRemoved += minDegeneracy freqCount -= minDegeneracy # Log warning logging.warning( 'For {0}, more characteristic frequencies were generated than vibrational modes allowed. Removed {1:d} groups ({2:d} frequencies) to compensate.' .format(molecule, groupsRemoved, freqsRemoved)) # Regenerate characteristic frequencies frequencies = [] for entry, count in groupCount.iteritems(): if count != 0: frequencies.extend( entry.data.generateFrequencies(count)) # Subtract out contributions to heat capacity from the group frequencies Tlist = numpy.arange(300.0, 1501.0, 100.0, numpy.float64) Cv = numpy.array( [thermoModel.getHeatCapacity(T) / constants.R for T in Tlist], numpy.float64) ho = HarmonicOscillator(frequencies=(frequencies, "cm^-1")) for i in range(Tlist.shape[0]): Cv[i] -= ho.getHeatCapacity(Tlist[i]) / constants.R # Subtract out translational modes Cv -= 1.5 # Subtract out external rotational modes Cv -= (1.5 if not linear else 1.0) # Subtract out PV term (Cp -> Cv) Cv -= 1.0 # Fit remaining frequencies and hindered rotors to the heat capacity data from statmechfit import fitStatmechToHeatCapacity modes = fitStatmechToHeatCapacity(Tlist, Cv, numVibrations - len(frequencies), numRotors, molecule) for mode in modes: if isinstance(mode, HarmonicOscillator): uncertainties = [0 for f in frequencies ] # probably shouldn't be zero frequencies.extend(mode.frequencies.value_si) uncertainties.extend(mode.frequencies.uncertainty) mode.frequencies.value_si = numpy.array( frequencies, numpy.float) mode.frequencies.uncertainty = numpy.array( uncertainties, numpy.float) break else: modes.insert( 0, HarmonicOscillator(frequencies=(frequencies, "cm^-1"))) conformer.modes = modes return (conformer, None, None)
def get_thermo(optfreq_log, optfreq_level, energy_level, energy_log=None, mol=None, bacs=None, soc=False, infer_symmetry=False, infer_chirality=False, unique_id='0', scr_dir='SCRATCH'): q = QChem(logfile=optfreq_log) symbols, coords = q.get_geometry() inertia = q.get_moments_of_inertia() freqs = q.get_frequencies() zpe = q.get_zpe() if energy_log is None: e0 = q.get_energy() multiplicity = q.get_multiplicity() else: m = Molpro(logfile=energy_log) e0 = m.get_energy() multiplicity = m.get_multiplicity() # Infer connections only if not given explicitly if mol is None: mol = geo_to_rmg_mol((symbols, coords)) # Does not contain bond orders # Try to infer point group to calculate symmetry number and chirality symmetry = optical_isomers = 1 point_group = None if infer_symmetry or infer_chirality: qmdata = QMData( groundStateDegeneracy=multiplicity, # Only needed to check if valid QMData numberOfAtoms=len(symbols), atomicNumbers=[atomic_symbol_dict[sym] for sym in symbols], atomCoords=(coords, 'angstrom'), energy=(e0 * 627.5095, 'kcal/mol') # Only needed to avoid error ) settings = type("", (), dict(symmetryPath='symmetry', scratchDirectory=scr_dir))() # Creates anonymous class pgc = PointGroupCalculator(settings, unique_id, qmdata) point_group = pgc.calculate() if point_group is not None: if infer_symmetry: symmetry = point_group.symmetryNumber if infer_chirality and point_group.chiral: optical_isomers = 2 # Translational mode mass = mol.getMolecularWeight() translation = IdealGasTranslation(mass=(mass, 'kg/mol')) # Rotational mode if isinstance(inertia, list): # Nonlinear rotation = NonlinearRotor(inertia=(inertia, 'amu*angstrom^2'), symmetry=symmetry) else: rotation = LinearRotor(inertia=(inertia, 'amu*angstrom^2'), symmetry=symmetry) # Vibrational mode freq_scale_factor = freq_scale_factors.get(optfreq_level, 1.0) freqs = [f * freq_scale_factor for f in freqs] vibration = HarmonicOscillator(frequencies=(freqs, 'cm^-1')) # Bring energy to gas phase reference state e0 *= constants.E_h * constants.Na zpe *= constants.E_h * constants.Na * freq_scale_factor for sym in symbols: if soc: e0 -= (atom_energies[energy_level][sym] - atom_socs[sym]) * constants.E_h * constants.Na else: e0 -= atom_energies[energy_level][sym] * constants.E_h * constants.Na e0 += (h0expt[sym] - h298corr[sym]) * 4184.0 if bacs is not None: e0 -= get_bac_correction(mol, **bacs) * 4184.0 # Group modes into Conformer object modes = [translation, rotation, vibration] conformer = Conformer(modes=modes, spinMultiplicity=multiplicity, opticalIsomers=optical_isomers) # Calculate heat of formation, entropy of formation, and heat capacities conformer.E0 = (e0 + zpe, 'J/mol') hf298 = conformer.getEnthalpy(298.0) + conformer.E0.value_si s298 = conformer.getEntropy(298.0) Tlist = [300.0, 400.0, 500.0, 600.0, 800.0, 1000.0, 1500.0] cp = np.zeros(len(Tlist)) for i, T in enumerate(Tlist): cp[i] = conformer.getHeatCapacity(T) # Return in kcal/mol and cal/mol/K return hf298/4184.0, s298/4.184, cp/4.184