diff --git a/arkane/commonTest.py b/arkane/commonTest.py index 42efbab977..65536b50de 100644 --- a/arkane/commonTest.py +++ b/arkane/commonTest.py @@ -257,12 +257,12 @@ def testSpeciesThermo(self): """Test thermo job execution for species from separate input file.""" input.thermo('C2H4', 'NASA') job = jobList[-1] - filepath = os.path.join(self.directory, 'reactions', 'H+C2H4=C2H5', 'output.py') - job.execute(outputFile=filepath) - self.assertTrue(os.path.isfile(os.path.join(os.path.dirname(filepath), 'output.py'))) - self.assertTrue(os.path.isfile(os.path.join(os.path.dirname(filepath), 'chem.inp'))) - os.remove(os.path.join(os.path.dirname(filepath), 'output.py')) - os.remove(os.path.join(os.path.dirname(filepath), 'chem.inp')) + filepath = os.path.join(self.directory, 'reactions', 'H+C2H4=C2H5') + job.execute(output_directory=filepath) + self.assertTrue(os.path.isfile(os.path.join(filepath, 'output.py'))) + self.assertTrue(os.path.isfile(os.path.join(filepath, 'chem.inp'))) + os.remove(os.path.join(filepath, 'output.py')) + os.remove(os.path.join(filepath, 'chem.inp')) def testTransitionState(self): """Test loading of transition state input file.""" @@ -332,8 +332,8 @@ def test_dump_yaml(self): """ jobList = self.arkane.loadInputFile(self.dump_input_path) for job in jobList: - job.execute(outputFile=self.dump_output_file) - self.assertTrue(os.path.isfile(self.dump_yaml_file)) + job.execute(output_directory=self.dump_path) + self.assertTrue(os.path.isfile(self.dump_output_file)) def test_create_and_load_yaml(self): """ @@ -342,7 +342,7 @@ def test_create_and_load_yaml(self): # Create YAML file by running Arkane jobList = self.arkane.loadInputFile(self.dump_input_path) for job in jobList: - job.execute(outputFile=self.dump_output_file) + job.execute(output_directory=self.dump_path) # Load in newly created YAML file arkane_spc_old = jobList[0].arkane_species diff --git a/arkane/encorr/corr.py b/arkane/encorr/corr.py index 14c199f1ac..29c563e220 100644 --- a/arkane/encorr/corr.py +++ b/arkane/encorr/corr.py @@ -34,6 +34,7 @@ """ import rmgpy.constants as constants +import logging from arkane.exceptions import AtomEnergyCorrectionError, BondAdditivityCorrectionError @@ -66,6 +67,8 @@ def get_energy_correction(model_chemistry, atoms, bonds, coords, nums, multiplic Returns: The correction to the electronic energy in J/mol. """ + logging.warning('get_energy_correction has be deprecated, use get_atom_correction ' + 'and get_bac instead') model_chemistry = model_chemistry.lower() corr = 0.0 @@ -83,17 +86,20 @@ def get_atom_correction(model_chemistry, atoms, atom_energies=None): quantum chemistry calculation at a given model chemistry such that it is consistent with the normal gas-phase reference states. - `atoms` is a dictionary associating element symbols with the number - of that element in the molecule. The atom energies are in Hartrees, - which are from single atom calculations using corresponding model - chemistries. + Args: + model_chemistry: The model chemistry, typically specified as method/basis. + atoms: A dictionary of element symbols with their associated counts. + atom_energies: A dictionary of element symbols with their associated atomic energies in Hartree. + + Returns: + The atom correction to the electronic energy in J/mol. The assumption for the multiplicity of each atom is: H doublet, C triplet, N quartet, O triplet, F doublet, Si triplet, P quartet, S triplet, Cl doublet, Br doublet, I doublet. """ corr = 0.0 - + model_chemistry = model_chemistry.lower() # Step 1: Reference all energies to a model chemistry-independent # basis by subtracting out that model chemistry's atomic energies if atom_energies is None: @@ -130,8 +136,28 @@ def get_atom_correction(model_chemistry, atoms, atom_energies=None): def get_bac(model_chemistry, bonds, coords, nums, bac_type='p', multiplicity=1): """ - Calculate bond additivity correction. + Returns the bond additivity correction in J/mol. + + There are two bond additivity corrections currently supported. Peterson-type + corrections can be specified by setting `bac_type` to 'p'. This will use the + `bonds` attribute, which is a dictionary associating bond types with the number + of that bond in the molecule. + + The Melius-type BAC is specified with 'm' and utilizes the atom xyz coordinates + in `coords` and array of atomic numbers of atoms as well as the structure's multiplicity. + + Args: + model_chemistry: The model chemistry, typically specified as method/basis. + bonds: A dictionary of bond types (e.g., 'C=O') with their associated counts. + coords: A Numpy array of Cartesian molecular coordinates. + nums: A sequence of atomic numbers. + multiplicity: The spin multiplicity of the molecule. + bac_type: The type of bond additivity correction to use. + + Returns: + The bond correction to the electronic energy in J/mol. """ + model_chemistry = model_chemistry.lower() if bac_type.lower() == 'p': # Petersson-type BACs return pbac.get_bac(model_chemistry, bonds) elif bac_type.lower() == 'm': # Melius-type BACs diff --git a/arkane/gaussian.py b/arkane/gaussian.py index feb0ea5d00..7f81cc0520 100644 --- a/arkane/gaussian.py +++ b/arkane/gaussian.py @@ -169,7 +169,7 @@ def loadConformer(self, symmetry=None, spinMultiplicity=0, opticalIsomers=None, unscaled_frequencies = [] e0 = 0.0 if opticalIsomers is None or symmetry is None: - _opticalIsomers, _symmetry = self.get_optical_isomers_and_symmetry_number() + _opticalIsomers, _symmetry, _ = self.get_symmetry_properties() if opticalIsomers is None: opticalIsomers = _opticalIsomers if symmetry is None: @@ -384,6 +384,68 @@ def loadScanEnergies(self): return Vlist, angle + def _load_scan_specs(self, letter_spec): + """ + This method reads the ouptput file for optional parameters + sent to gaussian, and returns the list of optional parameters + as a list of tuples. + + `letter_spec` is a character used to identify whether a specification + defines pivot atoms ('S'), frozen atoms ('F') or other attributes. + + More information about the syntax can be found http://gaussian.com/opt/ + """ + output = [] + reached_input_spec_section = False + with open(self.path, 'r') as f: + line = f.readline() + while line != '': + if reached_input_spec_section: + terms = line.split() + if len(terms) == 0: + # finished reading specs + break + if terms[0] == 'D': + action_index = 5 # dihedral angle with four terms + elif terms[0] == 'A': + action_index = 4 # valance angle with three terms + elif terms[0] == 'B': + action_index = 3 # bond length with 2 terms + else: + raise ValueError('This file has an option not supported by arkane.' + 'Unable to read scan specs for line: {}'.format(line)) + if len(terms) > action_index: + # specified type explicitly + if terms[action_index] == letter_spec: + output.append(terms[1:action_index]) + else: + # no specific specification, assume freezing + if letter_spec == 'F': + output.append(terms[1:action_index]) + if " The following ModRedundant input section has been read:" in line: + reached_input_spec_section = True + line = f.readline() + return output + + def load_scan_pivot_atoms(self): + """ + Extract the atom numbers which the rotor scan pivots around + Return a list of atom numbers starting with the first atom as 1 + """ + output = self._load_scan_specs('S') + return output[0] if len(output) > 0 else [] + + def load_scan_frozen_atoms(self): + """ + Extract the atom numbers which were frozen during the scan + Return a list of list of atom numbers starting with the first atom as 1 + Each element of the outer lists represents a frozen bond + Inner lists with length 2 represent frozen bond lengths + Inner lists with length 3 represent frozen bond angles + Inner lists with length 4 represent frozen dihedral angles + """ + return self._load_scan_specs('F') + def loadNegativeFrequency(self): """ Return the negative frequency from a transition state frequency @@ -403,5 +465,6 @@ def loadNegativeFrequency(self): frequencies.sort() frequency = [freq for freq in frequencies if freq < 0][0] if frequency is None: - raise Exception('Unable to find imaginary frequency in Gaussian output file {0}'.format(self.path)) + raise Exception('Unable to find imaginary frequency of {1} ' + 'in Gaussian output file {0}'.format(self.path, self.species.label)) return frequency diff --git a/arkane/gaussianTest.py b/arkane/gaussianTest.py index 2939fc3a6c..8e71f96e8d 100644 --- a/arkane/gaussianTest.py +++ b/arkane/gaussianTest.py @@ -140,7 +140,7 @@ def testLoadSymmetryAndOptics(self): """ log = GaussianLog(os.path.join(os.path.dirname(__file__), 'data', 'oxygen.log')) - optical, symmetry = log.get_optical_isomers_and_symmetry_number() + optical, symmetry, _ = log.get_symmetry_properties() self.assertEqual(optical, 1) self.assertEqual(symmetry, 2) diff --git a/arkane/kinetics.py b/arkane/kinetics.py index 5fe94fc196..57adfab277 100644 --- a/arkane/kinetics.py +++ b/arkane/kinetics.py @@ -137,22 +137,42 @@ def Tlist(self): def Tlist(self, value): self._Tlist = quantity.Temperature(value) - def execute(self, outputFile=None, plot=True): + def execute(self, output_directory=None, plot=True): """ - Execute the kinetics job, saving the results to the given `outputFile` on disk. + Execute the kinetics job, saving the results within + the `output_directory`. + + If `plot` is True, then plots of the raw and fitted values for the kinetics + will be saved. """ if self.Tlist is not None: self.generateKinetics(self.Tlist.value_si) else: self.generateKinetics() - if outputFile is not None: - self.save(outputFile) + if output_directory is not None: + try: + self.write_output(output_directory) + except Exception as e: + logging.warning("Could not write kinetics output file due to error: " + "{0} in reaction {1}".format(e, self.reaction.label)) + try: + self.write_chemkin(output_directory) + except Exception as e: + logging.warning("Could not write kinetics chemkin output due to error: " + "{0} in reaction {1}".format(e, self.reaction.label)) if plot: - self.plot(os.path.dirname(outputFile)) - self.draw(os.path.dirname(outputFile)) + try: + self.plot(output_directory) + except Exception as e: + logging.warning("Could not plot kinetics due to error: " + "{0} in reaction {1}".format(e, self.reaction.label)) + try: + self.draw(output_directory) + except: + logging.warning("Could not draw reaction {1} due to error: {0}".format(e, self.reaction.label)) if self.sensitivity_conditions is not None: logging.info('\n\nRunning sensitivity analysis...') - sa(self, os.path.dirname(outputFile)) + sa(self, output_directory) logging.debug('Finished kinetics job for reaction {0}.'.format(self.reaction)) logging.debug(repr(self.reaction)) @@ -198,10 +218,10 @@ def generateKinetics(self, Tlist=None): self.reaction.kinetics = Arrhenius().fitToData(Tlist, klist, kunits=self.kunits) self.reaction.elementary_high_p = True - def save(self, outputFile): + def write_output(self, output_directory): """ - Save the results of the kinetics job to the file located - at `path` on disk. + Save the results of the kinetics job to the `output.py` file located + in `output_directory`. """ reaction = self.reaction @@ -213,9 +233,10 @@ def save(self, outputFile): logging.info('Saving kinetics for {0}...'.format(reaction)) order = len(self.reaction.reactants) + factor = 1e6 ** (order - 1) - f = open(outputFile, 'a') + f = open(os.path.join(output_directory, 'output.py'), 'a') if self.usedTST: # If TST is not used, eg. it was given in 'reaction', then this will throw an error. @@ -281,19 +302,23 @@ def save(self, outputFile): f.write('# krev (TST) = {0} \n'.format(kinetics0rev)) f.write('# krev (TST+T) = {0} \n\n'.format(kineticsrev)) - # Reaction path degeneracy is INCLUDED in the kinetics itself! rxn_str = 'kinetics(label={0!r}, kinetics={1!r})'.format(reaction.label, reaction.kinetics) f.write('{0}\n\n'.format(prettify(rxn_str))) f.close() - # Also save the result to chem.inp - f = open(os.path.join(os.path.dirname(outputFile), 'chem.inp'), 'a') + def write_chemkin(self, output_directory): + """ + Appends the kinetics rates to `chem.inp` in `outut_directory` + """ + + # obtain a unit conversion factor + order = len(self.reaction.reactants) + factor = 1e6 ** (order - 1) reaction = self.reaction kinetics = reaction.kinetics - rxn_str = '' if reaction.kinetics.comment: for line in reaction.kinetics.comment.split("\n"): @@ -305,33 +330,33 @@ def save(self, outputFile): kinetics.Ea.value_si / 4184., ) - f.write('{0}\n'.format(rxn_str)) - - f.close() + with open(os.path.join(output_directory, 'chem.inp'), 'a') as f: + f.write('{0}\n'.format(rxn_str)) - # We're saving a YAML file for TSs iff structures of the respective reactant/s and product/s are known - if all([spc.molecule is not None and len(spc.molecule) - for spc in self.reaction.reactants + self.reaction.products]): + def save_yaml(self, output_directory): + """ + Save a YAML file for TSs if structures of the respective reactant/s and product/s are known + """ + if all ([spc.molecule is not None and len(spc.molecule) + for spc in self.reaction.reactants + self.reaction.products]): self.arkane_species.update_species_attributes(self.reaction.transitionState) - self.arkane_species.reaction_label = reaction.label + self.arkane_species.reaction_label = self.reaction.label self.arkane_species.reactants = [{'label': spc.label, 'adjacency_list': spc.molecule[0].toAdjacencyList()} for spc in self.reaction.reactants] self.arkane_species.products = [{'label': spc.label, 'adjacency_list': spc.molecule[0].toAdjacencyList()} - for spc in self.reaction.products] - self.arkane_species.save_yaml(path=os.path.dirname(outputFile)) + for spc in self.reaction.products] + self.arkane_species.save_yaml(path=output_directory) - def plot(self, outputDirectory): + def plot(self, output_directory): """ Plot both the raw kinetics data and the Arrhenius fit versus temperature. The plot is saved to the file ``kinetics.pdf`` in the output directory. The plot is not generated if ``matplotlib`` is not installed. """ - # Skip this step if matplotlib is not installed - try: - import matplotlib.pyplot as plt - except ImportError: - return + import matplotlib.pyplot as plt + + f, ax = plt.subplots() if self.Tlist is not None: t_list = [t for t in self.Tlist.value_si] else: @@ -356,7 +381,7 @@ def plot(self, outputDirectory): plt.xlabel('1000 / Temperature (K^-1)') plt.ylabel('Rate coefficient ({0})'.format(self.kunits)) - plot_path = os.path.join(outputDirectory, 'plots') + plot_path = os.path.join(output_directory, 'plots') if not os.path.exists(plot_path): os.mkdir(plot_path) @@ -365,7 +390,7 @@ def plot(self, outputDirectory): plt.savefig(os.path.join(plot_path, filename)) plt.close() - def draw(self, outputDirectory, format='pdf'): + def draw(self, output_directory, format='pdf'): """ Generate a PDF drawing of the reaction. This requires that Cairo and its Python wrapper be available; if not, @@ -375,7 +400,7 @@ def draw(self, outputDirectory, format='pdf'): one of the following: `pdf`, `svg`, `png`. """ - drawing_path = os.path.join(outputDirectory, 'paths') + drawing_path = os.path.join(output_directory, 'paths') if not os.path.exists(drawing_path): os.mkdir(drawing_path) diff --git a/arkane/log.py b/arkane/log.py index e6cc855572..5d6c3d9552 100644 --- a/arkane/log.py +++ b/arkane/log.py @@ -110,6 +110,25 @@ def loadScanEnergies(self): raise NotImplementedError("loadScanEnergies is not implemented for the Log class. " "This method should be implemented by a subclass.") + def load_scan_pivot_atoms(self): + """ + Extract the atom numbers which the rotor scan pivots around + Return a list of atom numbers starting with the first atom as 1 + """ + raise NotImplementedError("load_scan_pivot_atoms is not implemented for the Log class") + + def load_scan_frozen_atoms(self): + """ + Extract the atom numbers which were frozen during the scan + Return a list of list of atom numbers starting with the first atom as 1 + Each element of the outer lists represents a frozen bond + Inner lists with length 2 represent frozen bond lengths + Inner lists with length 3 represent frozen bond angles + Inner lists with length 4 represent frozen dihedral angles + """ + raise NotImplementedError("load_scan_frozen_atoms is not implemented for the Log class") + + def loadNegativeFrequency(self): """ Return the imaginary frequency from a transition state frequency @@ -118,11 +137,12 @@ def loadNegativeFrequency(self): raise NotImplementedError("loadNegativeFrequency is not implemented for the Log class. " "This method should be implemented by a subclass.") - def get_optical_isomers_and_symmetry_number(self): + def get_symmetry_properties(self): """ This method uses the symmetry package from RMG's QM module and returns a tuple where the first element is the number - of optical isomers and the second element is the symmetry number. + of optical isomers, the second element is the symmetry number, + and the third element is the point group identified. """ coordinates, atom_numbers, _ = self.loadGeometry() unique_id = '0' # Just some name that the SYMMETRY code gives to one of its jobs @@ -152,6 +172,18 @@ def get_optical_isomers_and_symmetry_number(self): else: logging.error('Symmetry algorithm errored when computing point group\nfor log file located at{0}.\n' 'Manually provide values in Arkane input.'.format(self.path)) - return optical_isomers, symmetry + return optical_isomers, symmetry, pg.pointGroup finally: shutil.rmtree(scr_dir) + + def get_D1_diagnostic(self): + """ + This method returns the D1 diagnostic for certain quantum jobs + """ + raise NotImplementedError("get_D1_diagnostic is not implemented for all Log subclasses.") + + def get_T1_diagnostic(self): + """ + This method returns the T1 diagnostic for certain quantum jobs + """ + raise NotImplementedError("get_T1_diagnostic is not implemented for all Log subclasses.") diff --git a/arkane/main.py b/arkane/main.py index 350e135583..31c8096503 100644 --- a/arkane/main.py +++ b/arkane/main.py @@ -39,6 +39,7 @@ import argparse import time import csv +import numpy as np try: import matplotlib @@ -125,7 +126,8 @@ def parseCommandLineArguments(self): metavar='DIR', help='use DIR as output directory') # Add options for controlling generation of plots - parser.add_argument('-p', '--plot', action='store_true', default=True, help='generate plots of results') + parser.add_argument('-p', '--no-plot', action='store_false', default=True, + help='prevent generating plots', dest='plot') args = parser.parse_args() @@ -266,42 +268,66 @@ def execute(self): # run thermo and statmech jobs (also writes thermo blocks to Chemkin file) supporting_info = [] + hindered_rotor_info = [] for job in self.jobList: if isinstance(job, ThermoJob): - job.execute(outputFile=outputFile, plot=self.plot) + job.execute(output_directory=self.outputDirectory, plot=self.plot) if isinstance(job, StatMechJob): - job.execute(outputFile=outputFile, plot=self.plot, pdep=is_pdep(self.jobList)) - supporting_info.append(job.supporting_info) + job.execute(output_directory=self.outputDirectory, plot=self.plot, pdep=is_pdep(self.jobList)) + if hasattr(job, 'supporting_info'): + supporting_info.append(job.supporting_info) + if hasattr(job, 'raw_hindered_rotor_data'): + for hr_info in job.raw_hindered_rotor_data: + hindered_rotor_info.append(hr_info) with open(chemkinFile, 'a') as f: f.write('\n') f.write('END\n\n\n\n') f.write('REACTIONS KCAL/MOLE MOLES\n\n') - supporting_info_file = os.path.join(self.outputDirectory, 'supporting_information.csv') - with open(supporting_info_file, 'wb') as csvfile: - writer = csv.writer(csvfile, delimiter=',', quotechar='"', quoting=csv.QUOTE_MINIMAL) - writer.writerow(['Label', 'Rotational constant (cm-1)', 'Unscaled frequencies (cm-1)']) - for row in supporting_info: - label = row[0] - rot = '-' - freq = '-' - if len(row) > 1: # monoatomic species have no frequencies nor rotational constants - if isinstance(row[1].rotationalConstant.value, float): + if supporting_info: + # write supporting_info.csv for statmech jobs + supporting_info_file = os.path.join(self.outputDirectory, 'supporting_information.csv') + with open(supporting_info_file, 'wb') as csvfile: + writer = csv.writer(csvfile, delimiter=',', quotechar='"', quoting=csv.QUOTE_MINIMAL) + writer.writerow(['Label','Symmetry Number','Number of optical isomers','Symmetry Group', + 'Rotational constant (cm-1)','Calculated Frequencies (unscaled and prior to projection, cm^-1)', + 'Electronic energy (J/mol)','E0 (electronic energy + ZPE, J/mol)', + 'E0 with atom and bond corrections (J/mol)','Atom XYZ coordinates (angstrom)', + 'T1 diagnostic', 'D1 diagnostic']) + for row in supporting_info: + label = row[0] + rot = '-' + freq = '-' + if row[4] is not None and isinstance(row[4].rotationalConstant.value, float): # diatomic species have a single rotational constant - rot = '{0:.2f}'.format(row[1].rotationalConstant.value) - else: - rot = ', '.join(['{0:.2f}'.format(s) for s in row[1].rotationalConstant.value]) - freq = '' - if len(row) == 4: - freq = '{0:.1f}'.format(abs(row[3])) + 'i, ' - freq += ', '.join(['{0:.1f}'.format(s) for s in row[2]]) - writer.writerow([label, rot, freq]) - + rot = '{0:.2f}'.format(row[4].rotationalConstant.value) + elif row[4] is not None: + rot = ', '.join(['{0:.2f}'.format(s) for s in row[4].rotationalConstant.value]) + if row[5] is not None: + freq = '' + if row[6] is not None: #there is a negative frequency + freq = '{0:.1f}'.format(abs(row[6])) + 'i, ' + freq += ', '.join(['{0:.1f}'.format(s) for s in row[5]]) + atoms = ', '.join(["{0} {1}".format(atom," ".join([str(c) for c in coords])) for atom, coords in zip(row[10], row[11])]) + writer.writerow([label, row[1], row[2], row[3], rot, freq, row[7], row[8], row[9], atoms, row[12], + row[13]]) + if hindered_rotor_info: + hr_file = os.path.join(self.outputDirectory, 'hindered_rotor_scan_data.csv') + # find longest length to set column number for energies + max_energy_length = max([len(hr[4]) for hr in hindered_rotor_info]) + with open(hr_file, 'wb') as csvfile: + writer = csv.writer(csvfile, delimiter=',', quotechar='"', quoting=csv.QUOTE_MINIMAL) + writer.writerow(['species', 'rotor_number', 'symmetry', 'resolution (degrees)', + 'pivot_atoms', 'frozen_atoms'] + + ['energy (J/mol) {}'.format(i) for i in range(max_energy_length)]) + for row in hindered_rotor_info: + writer.writerow([row[0], row[1], row[2], row[3][1] * 180 / np.pi, + row[5], row[6]] + [a for a in row[4]]) # run kinetics and pdep jobs (also writes reaction blocks to Chemkin file) for job in self.jobList: if isinstance(job, KineticsJob): - job.execute(outputFile=outputFile, plot=self.plot) + job.execute(output_directory=self.outputDirectory, plot=self.plot) elif isinstance(job, PressureDependenceJob) and not any([isinstance(job, ExplorerJob) for job in self.jobList]): # if there is an explorer job the pdep job will be run in the explorer job diff --git a/arkane/molpro.py b/arkane/molpro.py index a1ce0687c1..309f05dd53 100644 --- a/arkane/molpro.py +++ b/arkane/molpro.py @@ -174,7 +174,7 @@ def loadConformer(self, symmetry=None, spinMultiplicity=0, opticalIsomers=None, unscaled_frequencies = [] e0 = 0.0 if opticalIsomers is None or symmetry is None: - _opticalIsomers, _symmetry = self.get_optical_isomers_and_symmetry_number() + _opticalIsomers, _symmetry, _ = self.get_symmetry_properties() if opticalIsomers is None: opticalIsomers = _opticalIsomers if symmetry is None: @@ -395,3 +395,31 @@ def loadScanEnergies(self): Rotor scans are not implemented in Molpro """ raise NotImplementedError('Rotor scans not implemented in Molpro') + + def get_T1_diagnostic(self): + """ + Returns the T1 diagnostic from output log. + If multiple occurrences exist, returns the last occurence + """ + with open(self.path) as f: + log = f.readlines() + + for line in reversed(log): + if 'T1 diagnostic: ' in line: + items = line.split() + return float(items[-1]) + raise ValueError('Unable to find T1 diagnostic in energy file: {}'.format(self.path)) + + def get_D1_diagnostic(self): + """ + Returns the D1 diagnostic from output log. + If multiple occurrences exist, returns the last occurence + """ + with open(self.path) as f: + log = f.readlines() + + for line in reversed(log): + if 'D1 diagnostic: ' in line: + items = line.split() + return float(items[-1]) + raise ValueError('Unable to find D1 diagnostic in energy file: {}'.format(self.path)) diff --git a/arkane/molproTest.py b/arkane/molproTest.py index 49cd64edcc..ea9aec9195 100644 --- a/arkane/molproTest.py +++ b/arkane/molproTest.py @@ -134,6 +134,21 @@ def test_load_negative_frequency(self): imaginary_freq = freq_log.loadNegativeFrequency() self.assertEqual(imaginary_freq, -1997.98) + def test_get_D1_diagnostic(self): + """ + Ensure molpro can retrieve the T1 diagnostic from CCSD calculations + """ + log=MolproLog(os.path.join(os.path.dirname(__file__),'data','ethylene_f12_dz.out')) + d1_diagnostic = log.get_D1_diagnostic() + self.assertAlmostEqual(d1_diagnostic, 0.03369031) + + def test_get_T1_diagnostic(self): + """ + Ensure molpro can retrieve the T1 diagnostic from CCSD calculations + """ + log=MolproLog(os.path.join(os.path.dirname(__file__),'data','ethylene_f12_dz.out')) + t1_diagnostic = log.get_T1_diagnostic() + self.assertAlmostEqual(t1_diagnostic, 0.01152184) if __name__ == '__main__': unittest.main(testRunner=unittest.TextTestRunner(verbosity=2)) diff --git a/arkane/qchem.py b/arkane/qchem.py index e4ca23c536..5490b379c9 100644 --- a/arkane/qchem.py +++ b/arkane/qchem.py @@ -183,7 +183,7 @@ def loadConformer(self, symmetry=None, spinMultiplicity=0, opticalIsomers=None, unscaled_frequencies = [] e0 = 0.0 if opticalIsomers is None or symmetry is None: - _opticalIsomers, _symmetry = self.get_optical_isomers_and_symmetry_number() + _opticalIsomers, _symmetry, _ = self.get_symmetry_properties() if opticalIsomers is None: opticalIsomers = _opticalIsomers if symmetry is None: diff --git a/arkane/statmech.py b/arkane/statmech.py index dc9c840f4f..6440de4193 100644 --- a/arkane/statmech.py +++ b/arkane/statmech.py @@ -59,7 +59,7 @@ from arkane.qchem import QChemLog from arkane.common import symbol_by_number from arkane.common import ArkaneSpecies -from arkane.encorr.corr import get_energy_correction +from arkane.encorr.corr import get_atom_correction, get_bac ################################################################################ @@ -183,22 +183,37 @@ def __init__(self, species, path): self.applyBondEnergyCorrections = True self.bondEnergyCorrectionType = 'p' self.atomEnergies = None - self.supporting_info = [self.species.label] self.bonds = None self.arkane_species = ArkaneSpecies(species=species) + self.hindered_rotor_plots = [] - def execute(self, outputFile=None, plot=False, pdep=False): + def execute(self, output_directory=None, plot=False, pdep=False): """ - Execute the statistical mechanics job, saving the results to the - given `outputFile` on disk. + Execute the statmech job, saving the results within + the `output_directory`. + + If `plot` is True, then plots of the hindered rotor fits will be saved. """ - self.load(pdep) - if outputFile is not None: - self.save(outputFile) + self.load(pdep, plot) + if output_directory is not None: + try: + self.write_output(output_directory) + except Exception as e: + logging.warning("Could not write statmech output file due to error: " + "{0} in species {1}".format(e, self.species.label)) + if plot: + hr_dir = os.path.join(output_directory, 'plots') + if not os.path.exists(hr_dir): + os.mkdir(hr_dir) + try: + self.save_hindered_rotor_figures(hr_dir) + except Exception as e: + logging.warning("Could not save hindered rotor scans due to error: " + "{0} in species {1}".format(e, self.species.label)) logging.debug('Finished statmech job for species {0}.'.format(self.species)) logging.debug(repr(self.species)) - def load(self, pdep=False): + def load(self, pdep=False, plot=False): """ Load the statistical mechanics parameters for each conformer from the associated files on disk. Creates :class:`Conformer` objects for @@ -292,9 +307,9 @@ def load(self, pdep=False): '{1!r}.'.format(self.modelChemistry, path)) e0, e_electronic = None, None # E0 = e_electronic + ZPE energyLog = None - if isinstance(energy, Log) and not isinstance(energy, (GaussianLog, QChemLog, MolproLog)): + if isinstance(energy, Log) and type(energy).__name__ == 'Log': energyLog = determine_qm_software(os.path.join(directory, energy.path)) - elif isinstance(energy, (GaussianLog, QChemLog, MolproLog)): + elif isinstance(energy, Log) and type(energy).__name__ != 'Log': energyLog = energy energyLog.path = os.path.join(directory, energyLog.path) elif isinstance(energy, float): @@ -302,7 +317,7 @@ def load(self, pdep=False): elif isinstance(energy, tuple) and len(energy) == 2: # this is likely meant to be a quantity object with ZPE already accounted for energy = Quantity(energy) - e_0 = energy.value_si # in J/mol + e0 = energy.value_si # in J/mol elif isinstance(energy, tuple) and len(energy) == 3: if energy[2].lower() == 'e_electronic': energy = Quantity(energy[:2]) @@ -313,29 +328,31 @@ def load(self, pdep=False): else: raise InputError('The third argument for E0 energy value should be e_elect (for energy w/o ZPE) ' 'or E0 (including the ZPE). Got: {0}'.format(energy[2])) - try: - geomLog = local_context['geometry'] - except KeyError: - raise InputError('Required attribute "geometry" not found in species file {0!r}.'.format(path)) - if isinstance(geomLog, Log) and not isinstance(energy, (GaussianLog, QChemLog, MolproLog)): - geomLog = determine_qm_software(os.path.join(directory, geomLog.path)) - else: - geomLog.path = os.path.join(directory, geomLog.path) try: statmechLog = local_context['frequencies'] except KeyError: raise InputError('Required attribute "frequencies" not found in species file {0!r}.'.format(path)) - if isinstance(statmechLog, Log) and not isinstance(energy, (GaussianLog, QChemLog, MolproLog)): + if isinstance(statmechLog, Log) and type(statmechLog).__name__ == 'Log': statmechLog = determine_qm_software(os.path.join(directory, statmechLog.path)) else: statmechLog.path = os.path.join(directory, statmechLog.path) + try: + geomLog = local_context['geometry'] + if isinstance(geomLog, Log) and type(geomLog).__name__ == 'Log': + geomLog = determine_qm_software(os.path.join(directory, geomLog.path)) + else: + geomLog.path = os.path.join(directory, geomLog.path) + except KeyError: + geomLog = statmechLog + logging.debug("Reading geometry from the specified frequencies file.") if 'frequencyScaleFactor' in local_context: logging.warning('Ignoring frequency scale factor in species file {0!r}.'.format(path)) rotors = [] if self.includeHinderedRotors: + self.raw_hindered_rotor_data = [] try: rotors = local_context['rotors'] except KeyError: @@ -367,17 +384,11 @@ def load(self, pdep=False): spinMultiplicity=spinMultiplicity, opticalIsomers=opticalIsomers, label=self.species.label) - translational_mode_exists = False + for mode in conformer.modes: - if isinstance(mode, (LinearRotor, NonlinearRotor)): - self.supporting_info.append(mode) - break if isinstance(mode, (Translation, IdealGasTranslation)): - translational_mode_exists = True - if unscaled_frequencies: - self.supporting_info.append(unscaled_frequencies) - - if not translational_mode_exists: + break + else: # Sometimes the translational mode is not appended to modes for monoatomic species conformer.modes.append(IdealGasTranslation(mass=self.species.molecularWeight)) @@ -416,19 +427,26 @@ def load(self, pdep=False): e_electronic = energyLog.loadEnergy(zpe_scale_factor) # in J/mol else: e_electronic *= constants.E_h * constants.Na # convert Hartree/particle into J/mol - if not self.applyAtomEnergyCorrections: + if self.applyAtomEnergyCorrections: + atom_corrections = get_atom_correction(self.modelChemistry, + atoms, self.atomEnergies) + + else: + atom_corrections = 0 logging.warning('Atom corrections are not being used. Do not trust energies and thermo.') - e_electronic += get_energy_correction( - self.modelChemistry, atoms, self.bonds, coordinates, number, - multiplicity=conformer.spinMultiplicity, atom_energies=self.atomEnergies, - apply_atom_corrections=self.applyAtomEnergyCorrections, apply_bac=self.applyBondEnergyCorrections, - bac_type=self.bondEnergyCorrectionType - ) + if self.applyBondEnergyCorrections: + if not self.bonds and hasattr(self.species, 'molecule') and self.species.molecule: + self.bonds = self.species.molecule[0].enumerate_bonds() + bond_corrections = get_bac(self.modelChemistry, self.bonds, coordinates, number, + bac_type=self.bondEnergyCorrectionType, multiplicity=conformer.spinMultiplicity) + else: + bond_corrections = 0 + e_electronic_with_corrections = e_electronic + atom_corrections + bond_corrections # Get ZPE only for polyatomic species (monoatomic species don't have frequencies, so ZPE = 0) zpe = statmechLog.loadZeroPointEnergy() * zpe_scale_factor if len(number) > 1 else 0 logging.debug('Scaled zero point energy (ZPE) is {0} J/mol'.format(zpe)) - e0 = e_electronic + zpe + e0 = e_electronic_with_corrections + zpe logging.debug(' Harmonic frequencies scaling factor used = {0:g}'.format(self.frequencyScaleFactor)) logging.debug(' Zero point energy scaling factor used = {0:g}'.format(zpe_scale_factor)) @@ -441,7 +459,6 @@ def load(self, pdep=False): if is_ts: neg_freq = statmechLog.loadNegativeFrequency() self.species.frequency = (neg_freq * self.frequencyScaleFactor, "cm^-1") - self.supporting_info.append(neg_freq) # Read and fit the 1D hindered rotors if applicable # If rotors are found, the vibrational frequencies are also @@ -470,28 +487,33 @@ def load(self, pdep=False): # the symmetry number will be derived from the scan scanLog, pivots, top, fit = q # Load the hindered rotor scan energies - if isinstance(scanLog, Log) and not isinstance(energy, (GaussianLog, QChemLog, MolproLog)): + if isinstance(scanLog, Log) and type(scanLog).__name__ == 'Log': scanLog = determine_qm_software(os.path.join(directory, scanLog.path)) - if isinstance(scanLog, GaussianLog): - scanLog.path = os.path.join(directory, scanLog.path) - v_list, angle = scanLog.loadScanEnergies() - scanLogOutput = ScanLog(os.path.join(directory, '{0}_rotor_{1}.txt'.format( - self.species.label, rotorCount + 1))) - scanLogOutput.save(angle, v_list) - elif isinstance(scanLog, QChemLog): - scanLog.path = os.path.join(directory, scanLog.path) + scanLog.path = os.path.join(directory, scanLog.path) + if isinstance(scanLog, (GaussianLog, QChemLog)): v_list, angle = scanLog.loadScanEnergies() - scanLogOutput = ScanLog(os.path.join(directory, '{0}_rotor_{1}.txt'.format( - self.species.label, rotorCount + 1))) - scanLogOutput.save(angle, v_list) + try: + pivot_atoms = scanLog.load_scan_pivot_atoms() + except Exception as e: + logging.warning("Unable to find pivot atoms in scan due to error: {}".format(e)) + pivot_atoms = 'N/A' + try: + frozen_atoms = scanLog.load_scan_frozen_atoms() + except Exception as e: + logging.warning("Unable to find pivot atoms in scan due to error: {}".format(e)) + frozen_atoms = 'N/A' elif isinstance(scanLog, ScanLog): - scanLog.path = os.path.join(directory, scanLog.path) angle, v_list = scanLog.load() + # no way to find pivot atoms or frozen atoms from ScanLog + pivot_atoms = 'N/A' + frozen_atoms = 'N/A' else: raise InputError('Invalid log file type {0} for scan log.'.format(scanLog.__class__)) if symmetry is None: symmetry = determine_rotor_symmetry(v_list, self.species.label, pivots) + self.raw_hindered_rotor_data.append((self.species.label, rotorCount, symmetry, angle, + v_list, pivot_atoms, frozen_atoms)) inertia = conformer.getInternalReducedMomentOfInertia(pivots, top) * constants.Na * 1e23 cosineRotor = HinderedRotor(inertia=(inertia, "amu*angstrom^2"), symmetry=symmetry) @@ -529,8 +551,11 @@ def load(self, pdep=False): rotor = cosineRotor conformer.modes.append(rotor) - - self.plotHinderedRotor(angle, v_list, cosineRotor, fourierRotor, rotor, rotorCount, directory) + if plot: + try: + self.create_hindered_rotor_figure(angle, v_list, cosineRotor, fourierRotor, rotor, rotorCount) + except Exception as e: + logging.warning("Could not plot hindered rotor graph due to error: {0}".format(e)) rotorCount += 1 @@ -558,14 +583,51 @@ def load(self, pdep=False): if isinstance(mode, HarmonicOscillator): mode.frequencies = (frequencies * self.frequencyScaleFactor, "cm^-1") + ##save supporting information for calculation + self.supporting_info = [self.species.label] + symmetry_read, optical_isomers_read, point_group_read = statmechLog.get_symmetry_properties() + self.supporting_info.append(externalSymmetry if externalSymmetry else symmetry_read) + self.supporting_info.append(opticalIsomers if opticalIsomers else optical_isomers_read) + self.supporting_info.append(point_group_read) + for mode in conformer.modes: + if isinstance(mode, (LinearRotor, NonlinearRotor)): + self.supporting_info.append(mode) + break + else: + self.supporting_info.append(None) + if unscaled_frequencies: + self.supporting_info.append(unscaled_frequencies) + else: + self.supporting_info.append(None) + if is_ts: + self.supporting_info.append(neg_freq) + else: + self.supporting_info.append(None) + self.supporting_info.append(e_electronic) + self.supporting_info.append(e_electronic + zpe) + self.supporting_info.append(e0) + self.supporting_info.append(list(map(lambda x: symbol_by_number[x],number))) #atom symbols + self.supporting_info.append(coordinates) + try: + t1d = energyLog.get_T1_diagnostic() + except (NotImplementedError, AttributeError): + t1d = None + self.supporting_info.append(t1d) + try: + d1d = energyLog.get_D1_diagnostic() + except (NotImplementedError, AttributeError): + d1d = None + self.supporting_info.append(d1d) + #save conformer self.species.conformer = conformer - def save(self, outputFile): + def write_output(self, output_directory): """ - Save the results of the statistical mechanics job to the file located - at `path` on disk. + Save the results of the statmech job to the `output.py` file located + in `output_directory`. """ + outputFile = os.path.join(output_directory, 'output.py') logging.info('Saving statistical mechanics parameters for {0}...'.format(self.species.label)) f = open(outputFile, 'a') @@ -595,17 +657,16 @@ def save(self, outputFile): f.write('{0}\n\n'.format(prettify(result))) f.close() - def plotHinderedRotor(self, angle, v_list, cosineRotor, fourierRotor, rotor, rotorIndex, directory): + def create_hindered_rotor_figure(self, angle, v_list, cosineRotor, fourierRotor, rotor, rotorIndex): """ Plot the potential for the rotor, along with its cosine and Fourier - series potential fits. The plot is saved to a set of files of the form - ``hindered_rotor_1.pdf``. + series potential fits, and save it in the `hindered_rotor_plots` attribute. """ try: import pylab - except ImportError: + except: + logging.warning("Unable to import pylab. not generating hindered rotor figures") return - phi = np.arange(0, 6.3, 0.02, np.float64) Vlist_cosine = np.zeros_like(phi) Vlist_fourier = np.zeros_like(phi) @@ -631,8 +692,16 @@ def plotHinderedRotor(self, angle, v_list, cosineRotor, fourierRotor, rotor, rot axes.set_xticklabels( ['$0$', '$\pi/4$', '$\pi/2$', '$3\pi/4$', '$\pi$', '$5\pi/4$', '$3\pi/2$', '$7\pi/4$', '$2\pi$']) - pylab.savefig(os.path.join(directory, '{0}_rotor_{1:d}.pdf'.format(self.species.label, rotorIndex + 1))) - pylab.close() + self.hindered_rotor_plots.append((fig,rotorIndex)) + + def save_hindered_rotor_figures(self, directory): + """ + Save hindered rotor plots as set of files of the form + ``rotor_[species_label]_0.pdf`` in the specified directory + """ + if hasattr(self, 'hindered_rotor_plots'): + for fig, rotor_index in self.hindered_rotor_plots: + fig.savefig(os.path.join(directory, 'rotor_{0}_{1:d}.pdf'.format(self.species.label, rotor_index))) ################################################################################ diff --git a/arkane/thermo.py b/arkane/thermo.py index e26f1654dc..48167e17cb 100644 --- a/arkane/thermo.py +++ b/arkane/thermo.py @@ -70,23 +70,40 @@ def __init__(self, species, thermoClass): self.thermoClass = thermoClass self.arkane_species = ArkaneSpecies(species=species) - def execute(self, outputFile=None, plot=False): + def execute(self, output_directory=None, plot=False): """ - Execute the thermodynamics job, saving the results to the - given `outputFile` on disk. + Execute the thermodynamics job, saving the results within + the `output_directory`. + + If `plot` is true, then plots of the raw and fitted values for heat + capacity, entropy, enthalpy, gibbs free energy, and hindered rotors + will be saved. """ self.generateThermo() - if outputFile is not None: - self.arkane_species.chemkin_thermo_string = self.save(outputFile) + if output_directory is not None: + try: + self.write_output(output_directory) + except Exception as e: + logging.warning("Could not write output file due to error: " + "{0} for species {1}".format(e, self.species.label)) + try: + self.arkane_species.chemkin_thermo_string = self.write_chemkin(output_directory) + except Exception as e: + logging.warning("Could not write chemkin output due to error: " + "{0} for species {1}".format(e, self.species.label)) if self.species.molecule is None or len(self.species.molecule) == 0: logging.debug("Not generating a YAML file for species {0}, since its structure wasn't" " specified".format(self.species.label)) else: # We're saving a YAML file for species iff Thermo is called and they're structure is known self.arkane_species.update_species_attributes(self.species) - self.arkane_species.save_yaml(path=os.path.dirname(outputFile)) + self.arkane_species.save_yaml(path=output_directory) if plot: - self.plot(os.path.dirname(outputFile)) + try: + self.plot(output_directory) + except Exception as e: + logging.warning("Could not create plots due to error: " + "{0} for species {1}".format(e, self.species.label)) def generateThermo(self): """ @@ -147,12 +164,13 @@ def generateThermo(self): else: species.thermo = wilhoit - def save(self, outputFile): + def write_output(self, output_directory): """ - Save the results of the thermodynamics job to the file located - at `path` on disk. + Save the results of the thermodynamics job to the `output.py` file located + in `output_directory`. """ species = self.species + outputFile = os.path.join(output_directory, 'output.py') logging.info('Saving thermo for {0}...'.format(species.label)) with open(outputFile, 'a') as f: @@ -179,8 +197,13 @@ def save(self, outputFile): thermo_string = 'thermo(label={0!r}, thermo={1!r})'.format(species.label, species.getThermoData()) f.write('{0}\n\n'.format(prettify(thermo_string))) - # write Chemkin file - with open(os.path.join(os.path.dirname(outputFile), 'chem.inp'), 'a') as f: + def write_chemkin(self, output_directory): + """ + Appends the thermo block to `chem.inp` and species name to + `species_dictionary.txt` within the `outut_directory` specified + """ + species = self.species + with open(os.path.join(output_directory, 'chem.inp'), 'a') as f: if isinstance(species, Species): if species.molecule and isinstance(species.molecule[0], Molecule): element_counts = retrieveElementCount(species.molecule[0]) @@ -197,7 +220,7 @@ def save(self, outputFile): # write species dictionary if isinstance(species, Species): if species.molecule and isinstance(species.molecule[0], Molecule): - spec_dict_path = os.path.join(os.path.dirname(outputFile), 'species_dictionary.txt') + spec_dict_path = os.path.join(output_directory, 'species_dictionary.txt') is_species_in_dict = False if os.path.isfile(spec_dict_path): with open(spec_dict_path, 'r') as f: diff --git a/documentation/source/users/arkane/input.rst b/documentation/source/users/arkane/input.rst index 784fde4a05..ce6f0a996f 100644 --- a/documentation/source/users/arkane/input.rst +++ b/documentation/source/users/arkane/input.rst @@ -163,8 +163,13 @@ For this option, the ``species()`` function only requires two parameters, as in species('C2H6', 'C2H6.py', structure = SMILES('CC')) -The first parameter (``'C2H6'`` above) is the species label, which can be referenced later in the input file. The second -parameter (``'C2H6.py'`` above) points to the location of another python file containing details of the species. This file +The first required parameter (``'C2H6'`` above) is the species label, which can be +referenced later in the input file and is used when constructing output files. +For chemkin output to run properly, limit names to alphanumeric characters +with approximately 13 characters or less. + +The second parameter (``'C2H6.py'`` above) points to the location of another +python file containing details of the species. This file will be referred to as the species input file. The third parameter (``'structure = SMILES('CC')'`` above) gives the species structure (either SMILES, adjacencyList, or InChI could be used). The structure parameter isn't necessary for the calculation, however if it is not specified a .yml file representing an ArkaneSpecies will not be @@ -177,13 +182,13 @@ Parameter Required? Description ======================= =========================== ==================================== ``bonds`` optional Type and number of bonds in the species ``linear`` optional ``True`` if the molecule is linear, ``False`` if not -``externalSymmetry`` yes The external symmetry number for rotation +``externalSymmetry`` optional The external symmetry number for rotation ``spinMultiplicity`` yes The ground-state spin multiplicity (degeneracy) -``opticalIsomers`` yes The number of optical isomers of the species +``opticalIsomers`` optional The number of optical isomers of the species ``energy`` yes The ground-state 0 K atomization energy in Hartree (without zero-point energy) **or** The path to the quantum chemistry output file containing the energy -``geometry`` yes The path to the quantum chemistry output file containing the optimized geometry +``geometry`` optional The path to the quantum chemistry output file containing the optimized geometry ``frequencies`` yes The path to the quantum chemistry output file containing the computed frequencies ``rotors`` optional A list of :class:`HinderedRotor()` and/or :class:`FreeRotor()` objects describing the hindered/free rotors ======================= =========================== ==================================== @@ -194,7 +199,10 @@ to apply atomization energy corrections (AEC) and spin orbit corrections (SOC) f atom corrections can be turned off by setting ``useAtomCorrections`` to ``False``. The ``bonds`` parameter is used to apply bond additivity corrections (BACs) for a given ``modelChemistry`` if using -Petersson-type BACs (``bondCorrectionType = 'p'``). When using Melius-type BACs (``bondCorrectionType = 'm'``), +Petersson-type BACs (``bondCorrectionType = 'p'``). +If the species' structure is specified in the Arkane input file, then the `bonds` attribute +can be automatically populated. Including this parameter in this case will overwrite +the automatically generated bonds. When using Melius-type BACs (``bondCorrectionType = 'm'``), specifying ``bonds`` is not required because the molecular connectivity is automatically inferred from the output of the quantum chemistry calculation. For a description of Petersson-type BACs, see Petersson et al., J. Chem. Phys. 1998, 109, 10570-10579. @@ -205,7 +213,8 @@ Allowed bond types for the ``bonds`` parameter are, e.g., ``'C-H'``, ``'C-C'``, ``'O=S=O'`` is also allowed. -The order of elements in the bond correction label is not important. Use ``-``/``=``/``#`` to denote a +The order of elements in the bond correction label is important. The first atom +should follow this priority: 'C', 'N', 'O', 'S', 'P', and 'H'. For bonds, use ``-``/``=``/``#`` to denote a single/double/triple bond, respectively. For example, for formaldehyde we would write:: bonds = {'C=O': 1, 'C-H': 2} @@ -239,7 +248,7 @@ directly. The energy used will depend on what ``modelChemistry`` was specified i energy from a Gaussian, Molpro, or QChem log file, all using the same ``Log`` class, as shown below. The input to the remaining parameters, ``geometry``, ``frequencies`` and ``rotors``, will depend on if hindered/free -rotors are included. +rotors are included. If ``geometry`` is not set, then Arkane will read the geometry from the ``frequencies`` file. Both cases are described below. Without Hindered/Free Rotors @@ -310,15 +319,15 @@ The output of step 2 is the correct log file to use for ``geometry/frequencies`` ``rotors`` is a list of :class:`HinderedRotor()` and/or :class:`FreeRotor()` objects. Each :class:`HinderedRotor()` object requires the following parameters: -====================== ========================================================================================== -Parameter Description -====================== ========================================================================================== -``scanLog`` The path to the Gaussian/Qchem log file, or a text file containing the scan energies -``pivots`` The indices of the atoms in the hindered rotor torsional bond -``top`` The indices of all atoms on one side of the torsional bond (including the pivot atom) -``symmetry`` The symmetry number for the torsional rotation (number of indistinguishable energy minima) -``fit`` Fit to the scan data. Can be either ``fourier``, ``cosine`` or ``best`` (default). -====================== ========================================================================================== +======================= =========================== ==================================== +Parameter Required? Description +======================= =========================== ==================================== +``scanLog`` yes The path to the Gaussian/Qchem log file, or a text file containing the scan energies +``pivots`` yes The indices of the atoms in the hindered rotor torsional bond +``top`` yes The indices of all atoms on one side of the torsional bond (including the pivot atom) +``symmetry`` optional The symmetry number for the torsional rotation (number of indistinguishable energy minima) +``fit`` optional Fit to the scan data. Can be either ``fourier``, ``cosine`` or ``best`` (default). +======================= =========================== ==================================== ``scanLog`` can either point to a ``Log`` file, or simply a ``ScanLog``, with the path to a text file summarizing the scan in the following format:: diff --git a/documentation/source/users/arkane/running.rst b/documentation/source/users/arkane/running.rst index 09aa05962d..f5d674feba 100644 --- a/documentation/source/users/arkane/running.rst +++ b/documentation/source/users/arkane/running.rst @@ -8,7 +8,7 @@ To execute an Arkane job, invoke the command :: The absolute or relative paths to the Arkane.py file as well as to the input file must be given. -The job will run and the results will be saved to ``output.py`` in the same +The job will run and the results will be saved in the same directory as the input file. If you wish to save the output elsewhere, use the ``-o``/``--output`` option, e.g. :: @@ -26,9 +26,17 @@ Log Verbosity ============= You can manipulate the amount of information logged to the console window using -the ``-q``/``--quiet`` flag (for quiet mode) or the ``-v``/``--verbose`` flag -(for verbose mode). The former causes the amount of logging information shown -to decrease; the latter causes it to increase. +the ``-q``/``--quiet`` flag (for quiet mode), the ``-v``/``--verbose`` flag +(for verbose mode), or the ``-d``/``--debug`` flag (for debug mode). +The former causes the amount of logging information shown +to decrease; the latter two cause it to increase. + +Suppressing plot creation +========================= + +Arkane by default will generate many plot files. By adding the ``-p``/``--no-plot`` +flag, Arkane will not generate any plots, reducing file size of output and +increasing the calculation speed. Help ==== diff --git a/rmgpy/pdep/configuration.pxd b/rmgpy/pdep/configuration.pxd index b6fb076257..fa722d9e69 100644 --- a/rmgpy/pdep/configuration.pxd +++ b/rmgpy/pdep/configuration.pxd @@ -43,7 +43,9 @@ cdef class Configuration: cpdef bint isUnimolecular(self) except -2 cpdef bint isBimolecular(self) except -2 - + + cpdef bint isTermolecular(self) except -2 + cpdef bint isTransitionState(self) except -2 cpdef bint hasStatMech(self) except -2 diff --git a/rmgpy/pdep/configuration.pyx b/rmgpy/pdep/configuration.pyx index fe6caebb1a..e245c53d03 100644 --- a/rmgpy/pdep/configuration.pyx +++ b/rmgpy/pdep/configuration.pyx @@ -105,7 +105,14 @@ cdef class Configuration: or product channel, or ``False`` otherwise. """ return len(self.species) == 2 - + + cpdef bint isTermolecular(self) except -2: + """ + Return ``True`` if the configuration represents a termolecular reactant + or product channel, or ``False`` otherwise. + """ + return len(self.species) == 3 + cpdef bint isTransitionState(self) except -2: """ Return ``True`` if the configuration represents a transition state, @@ -260,7 +267,7 @@ cdef class Configuration: else: # If the configuration is bimolecular, also include the relative # translational motion of the two molecules - if self.isBimolecular(): + if self.isBimolecular() or self.isTermolecular(): mass = [] for species in self.species: for mode in species.conformer.modes: @@ -275,10 +282,18 @@ cdef class Configuration: for atom in species.molecule[0].atoms: m += atom.element.mass mass.append(m * constants.amu * 1000) - assert len(mass) == 2 - mu = 1.0/(1.0/mass[0] + 1.0/mass[1]) - modes.insert(0, IdealGasTranslation(mass=(mu/constants.amu,"amu"))) - + if self.isBimolecular(): + if len(mass) != 2: + raise AttributeError('Length of masses should be two for bimolecular reactants. We got {0}.'.format(len(mass))) + mu = 1.0/(1.0/mass[0] + 1.0/mass[1]) + modes.insert(0, IdealGasTranslation(mass=(mu/constants.amu,"amu"))) + else: + if len(mass) != 3: + raise AttributeError('Length of masses should be three for termolecular reactants. We got {0}.'.format(len(mass))) + mu = 1.0/(1.0/mass[0] + 1.0/mass[1]) + modes.insert(0, IdealGasTranslation(mass=(mu/constants.amu,"amu"))) + mu2 = 1.0/(1.0/mass[0] + 1.0/mass[2]) + modes.insert(0, IdealGasTranslation(mass=(mu2/constants.amu,"amu"))) if rmgmode: # Compute the density of states by direct count # This is currently faster than the method of steepest descents, diff --git a/rmgpy/quantity.py b/rmgpy/quantity.py index 805c3c8c2d..fd00865f79 100644 --- a/rmgpy/quantity.py +++ b/rmgpy/quantity.py @@ -740,6 +740,7 @@ def __call__(self, *args, **kwargs): Energy = Enthalpy = FreeEnergy = UnitType('J/mol', commonUnits=['kJ/mol', 'cal/mol', 'kcal/mol', 'eV/molecule'], extraDimensionality={'K': constants.R, + 'cm^-1': constants.h * constants.c * 100 # the following hack also allows 'J' and 'kJ' etc. to be specified without /mol[ecule] # so is not advisable (and fails unit tests) # 'eV': constants.Na, # allow people to be lazy and neglect the "/molecule"