Skip to content

Commit

Permalink
Merge pull request #1607 from ReactionMechanismGenerator/arkane_outpu…
Browse files Browse the repository at this point in the history
…t_fixes

Arkane output fixes
  • Loading branch information
alongd authored Jul 27, 2019
2 parents adfa67b + 841c8a0 commit 1369f9e
Show file tree
Hide file tree
Showing 17 changed files with 527 additions and 185 deletions.
18 changes: 9 additions & 9 deletions arkane/commonTest.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""
Expand Down Expand Up @@ -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):
"""
Expand All @@ -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
Expand Down
38 changes: 32 additions & 6 deletions arkane/encorr/corr.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
"""

import rmgpy.constants as constants
import logging

from arkane.exceptions import AtomEnergyCorrectionError, BondAdditivityCorrectionError

Expand Down Expand Up @@ -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
Expand All @@ -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:
Expand Down Expand Up @@ -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
Expand Down
67 changes: 65 additions & 2 deletions arkane/gaussian.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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
Expand All @@ -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
2 changes: 1 addition & 1 deletion arkane/gaussianTest.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
91 changes: 58 additions & 33 deletions arkane/kinetics.py
Original file line number Diff line number Diff line change
Expand Up @@ -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))

Expand Down Expand Up @@ -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

Expand All @@ -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.
Expand Down Expand Up @@ -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"):
Expand All @@ -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:
Expand All @@ -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)
Expand All @@ -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,
Expand All @@ -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)
Expand Down
Loading

0 comments on commit 1369f9e

Please sign in to comment.