From 311f15939dd49e9d6034e0cb7947c922b1c12959 Mon Sep 17 00:00:00 2001 From: Mark Goldman Date: Fri, 8 Feb 2019 18:49:49 -0500 Subject: [PATCH 1/6] Inheritance for Log files Arkane This commit makes the Log file readers for QChem, Gaussian and Molepro inherit from the base Log class, which can reduce code duplication going forward. To do this, the method `determine_qm_software` was removed from the Log class as this causes circular import errors, and the `Log` class was moved to a separate file, in accordance with PEP-8. Methods used in all child classes are added to the parent Log class. This commit also modified methods calling `determine_qm_software`, and references to `isinstance(x,Log)` were altered to not include the children log files. --- arkane/gaussian.py | 3 +- arkane/log.py | 76 ++++++++++++++++++++++++++++++++++++++++++++++ arkane/molpro.py | 4 +-- arkane/qchem.py | 3 +- arkane/statmech.py | 42 +++++++++---------------- 5 files changed, 97 insertions(+), 31 deletions(-) create mode 100644 arkane/log.py diff --git a/arkane/gaussian.py b/arkane/gaussian.py index 6c9b81c093..399608a376 100644 --- a/arkane/gaussian.py +++ b/arkane/gaussian.py @@ -38,11 +38,12 @@ from rmgpy.exceptions import InputError from arkane.common import check_conformer_energy, get_element_mass +from arkane.statmech import Log ################################################################################ -class GaussianLog: +class GaussianLog(Log): """ Represent a log file from Gaussian. The attribute `path` refers to the location on disk of the Gaussian log file of interest. Methods are provided diff --git a/arkane/log.py b/arkane/log.py new file mode 100644 index 0000000000..2df026a3fc --- /dev/null +++ b/arkane/log.py @@ -0,0 +1,76 @@ +#!/usr/bin/env python2 +# -*- coding: utf-8 -*- +""" +A general class for parsing quantum mechanical log files +""" + +class Log(object): + """ + Represent a general log file. + The attribute `path` refers to the location on disk of the log file of interest. + """ + def __init__(self, path): + self.path = path + + def getNumberOfAtoms(self): + """ + Return the number of atoms in the molecular configuration used in + the MolPro log file. + """ + raise NotImplementedError("loadGeometry is not implemented for the Log class") + + def loadForceConstantMatrix(self): + """ + Return the force constant matrix (in Cartesian coordinates) from the + QChem log file. If multiple such matrices are identified, + only the last is returned. The units of the returned force constants + are J/m^2. If no force constant matrix can be found in the log file, + ``None`` is returned. + """ + raise NotImplementedError("loadGeometry is not implemented for the Log class") + + def loadGeometry(self): + """ + Return the optimum geometry of the molecular configuration from the + log file. If multiple such geometries are identified, only the + last is returned. + """ + raise NotImplementedError("loadGeometry is not implemented for the Log class") + + def loadConformer(self, symmetry=None, spinMultiplicity=0, opticalIsomers=None, symfromlog=None, label=''): + """ + Load the molecular degree of freedom data from an output file created as the result of a + QChem "Freq" calculation. As QChem's guess of the external symmetry number is not always correct, + you can use the `symmetry` parameter to substitute your own value; + if not provided, the value in the QChem output file will be adopted. + """ + raise NotImplementedError("loadGeometry is not implemented for the Log class") + + def loadEnergy(self, frequencyScaleFactor=1.): + """ + Load the energy in J/mol from a QChem log file. Only the last energy + in the file is returned. The zero-point energy is *not* included in + the returned value. + """ + raise NotImplementedError("loadGeometry is not implemented for the Log class") + + def loadZeroPointEnergy(self,frequencyScaleFactor=1.): + """ + Load the unscaled zero-point energy in J/mol from a QChem output file. + """ + raise NotImplementedError("loadGeometry is not implemented for the Log class") + + def loadScanEnergies(self): + """ + Extract the optimized energies in J/mol from a QChem log file, e.g. the + result of a QChem "PES Scan" quantum chemistry calculation. + """ + raise NotImplementedError("loadGeometry is not implemented for the Log class") + + def loadNegativeFrequency(self): + """ + Return the imaginary frequency from a transition state frequency + calculation in cm^-1. + """ + raise NotImplementedError("loadGeometry is not implemented for the Log class") + diff --git a/arkane/molpro.py b/arkane/molpro.py index 043c31dbb6..eae5b2f8b7 100644 --- a/arkane/molpro.py +++ b/arkane/molpro.py @@ -37,11 +37,11 @@ from rmgpy.statmech import IdealGasTranslation, NonlinearRotor, LinearRotor, HarmonicOscillator, Conformer from arkane.common import get_element_mass - +from arkane.statmech import Log ################################################################################ -class MolproLog: +class MolproLog(Log): """ Represents a Molpro log file. The attribute `path` refers to the location on disk of the Molpro log file of interest. Methods are provided diff --git a/arkane/qchem.py b/arkane/qchem.py index 6a53d76fcd..d6062402e8 100644 --- a/arkane/qchem.py +++ b/arkane/qchem.py @@ -38,11 +38,12 @@ from rmgpy.statmech import IdealGasTranslation, NonlinearRotor, LinearRotor, HarmonicOscillator, Conformer from arkane.common import check_conformer_energy, get_element_mass +from arkane.statmech import Log ################################################################################ -class QChemLog: +class QChemLog(Log): """ Represent an output file from QChem. The attribute `path` refers to the location on disk of the QChem output file of interest. Methods are provided diff --git a/arkane/statmech.py b/arkane/statmech.py index 4dfffc9b7c..51d6553c0b 100644 --- a/arkane/statmech.py +++ b/arkane/statmech.py @@ -53,6 +53,7 @@ from rmgpy.molecule.molecule import Molecule from arkane.output import prettify +from arkane.log import Log from arkane.gaussian import GaussianLog from arkane.molpro import MolproLog from arkane.qchem import QChemLog @@ -287,9 +288,8 @@ def load(self, pdep=False): '{1!r}.'.format(self.modelChemistry, path)) E0_withZPE, E0 = None, None energyLog = None - if isinstance(energy, Log): - energy.determine_qm_software(os.path.join(directory, energy.path)) - energyLog = energy.software_log + if isinstance(energy, Log) and not isinstance(energy, (GaussianLog,QChemLog,MolproLog)): + energyLog = determine_qm_software(os.path.join(directory, energy.path)) elif isinstance(energy, (GaussianLog,QChemLog,MolproLog)): energyLog = energy energyLog.path = os.path.join(directory, energyLog.path) @@ -314,9 +314,8 @@ def load(self, pdep=False): geomLog = local_context['geometry'] except KeyError: raise InputError('Required attribute "geometry" not found in species file {0!r}.'.format(path)) - if isinstance(geomLog, Log): - geomLog.determine_qm_software(os.path.join(directory, geomLog.path)) - geomLog = geomLog.software_log + 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) @@ -324,9 +323,8 @@ def load(self, pdep=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): - statmechLog.determine_qm_software(os.path.join(directory, statmechLog.path)) - statmechLog = statmechLog.software_log + if isinstance(statmechLog, Log) and not isinstance(energy, (GaussianLog,QChemLog,MolproLog)): + statmechLog = determine_qm_software(os.path.join(directory, statmechLog.path)) else: statmechLog.path = os.path.join(directory, statmechLog.path) @@ -462,9 +460,8 @@ 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): - scanLog.determine_qm_software(os.path.join(directory, scanLog.path)) - scanLog = scanLog.software_log + 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) v_list, angle = scanLog.loadScanEnergies() @@ -874,21 +871,11 @@ def applyEnergyCorrections(E0, modelChemistry, atoms, bonds, return E0 - -class Log(object): +def determine_qm_software(fullpath): """ - Represent a general log file. - The attribute `path` refers to the location on disk of the log file of interest. - A method is provided to determine whether it is a Gaussian, Molpro, or QChem type. + Given a path to the log file of a QM software, determine whether it is Gaussian, Molpro, or QChem """ - def __init__(self, path): - self.path = path - - def determine_qm_software(self, fullpath): - """ - Given a path to the log file of a QM software, determine whether it is Gaussian, Molpro, or QChem - """ - f = open(fullpath, 'r') + with open(fullpath, 'r') as f: line = f.readline() software_log = None while line != '': @@ -905,8 +892,9 @@ def determine_qm_software(self, fullpath): software_log = MolproLog(fullpath) break line = f.readline() - f.close() - self.software_log = software_log + else: + raise InputError("File at {0} could not be identified as a Gaussian, QChem or Molpro log file.".format(fullpath)) + return software_log def projectRotors(conformer, F, rotors, linear, is_ts): From 24df7fb5cea35dcb98083a6fa0731534552664d3 Mon Sep 17 00:00:00 2001 From: Mark Goldman Date: Sun, 10 Feb 2019 20:21:52 -0500 Subject: [PATCH 2/6] Allow no opticalIsomers parameter in Arkane Previously, not having the number of opticalIsomers would throw and error. Now the computer can estimate the number of optical isomers, so no input is accepted. --- arkane/statmech.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/arkane/statmech.py b/arkane/statmech.py index 51d6553c0b..7c070dda1a 100644 --- a/arkane/statmech.py +++ b/arkane/statmech.py @@ -273,7 +273,8 @@ def load(self, pdep=False): try: opticalIsomers = local_context['opticalIsomers'] except KeyError: - raise InputError('Required attribute "opticalIsomers" not found in species file {0!r}.'.format(path)) + logging.debug('No opticalIsomers provided, estimating them from the quantum file.') + opticalIsomers = None try: energy = local_context['energy'] From 1d39a9bf95cc603e3bb410c0fe34a83c9371fc03 Mon Sep 17 00:00:00 2001 From: Mark Goldman Date: Sun, 10 Feb 2019 21:20:46 -0500 Subject: [PATCH 3/6] Get loadConformer to automatically estimate optical isomers Prevously loadConformer required an integer input for optical isomers. This commit allows this method to call the Symmetry package and estimate the value automatically. --- arkane/gaussian.py | 7 ++++--- arkane/molpro.py | 6 ++++-- arkane/qchem.py | 6 ++++-- 3 files changed, 12 insertions(+), 7 deletions(-) diff --git a/arkane/gaussian.py b/arkane/gaussian.py index 399608a376..3edd615e7b 100644 --- a/arkane/gaussian.py +++ b/arkane/gaussian.py @@ -38,7 +38,7 @@ from rmgpy.exceptions import InputError from arkane.common import check_conformer_energy, get_element_mass -from arkane.statmech import Log +from arkane.log import Log ################################################################################ @@ -157,7 +157,7 @@ def loadGeometry(self): return coord, number, mass - def loadConformer(self, symmetry=None, spinMultiplicity=0, opticalIsomers=1, symfromlog=None, label=''): + def loadConformer(self, symmetry=None, spinMultiplicity=0, opticalIsomers=None, symfromlog=None, label=''): """ Load the molecular degree of freedom data from a log file created as the result of a Gaussian "Freq" quantum chemistry calculation. As @@ -252,7 +252,8 @@ def loadConformer(self, symmetry=None, spinMultiplicity=0, opticalIsomers=1, sym # Close file when finished f.close() - + if opticalIsomers is None: + opticalIsomers = self.get_optical_isomers_and_symmetry_number()[0] return Conformer(E0=(E0*0.001,"kJ/mol"), modes=modes, spinMultiplicity=spinMultiplicity, opticalIsomers=opticalIsomers), unscaled_frequencies diff --git a/arkane/molpro.py b/arkane/molpro.py index eae5b2f8b7..ac1a851a53 100644 --- a/arkane/molpro.py +++ b/arkane/molpro.py @@ -37,7 +37,7 @@ from rmgpy.statmech import IdealGasTranslation, NonlinearRotor, LinearRotor, HarmonicOscillator, Conformer from arkane.common import get_element_mass -from arkane.statmech import Log +from arkane.log import Log ################################################################################ @@ -167,7 +167,7 @@ def loadGeometry(self): return coord, number, mass - def loadConformer(self, symmetry=None, spinMultiplicity=0, opticalIsomers=1, symfromlog=None, label=''): + def loadConformer(self, symmetry=None, spinMultiplicity=0, opticalIsomers=None, symfromlog=None, label=''): """ Load the molecular degree of freedom data from a log file created as the result of a MolPro "Freq" quantum chemistry calculation with the thermo printed. @@ -266,6 +266,8 @@ def loadConformer(self, symmetry=None, spinMultiplicity=0, opticalIsomers=1, sym # Close file when finished f.close() + if opticalIsomers is None: + opticalIsomers = self.get_optical_isomers_and_symmetry_number()[0] return Conformer(E0=(E0*0.001,"kJ/mol"), modes=modes, spinMultiplicity=spinMultiplicity, opticalIsomers=opticalIsomers), unscaled_frequencies diff --git a/arkane/qchem.py b/arkane/qchem.py index d6062402e8..780b2662bb 100644 --- a/arkane/qchem.py +++ b/arkane/qchem.py @@ -38,7 +38,7 @@ from rmgpy.statmech import IdealGasTranslation, NonlinearRotor, LinearRotor, HarmonicOscillator, Conformer from arkane.common import check_conformer_energy, get_element_mass -from arkane.statmech import Log +from arkane.log import Log ################################################################################ @@ -167,7 +167,7 @@ def loadGeometry(self): return coord, number, mass - def loadConformer(self, symmetry=None, spinMultiplicity=0, opticalIsomers=1, symfromlog=None, label=''): + def loadConformer(self, symmetry=None, spinMultiplicity=0, opticalIsomers=None, symfromlog=None, label=''): """ Load the molecular degree of freedom data from an output file created as the result of a QChem "Freq" calculation. As QChem's guess of the external symmetry number is not always correct, @@ -262,6 +262,8 @@ def loadConformer(self, symmetry=None, spinMultiplicity=0, opticalIsomers=1, sym # Close file when finished f.close() + if opticalIsomers is None: + opticalIsomers = self.get_optical_isomers_and_symmetry_number()[0] modes = mmass + rot + freq return Conformer(E0=(E0*0.001,"kJ/mol"), modes=modes, spinMultiplicity=spinMultiplicity, opticalIsomers=opticalIsomers), unscaled_frequencies From 598440d693a9fd83668603e1b2384dbc0481b8c9 Mon Sep 17 00:00:00 2001 From: Mark Goldman Date: Mon, 11 Feb 2019 16:43:16 -0500 Subject: [PATCH 4/6] Get Arkane to use Symmetry package for symmetry/optical isomers Created new method get_optical_isomers_and_symmetry_number in Log class This method allows Log objects to calculate the number of optical isomers and symmetry numbers contained within a log file. This was taken and modified from the equivalent method in ARC (credits to Alon). Remove symfromlog parameter. If using the RMG QM symmetry package, the symfromlog parameter is not necessary and confusing. This commit removes the need for such a parameter. Get Arkane to use Symmetry package for symmetry. This commit uses the Symmetry package to estimate symmetry if not explicitly given by the user by modifying the gaussian, molpro, and qchem parsers. --- arkane/gaussian.py | 16 +++++++--------- arkane/gaussianTest.py | 6 +++--- arkane/log.py | 39 ++++++++++++++++++++++++++++++++++++++- arkane/molpro.py | 15 +++++++-------- arkane/molproTest.py | 2 +- arkane/qchem.py | 17 +++++++---------- arkane/qchemTest.py | 12 ++++++------ arkane/statmech.py | 5 ----- 8 files changed, 69 insertions(+), 43 deletions(-) diff --git a/arkane/gaussian.py b/arkane/gaussian.py index 3edd615e7b..129d7ece25 100644 --- a/arkane/gaussian.py +++ b/arkane/gaussian.py @@ -157,7 +157,7 @@ def loadGeometry(self): return coord, number, mass - def loadConformer(self, symmetry=None, spinMultiplicity=0, opticalIsomers=None, symfromlog=None, label=''): + def loadConformer(self, symmetry=None, spinMultiplicity=0, opticalIsomers=None, label=''): """ Load the molecular degree of freedom data from a log file created as the result of a Gaussian "Freq" quantum chemistry calculation. As @@ -171,7 +171,12 @@ def loadConformer(self, symmetry=None, spinMultiplicity=0, opticalIsomers=None, modes = [] unscaled_frequencies = [] E0 = 0.0 - + if opticalIsomers is None or symmetry is None: + _opticalIsomers, _symmetry = self.get_optical_isomers_and_symmetry_number() + if opticalIsomers is None: + opticalIsomers = _opticalIsomers + if symmetry is None: + symmetry = _symmetry f = open(self.path, 'r') line = f.readline() while line != '': @@ -198,11 +203,6 @@ def loadConformer(self, symmetry=None, spinMultiplicity=0, opticalIsomers=None, translation = IdealGasTranslation(mass=(mass,"amu")) modes.append(translation) - # Read Gaussian's estimate of the external symmetry number - elif 'Rotational symmetry number' in line and symmetry is None: - if symfromlog is True: - symmetry = int(float(line.split()[3])) - # Read moments of inertia for external rotational modes elif 'Rotational constants (GHZ):' in line: inertia = [float(d) for d in line.split()[-3:]] @@ -252,8 +252,6 @@ def loadConformer(self, symmetry=None, spinMultiplicity=0, opticalIsomers=None, # Close file when finished f.close() - if opticalIsomers is None: - opticalIsomers = self.get_optical_isomers_and_symmetry_number()[0] return Conformer(E0=(E0*0.001,"kJ/mol"), modes=modes, spinMultiplicity=spinMultiplicity, opticalIsomers=opticalIsomers), unscaled_frequencies diff --git a/arkane/gaussianTest.py b/arkane/gaussianTest.py index 9ab5fa52ff..20978c5386 100644 --- a/arkane/gaussianTest.py +++ b/arkane/gaussianTest.py @@ -54,7 +54,7 @@ def testLoadEthyleneFromGaussianLog_CBSQB3(self): """ log = GaussianLog(os.path.join(os.path.dirname(__file__),'data','ethylene.log')) - conformer, unscaled_frequencies = log.loadConformer(symfromlog=True) + conformer, unscaled_frequencies = log.loadConformer() E0 = log.loadEnergy() self.assertTrue(len([mode for mode in conformer.modes if isinstance(mode,IdealGasTranslation)]) == 1) @@ -81,7 +81,7 @@ def testLoadOxygenFromGaussianLog(self): """ log = GaussianLog(os.path.join(os.path.dirname(__file__),'data','oxygen.log')) - conformer, unscaled_frequencies = log.loadConformer(symfromlog=True) + conformer, unscaled_frequencies = log.loadConformer() E0 = log.loadEnergy() self.assertTrue(len([mode for mode in conformer.modes if isinstance(mode,IdealGasTranslation)]) == 1) @@ -109,7 +109,7 @@ def testLoadEthyleneFromGaussianLog_G3(self): """ log = GaussianLog(os.path.join(os.path.dirname(__file__),'data','ethylene_G3.log')) - conformer, unscaled_frequencies = log.loadConformer(symfromlog=True) + conformer, unscaled_frequencies = log.loadConformer() E0 = log.loadEnergy() self.assertTrue(len([mode for mode in conformer.modes if isinstance(mode,IdealGasTranslation)]) == 1) diff --git a/arkane/log.py b/arkane/log.py index 2df026a3fc..8c684f4366 100644 --- a/arkane/log.py +++ b/arkane/log.py @@ -3,6 +3,12 @@ """ A general class for parsing quantum mechanical log files """ +import os.path +import logging +import shutil + +from rmgpy.qm.qmdata import QMData +from rmgpy.qm.symmetry import PointGroupCalculator class Log(object): """ @@ -37,7 +43,7 @@ def loadGeometry(self): """ raise NotImplementedError("loadGeometry is not implemented for the Log class") - def loadConformer(self, symmetry=None, spinMultiplicity=0, opticalIsomers=None, symfromlog=None, label=''): + def loadConformer(self, symmetry=None, spinMultiplicity=0, opticalIsomers=None, label=''): """ Load the molecular degree of freedom data from an output file created as the result of a QChem "Freq" calculation. As QChem's guess of the external symmetry number is not always correct, @@ -74,3 +80,34 @@ def loadNegativeFrequency(self): """ raise NotImplementedError("loadGeometry is not implemented for the Log class") + def get_optical_isomers_and_symmetry_number(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. + """ + coordinates, atom_numbers, _ = self.loadGeometry() + unique_id = '0' # Just some name that the SYMMETRY code gives to one of its jobs + scr_dir = os.path.join(os.path.abspath('.'), str('scratch')) # Scratch directory that the SYMMETRY code writes its files in + if not os.path.exists(scr_dir): + os.makedirs(scr_dir) + try: + qmdata = QMData( + groundStateDegeneracy=1, # Only needed to check if valid QMData + numberOfAtoms=len(atom_numbers), + atomicNumbers=atom_numbers, + atomCoords=(coordinates, str('angstrom')), + energy=(0.0, str('kcal/mol')) # Only needed to avoid error + ) + settings = type(str(''), (), dict(symmetryPath=str('symmetry'), scratchDirectory=scr_dir))() # Creates anonymous class + pgc = PointGroupCalculator(settings, unique_id, qmdata) + pg = pgc.calculate() + if pg is not None: + optical_isomers = 2 if pg.chiral else 1 + symmetry = pg.symmetryNumber + logging.debug("Symmetry algorithm found {0} optical isomers and a symmetry number of {1}".format(optical_isomers,symmetry)) + else: + logging.error("Symmetry algorithm errored when computing point group\nfor log file located at{0}.\nManually provide values in Arkane input.".format(self.path)) + return optical_isomers, symmetry + finally: + shutil.rmtree(scr_dir) diff --git a/arkane/molpro.py b/arkane/molpro.py index ac1a851a53..f44e4f83ef 100644 --- a/arkane/molpro.py +++ b/arkane/molpro.py @@ -167,7 +167,7 @@ def loadGeometry(self): return coord, number, mass - def loadConformer(self, symmetry=None, spinMultiplicity=0, opticalIsomers=None, symfromlog=None, label=''): + def loadConformer(self, symmetry=None, spinMultiplicity=0, opticalIsomers=None, label=''): """ Load the molecular degree of freedom data from a log file created as the result of a MolPro "Freq" quantum chemistry calculation with the thermo printed. @@ -176,7 +176,12 @@ def loadConformer(self, symmetry=None, spinMultiplicity=0, opticalIsomers=None, modes = [] unscaled_frequencies = [] E0 = 0.0 - + if opticalIsomers is None or symmetry is None: + _opticalIsomers, _symmetry = self.get_optical_isomers_and_symmetry_number() + if opticalIsomers is None: + opticalIsomers = _opticalIsomers + if symmetry is None: + symmetry = _symmetry f = open(self.path, 'r') line = f.readline() while line != '': @@ -224,10 +229,6 @@ def loadConformer(self, symmetry=None, spinMultiplicity=0, opticalIsomers=None, mass = float(line.split()[2]) translation = IdealGasTranslation(mass=(mass,"amu")) modes.append(translation) - # Read MolPro's estimate of the external symmetry number - elif 'Rotational Symmetry factor' in line and symmetry is None: - if symfromlog is True: - symmetry = int(float(line.split()[3])) # Read moments of inertia for external rotational modes elif 'Rotational Constants' in line and line.split()[-1]=='[GHz]': @@ -266,8 +267,6 @@ def loadConformer(self, symmetry=None, spinMultiplicity=0, opticalIsomers=None, # Close file when finished f.close() - if opticalIsomers is None: - opticalIsomers = self.get_optical_isomers_and_symmetry_number()[0] return Conformer(E0=(E0*0.001,"kJ/mol"), modes=modes, spinMultiplicity=spinMultiplicity, opticalIsomers=opticalIsomers), unscaled_frequencies diff --git a/arkane/molproTest.py b/arkane/molproTest.py index 8ae438fc33..3407cdb3df 100644 --- a/arkane/molproTest.py +++ b/arkane/molproTest.py @@ -86,7 +86,7 @@ def testLoadHOSIFromMolpro_log(self): """ log = MolproLog(os.path.join(os.path.dirname(__file__),'data','HOSI_ccsd_t1.out')) - conformer, unscaled_frequencies = log.loadConformer(symfromlog=True, spinMultiplicity=1) + conformer, unscaled_frequencies = log.loadConformer(spinMultiplicity=1) E0 = log.loadEnergy() self.assertTrue(len([mode for mode in conformer.modes if isinstance(mode,IdealGasTranslation)]) == 1) diff --git a/arkane/qchem.py b/arkane/qchem.py index 780b2662bb..fca2991b68 100644 --- a/arkane/qchem.py +++ b/arkane/qchem.py @@ -167,7 +167,7 @@ def loadGeometry(self): return coord, number, mass - def loadConformer(self, symmetry=None, spinMultiplicity=0, opticalIsomers=None, symfromlog=None, label=''): + def loadConformer(self, symmetry=None, spinMultiplicity=0, opticalIsomers=None, label=''): """ Load the molecular degree of freedom data from an output file created as the result of a QChem "Freq" calculation. As QChem's guess of the external symmetry number is not always correct, @@ -177,6 +177,12 @@ def loadConformer(self, symmetry=None, spinMultiplicity=0, opticalIsomers=None, modes = []; freq = []; mmass = []; rot = []; inertia = [] unscaled_frequencies = [] E0 = 0.0 + if opticalIsomers is None or symmetry is None: + _opticalIsomers, _symmetry = self.get_optical_isomers_and_symmetry_number() + if opticalIsomers is None: + opticalIsomers = _opticalIsomers + if symmetry is None: + symmetry = _symmetry f = open(self.path, 'r') line = f.readline() while line != '': @@ -228,11 +234,6 @@ def loadConformer(self, symmetry=None, spinMultiplicity=0, opticalIsomers=None, elif 'Eigenvalues --' in line: inertia = [float(d) for d in line.split()[-3:]] - # Read QChem's estimate of the external rotational symmetry number, which may very well be incorrect - elif 'Rotational Symmetry Number is' in line and symfromlog: - symmetry = int(float(line.split()[-1])) - logging.debug('Rotational Symmetry read from QChem is {}'.format(str(symmetry))) - # Read the next line in the file line = f.readline() @@ -240,8 +241,6 @@ def loadConformer(self, symmetry=None, spinMultiplicity=0, opticalIsomers=None, line = f.readline() if len(inertia): - if symmetry is None: - symmetry = 1 if inertia[0] == 0.0: # If the first eigenvalue is 0, the rotor is linear inertia.remove(0.0) @@ -262,8 +261,6 @@ def loadConformer(self, symmetry=None, spinMultiplicity=0, opticalIsomers=None, # Close file when finished f.close() - if opticalIsomers is None: - opticalIsomers = self.get_optical_isomers_and_symmetry_number()[0] modes = mmass + rot + freq return Conformer(E0=(E0*0.001,"kJ/mol"), modes=modes, spinMultiplicity=spinMultiplicity, opticalIsomers=opticalIsomers), unscaled_frequencies diff --git a/arkane/qchemTest.py b/arkane/qchemTest.py index 316b633e7e..b4f56bd09e 100644 --- a/arkane/qchemTest.py +++ b/arkane/qchemTest.py @@ -74,11 +74,11 @@ def testLoadVibrationsFromQChemLog(self): molecular energies can be properly read. """ log = QChemLog(os.path.join(os.path.dirname(__file__),'data','npropyl.out')) - conformer, unscaled_frequencies = log.loadConformer(symfromlog=True) + conformer, unscaled_frequencies = log.loadConformer() self.assertEqual(len(conformer.modes[2]._frequencies.getValue()), 24) self.assertEqual(conformer.modes[2]._frequencies.getValue()[5], 881.79) log = QChemLog(os.path.join(os.path.dirname(__file__),'data','co.out')) - conformer, unscaled_frequencies = log.loadConformer(symfromlog=True) + conformer, unscaled_frequencies = log.loadConformer() self.assertEqual(len(conformer.modes[2]._frequencies.getValue()), 1) self.assertEqual(conformer.modes[2]._frequencies.getValue(), 2253.16) @@ -88,7 +88,7 @@ def testLoadNpropylModesFromQChemLog(self): molecular modes can be properly read. """ log = QChemLog(os.path.join(os.path.dirname(__file__),'data','npropyl.out')) - conformer, unscaled_frequencies = log.loadConformer(symfromlog=True) + conformer, unscaled_frequencies = log.loadConformer() self.assertTrue(len([mode for mode in conformer.modes if isinstance(mode,IdealGasTranslation)]) == 1) self.assertTrue(len([mode for mode in conformer.modes if isinstance(mode,NonlinearRotor)]) == 1) @@ -101,10 +101,10 @@ def testSpinMultiplicityFromQChemLog(self): molecular degrees of freedom can be properly read. """ log = QChemLog(os.path.join(os.path.dirname(__file__),'data','npropyl.out')) - conformer, unscaled_frequencies = log.loadConformer(symfromlog=True) + conformer, unscaled_frequencies = log.loadConformer() self.assertEqual(conformer.spinMultiplicity, 2) log = QChemLog(os.path.join(os.path.dirname(__file__),'data','co.out')) - conformer, unscaled_frequencies = log.loadConformer(symfromlog=True) + conformer, unscaled_frequencies = log.loadConformer() self.assertEqual(conformer.spinMultiplicity, 1) def testLoadCOModesFromQChemLog(self): @@ -113,7 +113,7 @@ def testLoadCOModesFromQChemLog(self): molecular degrees of freedom can be properly read. """ log = QChemLog(os.path.join(os.path.dirname(__file__),'data','co.out')) - conformer, unscaled_frequencies = log.loadConformer(symfromlog=True) + conformer, unscaled_frequencies = log.loadConformer() E0 = log.loadEnergy() self.assertTrue(len([mode for mode in conformer.modes if isinstance(mode,IdealGasTranslation)]) == 1) diff --git a/arkane/statmech.py b/arkane/statmech.py index 7c070dda1a..ec76426c00 100644 --- a/arkane/statmech.py +++ b/arkane/statmech.py @@ -253,17 +253,13 @@ def load(self, pdep=False): try: linear = local_context['linear'] - symfromlog = False except KeyError: externalSymmetry = None - symfromlog = True try: externalSymmetry = local_context['externalSymmetry'] - symfromlog = False except KeyError: externalSymmetry = None - symfromlog = True try: spinMultiplicity = local_context['spinMultiplicity'] @@ -364,7 +360,6 @@ def load(self, pdep=False): conformer, unscaled_frequencies = statmechLog.loadConformer(symmetry=externalSymmetry, spinMultiplicity=spinMultiplicity, opticalIsomers=opticalIsomers, - symfromlog=symfromlog, label=self.species.label) for mode in conformer.modes: if isinstance(mode, (LinearRotor, NonlinearRotor)): From 1bb5b0a4b7313fdfd08d6edfda6e50557b5357cf Mon Sep 17 00:00:00 2001 From: MarkGoldman Date: Wed, 20 Mar 2019 15:07:22 -0400 Subject: [PATCH 5/6] Test symmetry,optical isomers loaded --- arkane/gaussianTest.py | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/arkane/gaussianTest.py b/arkane/gaussianTest.py index 20978c5386..96a870f46c 100644 --- a/arkane/gaussianTest.py +++ b/arkane/gaussianTest.py @@ -37,6 +37,7 @@ from external.wip import work_in_progress from arkane.gaussian import GaussianLog +from arkane.statmech import determine_qm_software ################################################################################ @@ -130,5 +131,32 @@ def testLoadEthyleneFromGaussianLog_G3(self): self.assertEqual(conformer.spinMultiplicity, 1) self.assertEqual(conformer.opticalIsomers, 1) + def testLoadSymmetryAndOptics(self): + """ + Uses a Gaussian03 log file for oxygen (O2) to test that its + molecular degrees of freedom can be properly read. + """ + + log = GaussianLog(os.path.join(os.path.dirname(__file__),'data','oxygen.log')) + optical, symmetry = log.get_optical_isomers_and_symmetry_number() + self.assertEqual(optical,1) + self.assertEqual(symmetry,2) + + conf = log.loadConformer()[0] + self.assertEqual(conf.opticalIsomers, 1) + found_rotor = False + for mode in conf.modes: + if isinstance(mode,LinearRotor): + self.assertEqual(mode.symmetry,2) + found_rotor = True + self.assertTrue(found_rotor) + + def testDetermineQMSoftware(self): + """ + Ensures that determine_qm_software returns a GaussianLog object + """ + log = determine_qm_software(os.path.join(os.path.dirname(__file__),'data','oxygen.log')) + self.assertIsInstance(log,GaussianLog) + if __name__ == '__main__': unittest.main( testRunner = unittest.TextTestRunner(verbosity=2) ) From 009a23cd0084866f620106a69c6355a651a26a8b Mon Sep 17 00:00:00 2001 From: MarkGoldman Date: Mon, 8 Apr 2019 18:49:53 -0400 Subject: [PATCH 6/6] Require `linear` parameter for Arkane statmech jobs Previously, no error was raised when the `linear` parameter was not implemented properly. This could hide information about incorrect calculations if the user is not properly implementing the parameter. This commit raises an error if no `linear` parameter is given. --- arkane/statmech.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/arkane/statmech.py b/arkane/statmech.py index ec76426c00..6260e604b9 100644 --- a/arkane/statmech.py +++ b/arkane/statmech.py @@ -254,7 +254,8 @@ def load(self, pdep=False): try: linear = local_context['linear'] except KeyError: - externalSymmetry = None + logging.error('You did not set whether the molecule is linear with the required `linear` parameter') + raise try: externalSymmetry = local_context['externalSymmetry']