From d3c0f42826a5c2bc8f217cebb16bc5b746dfe70f Mon Sep 17 00:00:00 2001 From: Mark Goldman Date: Mon, 6 May 2019 00:09:11 -0400 Subject: [PATCH 01/23] Allow wavenumbers as valid energy input This commit allows wavenumbers to be valid energy input as they are used for Lenard-Jones parameters among other applications. --- rmgpy/quantity.py | 1 + 1 file changed, 1 insertion(+) 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" From bb67d46590254d5e08c39de9e6ec623523484da7 Mon Sep 17 00:00:00 2001 From: Mark Goldman Date: Tue, 12 Mar 2019 11:13:15 -0400 Subject: [PATCH 02/23] Support termolecular products in Arkane pdep Previously, having termolecular products would break Arkane since it doesn't properly account for the reverse reaction. --- rmgpy/pdep/configuration.pxd | 4 +++- rmgpy/pdep/configuration.pyx | 27 +++++++++++++++++++++------ 2 files changed, 24 insertions(+), 7 deletions(-) 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, From eac22a71bfe2c2abbb918bfea3615a3d86cf10b8 Mon Sep 17 00:00:00 2001 From: Mark Goldman Date: Tue, 26 Feb 2019 23:04:07 -0500 Subject: [PATCH 03/23] Set default of Arkane to generate plots. This commit changes the default behavior of Arkane to generate plots, and integrates this change into the default documentation. --- arkane/main.py | 3 ++- documentation/source/users/arkane/running.rst | 16 ++++++++++++---- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/arkane/main.py b/arkane/main.py index 350e135583..d6ab7f4fa3 100644 --- a/arkane/main.py +++ b/arkane/main.py @@ -125,7 +125,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() 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 ==== From e866d0f832d51754804e035e076bf26d63b900a4 Mon Sep 17 00:00:00 2001 From: Mark Goldman Date: Tue, 26 Feb 2019 23:14:31 -0500 Subject: [PATCH 04/23] Only draw kinetic PDF if plotting is enabled This commit changes the default behavior of a high-p kinetics job to only draw the PES when the user specifies that they would like plots, by using the '-p' option when running arkane. This allows users who want automatically generated graphs to have them, whereas those that would not like those graphs will not obtain extra files or run extra code. --- arkane/kinetics.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/arkane/kinetics.py b/arkane/kinetics.py index 5fe94fc196..319a877f72 100644 --- a/arkane/kinetics.py +++ b/arkane/kinetics.py @@ -149,7 +149,7 @@ def execute(self, outputFile=None, plot=True): self.save(outputFile) if plot: self.plot(os.path.dirname(outputFile)) - self.draw(os.path.dirname(outputFile)) + self.draw(os.path.dirname(outputFile)) if self.sensitivity_conditions is not None: logging.info('\n\nRunning sensitivity analysis...') sa(self, os.path.dirname(outputFile)) From f61c1d973ce17f450152d7272cd4d5ac722852f6 Mon Sep 17 00:00:00 2001 From: MarkGoldman Date: Tue, 23 Apr 2019 18:28:44 -0400 Subject: [PATCH 05/23] Refactor Arkane thermo output This commit refactors arkane thermo output by splitting `save` into `write_output` and `write_chemkin`, adding try, except statements on outputs, and passing the output directory instead of the `output.py` file path to each method. --- arkane/main.py | 2 +- arkane/thermo.py | 49 +++++++++++++++++++++++++++++++++++------------- 2 files changed, 37 insertions(+), 14 deletions(-) diff --git a/arkane/main.py b/arkane/main.py index d6ab7f4fa3..7a275a30cc 100644 --- a/arkane/main.py +++ b/arkane/main.py @@ -269,7 +269,7 @@ def execute(self): supporting_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) 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: From 89691a398dbd2febde5c9995180c80f6a62ecf05 Mon Sep 17 00:00:00 2001 From: MarkGoldman Date: Thu, 25 Apr 2019 09:52:31 -0400 Subject: [PATCH 06/23] Refactor Arkane statmech output This commit refactors arkane statmech output by renaming `save` to `write_output`, adding try/except statements on outputs, and passing the output directory instead of the `output.py` file path to each method. --- arkane/main.py | 2 +- arkane/statmech.py | 23 +++++++++++++++-------- 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/arkane/main.py b/arkane/main.py index 7a275a30cc..77526d7e08 100644 --- a/arkane/main.py +++ b/arkane/main.py @@ -271,7 +271,7 @@ def execute(self): if isinstance(job, ThermoJob): 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)) + job.execute(output_directory=self.outputDirectory, plot=self.plot, pdep=is_pdep(self.jobList)) supporting_info.append(job.supporting_info) with open(chemkinFile, 'a') as f: diff --git a/arkane/statmech.py b/arkane/statmech.py index dc9c840f4f..120621c684 100644 --- a/arkane/statmech.py +++ b/arkane/statmech.py @@ -187,14 +187,20 @@ def __init__(self, species, path): self.bonds = None self.arkane_species = ArkaneSpecies(species=species) - 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) + 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)) logging.debug('Finished statmech job for species {0}.'.format(self.species)) logging.debug(repr(self.species)) @@ -560,12 +566,13 @@ def load(self, pdep=False): 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') From 74eb863ff6538f81b2a3d2265ad6f22cffeeefd2 Mon Sep 17 00:00:00 2001 From: MarkGoldman Date: Thu, 25 Apr 2019 10:41:31 -0400 Subject: [PATCH 07/23] Refactor Arkane kinetics output This commit refactors arkane kinetics output by splitting `save` into `write_output`, `write_yaml` and `write_chemkin`, adding try/except statements on outputs, and passing the output directory instead of the `output.py` file path to each method. --- arkane/kinetics.py | 91 +++++++++++++++++++++++++++++----------------- arkane/main.py | 2 +- 2 files changed, 59 insertions(+), 34 deletions(-) diff --git a/arkane/kinetics.py b/arkane/kinetics.py index 319a877f72..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/main.py b/arkane/main.py index 77526d7e08..051cec57a4 100644 --- a/arkane/main.py +++ b/arkane/main.py @@ -302,7 +302,7 @@ def execute(self): # 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 From dde36b6d952b70769285151c2e60ef7c3bec4103 Mon Sep 17 00:00:00 2001 From: MarkGoldman Date: Thu, 25 Apr 2019 16:21:29 -0400 Subject: [PATCH 08/23] Make execute() calls use directory in commonTest Previous commit got job.execute to take in the output directory instead of the `output.py` file. This commit gets commonTest.py to use this new input style. --- arkane/commonTest.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) 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 From 955533a1872e72f217c72c531ba2e1bb03b64495 Mon Sep 17 00:00:00 2001 From: MarkGoldman Date: Fri, 26 Apr 2019 18:26:00 -0400 Subject: [PATCH 09/23] Add get_T1_diagnostic method to log file parsers This commit allows Log objects to parse out the T1 diagnostic, so it can be automatically found and returned for Arkane users. The method was also added to the QchemLog class. --- arkane/log.py | 6 ++++++ arkane/molpro.py | 14 ++++++++++++++ arkane/molproTest.py | 8 ++++++++ 3 files changed, 28 insertions(+) diff --git a/arkane/log.py b/arkane/log.py index e6cc855572..8f2df5e7d6 100644 --- a/arkane/log.py +++ b/arkane/log.py @@ -155,3 +155,9 @@ def get_optical_isomers_and_symmetry_number(self): return optical_isomers, symmetry finally: shutil.rmtree(scr_dir) + + 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/molpro.py b/arkane/molpro.py index a1ce0687c1..43c0e10f15 100644 --- a/arkane/molpro.py +++ b/arkane/molpro.py @@ -395,3 +395,17 @@ 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)) \ No newline at end of file diff --git a/arkane/molproTest.py b/arkane/molproTest.py index 49cd64edcc..19f0a6ca7d 100644 --- a/arkane/molproTest.py +++ b/arkane/molproTest.py @@ -135,5 +135,13 @@ def test_load_negative_frequency(self): self.assertEqual(imaginary_freq, -1997.98) + 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)) From c4e95d5850e3844f6de6c5ef568d2f5e46775a5e Mon Sep 17 00:00:00 2001 From: Mark Goldman Date: Mon, 6 May 2019 21:10:54 -0400 Subject: [PATCH 10/23] Refactor Log.get_symmetry_properties Previously, it was not possible to retrieve and save the point group used for symmetry calculations in Arkane's output data for supporting information. This commit refactors a Log method to also output point group information, which involved renaming it to the more general `get_symmetry_properties`. --- arkane/gaussian.py | 2 +- arkane/gaussianTest.py | 2 +- arkane/log.py | 7 ++++--- arkane/molpro.py | 2 +- arkane/qchem.py | 2 +- 5 files changed, 8 insertions(+), 7 deletions(-) diff --git a/arkane/gaussian.py b/arkane/gaussian.py index feb0ea5d00..e361b40b92 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: 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/log.py b/arkane/log.py index 8f2df5e7d6..121833e214 100644 --- a/arkane/log.py +++ b/arkane/log.py @@ -118,11 +118,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,7 +153,7 @@ 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) diff --git a/arkane/molpro.py b/arkane/molpro.py index 43c0e10f15..f9f129fa36 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: 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: From 153081d83c1cd9e5028342b290f9c6bfc156f300 Mon Sep 17 00:00:00 2001 From: MarkGoldman Date: Thu, 25 Apr 2019 19:51:21 -0400 Subject: [PATCH 11/23] Add more information in supporting_information.csv This commit refactors where the supporting information is saved to be at the end of StatMechJob.load method instead of partially in the __init__ method and throughout the load method. This commit also adds the symmetry, optical isomers, energies (with and without zero-point energy), T1 diagnostic, and geometry to the exported csv file. This commit also prevents from creating the supporting_information.csv file when no species will be output, simplifying the process for other users of Arkane. Try/except statements are added to reduce potential for errors. This commit renamed the modified e_electronic to e_electronic_with_corrections to allow for the base electronic energy to be output. This commit also renames the unused e_0 to e0 --- arkane/main.py | 47 +++++++++++++++++++++++------------------ arkane/statmech.py | 52 +++++++++++++++++++++++++++++++++------------- 2 files changed, 65 insertions(+), 34 deletions(-) diff --git a/arkane/main.py b/arkane/main.py index 051cec57a4..d90cefbce2 100644 --- a/arkane/main.py +++ b/arkane/main.py @@ -272,33 +272,40 @@ def execute(self): job.execute(output_directory=self.outputDirectory, plot=self.plot) if isinstance(job, StatMechJob): job.execute(output_directory=self.outputDirectory, plot=self.plot, pdep=is_pdep(self.jobList)) - supporting_info.append(job.supporting_info) + if hasattr(job, 'supporting_info'): + supporting_info.append(job.supporting_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']) + 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]]) # run kinetics and pdep jobs (also writes reaction blocks to Chemkin file) for job in self.jobList: if isinstance(job, KineticsJob): diff --git a/arkane/statmech.py b/arkane/statmech.py index 120621c684..7bdc88ed07 100644 --- a/arkane/statmech.py +++ b/arkane/statmech.py @@ -183,7 +183,6 @@ 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) @@ -308,7 +307,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]) @@ -373,17 +372,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)) @@ -424,17 +417,18 @@ def load(self, pdep=False): e_electronic *= constants.E_h * constants.Na # convert Hartree/particle into J/mol if not self.applyAtomEnergyCorrections: logging.warning('Atom corrections are not being used. Do not trust energies and thermo.') - e_electronic += get_energy_correction( + corrections = 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 ) + e_electronic_with_corrections = e_electronic + 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)) @@ -447,7 +441,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 @@ -564,6 +557,37 @@ 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) + #save conformer self.species.conformer = conformer def write_output(self, output_directory): From 6f8a64b282fe6fde42efa46b79968fb852eccbef Mon Sep 17 00:00:00 2001 From: MarkGoldman Date: Sun, 28 Apr 2019 21:03:55 -0400 Subject: [PATCH 12/23] prevent errors from plotHinderedRotors This commit prevents plotHinderedRotors from being called unless the user uses the `--plot` argument. It also has a try except statement to prevent plotting from stopping a job. --- arkane/statmech.py | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/arkane/statmech.py b/arkane/statmech.py index 7bdc88ed07..f28865be29 100644 --- a/arkane/statmech.py +++ b/arkane/statmech.py @@ -193,7 +193,7 @@ def execute(self, output_directory=None, plot=False, pdep=False): If `plot` is True, then plots of the hindered rotor fits will be saved. """ - self.load(pdep) + self.load(pdep, plot) if output_directory is not None: try: self.write_output(output_directory) @@ -203,7 +203,7 @@ def execute(self, output_directory=None, plot=False, pdep=False): 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 @@ -528,8 +528,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.plotHinderedRotor(angle, v_list, cosineRotor, fourierRotor, rotor, rotorCount, directory) + except Exception as e: + logging.warning("Could not plot hindered rotor graph due to error: {0}".format(e)) rotorCount += 1 @@ -632,11 +635,7 @@ def plotHinderedRotor(self, angle, v_list, cosineRotor, fourierRotor, rotor, rot series potential fits. The plot is saved to a set of files of the form ``hindered_rotor_1.pdf``. """ - try: - import pylab - except ImportError: - return - + import pylab phi = np.arange(0, 6.3, 0.02, np.float64) Vlist_cosine = np.zeros_like(phi) Vlist_fourier = np.zeros_like(phi) From 1a1d849dd051245d26c72a19881c050ead1fb89f Mon Sep 17 00:00:00 2001 From: MarkGoldman Date: Sun, 28 Apr 2019 21:49:02 -0400 Subject: [PATCH 13/23] Output hindered rotor scans in csv file for run Previously, hindered rotor data was extracted from log data and saved into the directory where the log file was containted. This decentralized the information making it more difficult to compare and plot. This commit places this data inside `hindered_rotor_scan_data.csv` for all species and transition states in the arkane run, which facilitates future usage of the data. --- arkane/main.py | 19 ++++++++++++++++++- arkane/statmech.py | 16 ++++------------ 2 files changed, 22 insertions(+), 13 deletions(-) diff --git a/arkane/main.py b/arkane/main.py index d90cefbce2..2cf8fde19e 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 @@ -267,6 +268,7 @@ 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(output_directory=self.outputDirectory, plot=self.plot) @@ -274,6 +276,9 @@ def execute(self): 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') @@ -305,7 +310,19 @@ def execute(self): 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]]) + writer.writerow([label, row[1], row[2], row[3], rot, freq, row[7], row[8], row[9], atoms, row[12]]) + 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] + + [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): diff --git a/arkane/statmech.py b/arkane/statmech.py index f28865be29..95a31139af 100644 --- a/arkane/statmech.py +++ b/arkane/statmech.py @@ -341,6 +341,7 @@ def load(self, pdep=False, plot=False): rotors = [] if self.includeHinderedRotors: + self.raw_hindered_rotor_data = [] try: rotors = local_context['rotors'] except KeyError: @@ -471,26 +472,17 @@ def load(self, pdep=False, plot=False): # Load the hindered rotor scan energies if isinstance(scanLog, Log) and not isinstance(energy, (GaussianLog, QChemLog, MolproLog)): scanLog = determine_qm_software(os.path.join(directory, scanLog.path)) - if isinstance(scanLog, GaussianLog): - 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) - elif isinstance(scanLog, QChemLog): - 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, ScanLog): - scanLog.path = os.path.join(directory, scanLog.path) angle, v_list = scanLog.load() 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)) inertia = conformer.getInternalReducedMomentOfInertia(pivots, top) * constants.Na * 1e23 cosineRotor = HinderedRotor(inertia=(inertia, "amu*angstrom^2"), symmetry=symmetry) From 045ffd3eb02758e3054e5adcc4f351b976fb1a79 Mon Sep 17 00:00:00 2001 From: Mark Goldman Date: Mon, 6 May 2019 23:55:06 -0400 Subject: [PATCH 14/23] Read hindered rotor scan information and output to csv This commit adds methods for Arkane to automatically read hindered rotor scan information, like which dihedral is being pivoted, which dihedrals are being fixed, and outputs it to the hindered rotor csv file for easier analysis. --- arkane/gaussian.py | 65 +++++++++++++++++++++++++++++++++++++++++++++- arkane/log.py | 19 ++++++++++++++ arkane/main.py | 4 +-- arkane/statmech.py | 16 +++++++++++- 4 files changed, 100 insertions(+), 4 deletions(-) diff --git a/arkane/gaussian.py b/arkane/gaussian.py index e361b40b92..7f81cc0520 100644 --- a/arkane/gaussian.py +++ b/arkane/gaussian.py @@ -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/log.py b/arkane/log.py index 121833e214..308bd53df7 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 diff --git a/arkane/main.py b/arkane/main.py index 2cf8fde19e..97bc7bb6ff 100644 --- a/arkane/main.py +++ b/arkane/main.py @@ -321,8 +321,8 @@ def execute(self): '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] - + [a for a in row[4]]) + 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): diff --git a/arkane/statmech.py b/arkane/statmech.py index 95a31139af..5f2bc3d131 100644 --- a/arkane/statmech.py +++ b/arkane/statmech.py @@ -475,14 +475,28 @@ def load(self, pdep=False, plot=False): scanLog.path = os.path.join(directory, scanLog.path) if isinstance(scanLog, (GaussianLog, QChemLog)): v_list, angle = scanLog.loadScanEnergies() + 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): 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)) + 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) From 2fd1f8e4c67622a229d913019638686420095caf Mon Sep 17 00:00:00 2001 From: Mark Goldman Date: Mon, 6 May 2019 21:00:03 -0400 Subject: [PATCH 15/23] Refactor plotHinderedRotor Previously, plotHinderedRotor was called under the `load` method, which gave limited flexibility as to where the hindered rotor plot would be saved and was also unintuitive since it was not in a method labeled `output`. This commit refactors the plotHinderedRotor method by separating it into two separate methods, one for internally storing the data, and a second for writing the plot to disk. This should reduce any potential errors in writing from impacting the loading and creating of a model. The refactor also saves the plots in the Arkane output folder, which should be more intuitive for users. --- arkane/statmech.py | 35 ++++++++++++++++++++++++++++------- 1 file changed, 28 insertions(+), 7 deletions(-) diff --git a/arkane/statmech.py b/arkane/statmech.py index 5f2bc3d131..a23eb3bed5 100644 --- a/arkane/statmech.py +++ b/arkane/statmech.py @@ -185,6 +185,7 @@ def __init__(self, species, path): self.atomEnergies = None self.bonds = None self.arkane_species = ArkaneSpecies(species=species) + self.hindered_rotor_plots = [] def execute(self, output_directory=None, plot=False, pdep=False): """ @@ -200,6 +201,15 @@ def execute(self, output_directory=None, plot=False, pdep=False): 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)) @@ -536,7 +546,7 @@ def load(self, pdep=False, plot=False): conformer.modes.append(rotor) if plot: try: - self.plotHinderedRotor(angle, v_list, cosineRotor, fourierRotor, rotor, rotorCount, directory) + 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)) @@ -635,13 +645,16 @@ def write_output(self, output_directory): 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. """ - import pylab + try: + import pylab + 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) @@ -667,8 +680,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))) ################################################################################ From 98306e162aafe08679298e083b32c9d32ab34358 Mon Sep 17 00:00:00 2001 From: Mark Goldman Date: Mon, 20 May 2019 00:28:19 -0400 Subject: [PATCH 16/23] Update Arkane documentation This commit updates Arkane documentation to indicate the label given species is used in outputs of Arkane, indicate that symmetry and optical isomers are no longer required inputs, as well as update the hindered rotor section to indicate that symmetry is not required as well. --- documentation/source/users/arkane/input.rst | 31 ++++++++++++--------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/documentation/source/users/arkane/input.rst b/documentation/source/users/arkane/input.rst index 784fde4a05..ed5ee11867 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,9 +182,9 @@ 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 @@ -310,15 +315,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:: From cef40c2e15635eed33cc35026ef528d37a2d7a58 Mon Sep 17 00:00:00 2001 From: MarkGoldman Date: Wed, 3 Jul 2019 16:16:08 -0400 Subject: [PATCH 17/23] Make geometry input for Arkane.statmech optional If not provided, it will read from the frequencies file. --- arkane/statmech.py | 17 +++++++++-------- documentation/source/users/arkane/input.rst | 4 ++-- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/arkane/statmech.py b/arkane/statmech.py index a23eb3bed5..7fa3e8cefe 100644 --- a/arkane/statmech.py +++ b/arkane/statmech.py @@ -328,14 +328,6 @@ def load(self, pdep=False, plot=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'] @@ -345,6 +337,15 @@ def load(self, pdep=False, plot=False): 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 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) + 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)) diff --git a/documentation/source/users/arkane/input.rst b/documentation/source/users/arkane/input.rst index ed5ee11867..209c6dce33 100644 --- a/documentation/source/users/arkane/input.rst +++ b/documentation/source/users/arkane/input.rst @@ -188,7 +188,7 @@ Parameter Required? Description ``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 ======================= =========================== ==================================== @@ -244,7 +244,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 From 9d9be9b02dddb2046242107bbd4cb1d2db29097e Mon Sep 17 00:00:00 2001 From: MarkGoldman Date: Wed, 10 Jul 2019 17:39:12 -0400 Subject: [PATCH 18/23] Add method get_D1_diagnostic Added method to get the D1 diagnostic from coupled cluster calculations with molpro. --- arkane/log.py | 6 ++++++ arkane/molpro.py | 16 +++++++++++++++- arkane/molproTest.py | 7 +++++++ 3 files changed, 28 insertions(+), 1 deletion(-) diff --git a/arkane/log.py b/arkane/log.py index 308bd53df7..5d6c3d9552 100644 --- a/arkane/log.py +++ b/arkane/log.py @@ -176,6 +176,12 @@ def get_symmetry_properties(self): 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 diff --git a/arkane/molpro.py b/arkane/molpro.py index f9f129fa36..309f05dd53 100644 --- a/arkane/molpro.py +++ b/arkane/molpro.py @@ -408,4 +408,18 @@ def get_T1_diagnostic(self): 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)) \ No newline at end of file + 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 19f0a6ca7d..ea9aec9195 100644 --- a/arkane/molproTest.py +++ b/arkane/molproTest.py @@ -134,6 +134,13 @@ 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): """ From f7300c337effcf2da3328c6ed8a7c15ef33ace6e Mon Sep 17 00:00:00 2001 From: MarkGoldman Date: Wed, 10 Jul 2019 17:48:35 -0400 Subject: [PATCH 19/23] Output D1 diagnostic for arkane in supporting_info.csv This commit outputs the D1 diagnostic for coupled cluster calculations in the supporting_information.py when running Arkane calculations. --- arkane/main.py | 5 +++-- arkane/statmech.py | 5 +++++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/arkane/main.py b/arkane/main.py index 97bc7bb6ff..31c8096503 100644 --- a/arkane/main.py +++ b/arkane/main.py @@ -294,7 +294,7 @@ def execute(self): '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']) + 'T1 diagnostic', 'D1 diagnostic']) for row in supporting_info: label = row[0] rot = '-' @@ -310,7 +310,8 @@ def execute(self): 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]]) + 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 diff --git a/arkane/statmech.py b/arkane/statmech.py index 7fa3e8cefe..5e7dffff59 100644 --- a/arkane/statmech.py +++ b/arkane/statmech.py @@ -607,6 +607,11 @@ def load(self, pdep=False, plot=False): 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 From 66abeb4db3c593b129bf4f73128ebacb626b8a56 Mon Sep 17 00:00:00 2001 From: MarkGoldman Date: Wed, 10 Jul 2019 19:15:17 -0400 Subject: [PATCH 20/23] Deprecate apply_energy_correction This commit gets StatmechJob to use getatom_correction and get_bac, so that the corrected energies from both methods can be included in future statmech analysis (for example, not including bond corrections for kinetics calculations, but including them in thermo calculations). Since apply_energy_correction is no longer used, a deprecation warning has been added. --- arkane/encorr/corr.py | 38 ++++++++++++++++++++++++++++++++------ arkane/statmech.py | 22 +++++++++++++--------- 2 files changed, 45 insertions(+), 15 deletions(-) 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/statmech.py b/arkane/statmech.py index 5e7dffff59..ee0250bfd4 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 ################################################################################ @@ -427,15 +427,19 @@ def load(self, pdep=False, plot=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.') - corrections = 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 - ) - e_electronic_with_corrections = e_electronic + corrections + if self.applyBondEnergyCorrections: + bond_corrections = get_bac(self.modelChemistry, 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)) From efa7c5ad121debbdfdca6429d9210d74276cd4ec Mon Sep 17 00:00:00 2001 From: MarkGoldman Date: Wed, 10 Jul 2019 19:21:07 -0400 Subject: [PATCH 21/23] Correct documentation error for bond correction labels The documentation previously said the order of atoms in the bond correction labels is NOT important. This is entirely incorrect. This commit corrected this by copying info from the comments in applyBondEnergyCorrections. --- documentation/source/users/arkane/input.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/documentation/source/users/arkane/input.rst b/documentation/source/users/arkane/input.rst index 209c6dce33..4b736e32ae 100644 --- a/documentation/source/users/arkane/input.rst +++ b/documentation/source/users/arkane/input.rst @@ -210,7 +210,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} From 97c31e80a3a194830031786d36c86796e0ef6ef1 Mon Sep 17 00:00:00 2001 From: MarkGoldman Date: Wed, 10 Jul 2019 19:39:50 -0400 Subject: [PATCH 22/23] Automatically generate bonds for bond energy corrections This commit uses the molecule.enuerate_bonds to generate the bonds for bond energy corrections if the user did not specify the bonds. --- arkane/statmech.py | 4 +++- documentation/source/users/arkane/input.rst | 5 ++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/arkane/statmech.py b/arkane/statmech.py index ee0250bfd4..0dece38cf6 100644 --- a/arkane/statmech.py +++ b/arkane/statmech.py @@ -435,7 +435,9 @@ def load(self, pdep=False, plot=False): atom_corrections = 0 logging.warning('Atom corrections are not being used. Do not trust energies and thermo.') if self.applyBondEnergyCorrections: - bond_corrections = get_bac(self.modelChemistry, bonds, coordinates, number, + 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 diff --git a/documentation/source/users/arkane/input.rst b/documentation/source/users/arkane/input.rst index 4b736e32ae..ce6f0a996f 100644 --- a/documentation/source/users/arkane/input.rst +++ b/documentation/source/users/arkane/input.rst @@ -199,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. From 841c8a089bb4f57b82392ba5849131ae20ad6be4 Mon Sep 17 00:00:00 2001 From: MarkGoldman Date: Mon, 15 Jul 2019 23:54:45 -0400 Subject: [PATCH 23/23] Use type(obj).__name__ to determime logfile type Previously statmech.py used `isinstance(x, GaussianLog, QchemLog, MolproLog))` when determining if the logfile has the proper methods implemented. This may cause issues when adding more subclasses, if these lines are not properly modified. This commit reduces that potential error by replacing that command with `isinstance(x, Log) and type(obj).__name__ != 'Log'` --- arkane/statmech.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/arkane/statmech.py b/arkane/statmech.py index 0dece38cf6..6440de4193 100644 --- a/arkane/statmech.py +++ b/arkane/statmech.py @@ -307,9 +307,9 @@ def load(self, pdep=False, plot=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): @@ -333,13 +333,13 @@ def load(self, pdep=False, plot=False): 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 not isinstance(energy, (GaussianLog, QChemLog, MolproLog)): + 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) @@ -487,7 +487,7 @@ def load(self, pdep=False, plot=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)) scanLog.path = os.path.join(directory, scanLog.path) if isinstance(scanLog, (GaussianLog, QChemLog)):