Skip to content

Commit

Permalink
Merge pull request #1402 from ReactionMechanismGenerator/read_spc
Browse files Browse the repository at this point in the history
Save and parse Arkane species with all statmech properties using YAML format
  • Loading branch information
alongd authored Jan 22, 2019
2 parents c61d2ef + 4c77d09 commit 3a7b901
Show file tree
Hide file tree
Showing 37 changed files with 1,030 additions and 124 deletions.
1 change: 1 addition & 0 deletions arkane/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,4 @@
from arkane.thermo import ThermoJob
from arkane.kinetics import KineticsJob
from arkane.pdep import PressureDependenceJob
from arkane.common import ArkaneSpecies
217 changes: 217 additions & 0 deletions arkane/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,229 @@
import numpy
import os.path
import logging
import time
import string

import yaml
try:
from yaml import CDumper as Dumper, CLoader as Loader, CSafeLoader as SafeLoader
except ImportError:
from yaml import Dumper, Loader, SafeLoader

from rmgpy.rmgobject import RMGObject
from rmgpy import __version__
from rmgpy.quantity import ScalarQuantity, ArrayQuantity
from rmgpy.molecule.element import elementList
from rmgpy.molecule.translator import toInChI, toInChIKey
from rmgpy.statmech.conformer import Conformer
from rmgpy.statmech.rotation import LinearRotor, NonlinearRotor, KRotor, SphericalTopRotor
from rmgpy.statmech.torsion import HinderedRotor, FreeRotor
from rmgpy.statmech.translation import IdealGasTranslation
from rmgpy.statmech.vibration import HarmonicOscillator
from rmgpy.pdep.collision import SingleExponentialDown
from rmgpy.transport import TransportData
from rmgpy.thermo import NASA, Wilhoit
import rmgpy.constants as constants

from arkane.pdep import PressureDependenceJob

################################################################################


class ArkaneSpecies(RMGObject):
"""
A class for archiving an Arkane species including its statmech data into .yml files
"""
def __init__(self, species=None, conformer=None, author='', level_of_theory='', model_chemistry='',
frequency_scale_factor=None, use_hindered_rotors=None, use_bond_corrections=None, atom_energies='',
chemkin_thermo_string='', smiles=None, adjacency_list=None, inchi=None, inchi_key=None, xyz=None,
molecular_weight=None, symmetry_number=None, transport_data=None, energy_transfer_model=None,
thermo=None, thermo_data=None, label=None, datetime=None, RMG_version=None):
if species is None and conformer is None:
# Expecting to get a `species` when generating the object within Arkane,
# or a `conformer` when parsing from YAML.
raise ValueError('No species or conformer was passed to the ArkaneSpecies object')
if conformer is not None:
self.conformer = conformer
if label is None and species is not None:
self.label = species.label
else:
self.label = label
self.author = author
self.level_of_theory = level_of_theory
self.model_chemistry = model_chemistry
self.frequency_scale_factor = frequency_scale_factor
self.use_hindered_rotors = use_hindered_rotors
self.use_bond_corrections = use_bond_corrections
self.atom_energies = atom_energies
self.chemkin_thermo_string = chemkin_thermo_string
self.smiles = smiles
self.adjacency_list = adjacency_list
self.inchi = inchi
self.inchi_key = inchi_key
self.xyz = xyz
self.molecular_weight = molecular_weight
self.symmetry_number = symmetry_number
self.transport_data = transport_data
self.energy_transfer_model = energy_transfer_model # check pdep flag
self.thermo = thermo
self.thermo_data = thermo_data
if species is not None:
self.update_species_attributes(species)
self.RMG_version = RMG_version if RMG_version is not None else __version__
self.datetime = datetime if datetime is not None else time.strftime("%Y-%m-%d %H:%M")

def __repr__(self):
"""
Return a string representation that can be used to reconstruct the object
"""
result = '{0!r}'.format(self.__class__.__name__)
result += '{'
for key, value in self.as_dict().iteritems():
if key != 'class':
result += '{0!r}: {1!r}'.format(str(key), str(value))
result += '}'
return result

def update_species_attributes(self, species=None):
"""
Update the object with a new species (while keeping non-species-dependent attributes unchanged)
"""
if species is None:
raise ValueError('No species was passed to ArkaneSpecies')
self.label = species.label
if species.molecule is not None and len(species.molecule) > 0:
self.smiles = species.molecule[0].toSMILES()
self.adjacency_list = species.molecule[0].toAdjacencyList()
try:
inchi = toInChI(species.molecule[0], backend='try-all', aug_level=0)
except ValueError:
inchi = ''
try:
inchi_key = toInChIKey(species.molecule[0], backend='try-all', aug_level=0)
except ValueError:
inchi_key = ''
self.inchi = inchi
self.inchi_key = inchi_key
if species.conformer is not None:
self.conformer = species.conformer
self.xyz = self.update_xyz_string()
self.molecular_weight = species.molecularWeight
if species.symmetryNumber != -1:
self.symmetry_number = species.symmetryNumber
if species.transportData is not None:
self.transport_data = species.transportData # called `collisionModel` in Arkane
if species.energyTransferModel is not None:
self.energy_transfer_model = species.energyTransferModel
if species.thermo is not None:
self.thermo = species.thermo.as_dict()
thermo_data = species.getThermoData()
h298 = thermo_data.getEnthalpy(298) / 4184.
s298 = thermo_data.getEntropy(298) / 4.184
cp = dict()
for t in [300,400,500,600,800,1000,1500,2000,2400]:
temp_str = '{0} K'.format(t)
cp[temp_str] = '{0:.2f}'.format(thermo_data.getHeatCapacity(t) / 4.184)
self.thermo_data = {'H298': '{0:.2f} kcal/mol'.format(h298),
'S298': '{0:.2f} cal/mol*K'.format(s298),
'Cp (cal/mol*K)': cp}

def update_xyz_string(self):
if self.conformer is not None and self.conformer.number is not None:
# generate the xyz-format string from the Conformer coordinates
xyz_string = '{0}\n{1}'.format(len(self.conformer.number.value_si), self.label)
for i, coorlist in enumerate(self.conformer.coordinates.value_si):
for element in elementList:
if element.number == int(self.conformer.number.value_si[i]):
element_symbol = element.symbol
break
else:
raise ValueError('Could not find element symbol corresponding to atom number {0}'.format(
self.conformer.number.value_si[i]))
xyz_string += '\n{0} {1} {2} {3}'.format(element_symbol,
coorlist[0],
coorlist[1],
coorlist[2])
else:
xyz_string = ''
return xyz_string

def save_yaml(self, path):
"""
Save the species with all statMech data to a .yml file
"""
if not os.path.exists(os.path.join(os.path.abspath(path),'ArkaneSpecies', '')):
os.mkdir(os.path.join(os.path.abspath(path),'ArkaneSpecies', ''))
valid_chars = "-_.()<=>+ %s%s" % (string.ascii_letters, string.digits)
filename = os.path.join('ArkaneSpecies',
''.join(c for c in self.label if c in valid_chars) + '.yml')
full_path = os.path.join(path, filename)
content = yaml.dump(data=self.as_dict(), Dumper=Dumper)
# remove empty lines from the file (multi-line strings have excess new line brakes for some reason):
content = content.replace('\n\n', '\n')
with open(full_path, 'w') as f:
f.write(content)
logging.debug('Dumping species {0} data as {1}'.format(self.label, filename))

def load_yaml(self, path, species, pdep=False):
"""
Load the all statMech data from the .yml file in `path` into `species`
`pdep` is a boolean specifying whether or not jobList includes a pressureDependentJob.
"""
logging.info('Loading statistical mechanics parameters for {0} from .yml file...'.format(species.label))
with open(path, 'r') as f:
data = yaml.safe_load(stream=f)
try:
if species.label != data['label']:
logging.warning('Found different labels for species: {0} in input file, and {1} in the .yml file. '
'Using the label "{0}" for this species.'.format(species.label, data['label']))
except KeyError:
# Lacking label in the YAML file is strange, but accepted
logging.debug('Did not find label for species {0} in .yml file.'.format(species.label))
try:
class_name = data['class']
except KeyError:
raise KeyError("Can only make objects if the `class` attribute in the dictionary is known")
if class_name != 'ArkaneSpecies':
raise KeyError("Expected a ArkaneSpecies object, but got {0}".format(class_name))
del data['class']
class_dict = {'ScalarQuantity': ScalarQuantity,
'ArrayQuantity': ArrayQuantity,
'Conformer': Conformer,
'LinearRotor': LinearRotor,
'NonlinearRotor': NonlinearRotor,
'KRotor': KRotor,
'SphericalTopRotor': SphericalTopRotor,
'HinderedRotor': HinderedRotor,
'FreeRotor': FreeRotor,
'IdealGasTranslation': IdealGasTranslation,
'HarmonicOscillator': HarmonicOscillator,
'TransportData': TransportData,
'SingleExponentialDown': SingleExponentialDown,
'Wilhoit': Wilhoit,
'NASA': NASA,
}
self.make_object(data=data, class_dict=class_dict)
if pdep and (self.transport_data is None or self.energy_transfer_model is None):
raise ValueError('Transport data and an energy transfer model must be given if pressure-dependent '
'calculations are requested. Check file {0}'.format(path))
if pdep and self.smiles is None and self.adjacency_list is None\
and self.inchi is None and self.molecular_weight is None:
raise ValueError('The molecular weight was not specified, and a structure was not given so it could '
'not be calculated. Specify either the molecular weight or structure if '
'pressure-dependent calculations are requested. Check file {0}'.format(path))
logging.debug("Parsed all YAML objects")

################################################################################


def is_pdep(jobList):
for job in jobList:
if isinstance(job, PressureDependenceJob):
return True
return False


def check_conformer_energy(Vlist,path):
"""
Check to see that the starting energy of the species in the potential energy scan calculation
Expand Down
104 changes: 103 additions & 1 deletion arkane/commonTest.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,13 +35,17 @@
import unittest
import numpy
import os
import shutil
import logging

import rmgpy
import rmgpy.constants as constants
from rmgpy.species import Species, TransitionState
from rmgpy.quantity import ScalarQuantity
from rmgpy.thermo import NASA

from arkane.common import get_element_mass
from arkane import Arkane, input
from arkane.common import ArkaneSpecies, get_element_mass
from arkane.statmech import InputError, StatMechJob
from arkane.input import jobList

Expand Down Expand Up @@ -270,6 +274,104 @@ def testTransitionStateStatmech(self):
job.load()


class TestStatmech(unittest.TestCase):
"""
Contains unit tests of statmech.py
"""
@classmethod
def setUp(self):
arkane = Arkane()
self.job_list = arkane.loadInputFile(os.path.join(os.path.dirname(os.path.abspath(__file__)),
'data', 'Benzyl', 'input.py'))

def test_gaussian_log_file_error(self):
"""Test that the proper error is raised if gaussian geometry and frequency file paths are the same"""
job = self.job_list[-2]
self.assertTrue(isinstance(job, StatMechJob))
with self.assertRaises(InputError):
job.load()


class TestArkaneSpecies(unittest.TestCase):
"""
Contains YAML dump and load unit tests for :class:ArkaneSpecies
"""

@classmethod
def setUpClass(cls):
"""
A method that is run ONCE before all unit tests in this class.
"""
cls.arkane = Arkane()
path = os.path.join(os.path.dirname(os.path.dirname(rmgpy.__file__)),
'examples', 'arkane', 'species')
cls.dump_path = os.path.join(path, 'C2H6')
cls.dump_input_path = os.path.join(cls.dump_path, 'input.py')
cls.dump_output_file = os.path.join(cls.dump_path, 'output.py')
cls.dump_yaml_file = os.path.join(cls.dump_path, 'ArkaneSpecies', 'C2H6.yml')

cls.load_path = os.path.join(path, 'C2H6_from_yaml')
cls.load_input_path = os.path.join(cls.load_path, 'input.py')
cls.load_output_file = os.path.join(cls.load_path, 'output.py')

if os.path.exists(cls.dump_yaml_file):
logging.debug('removing existing yaml file {0} before running tests'.format(cls.dump_yaml_file))
os.remove(cls.dump_yaml_file)

def test_dump_yaml(self):
"""
Test properly dumping the ArkaneSpecies object and respective sub-objects
"""
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))

def test_load_yaml(self):
"""
Test properly loading the ArkaneSpecies object and respective sub-objects
"""
jobList = self.arkane.loadInputFile(self.load_input_path)
for job in jobList:
job.execute(outputFile=self.load_output_file)
arkane_spc = jobList[0].arkane_species
self.assertIsInstance(arkane_spc, ArkaneSpecies) # checks make_object
self.assertIsInstance(arkane_spc.molecular_weight, ScalarQuantity)
self.assertIsInstance(arkane_spc.thermo, NASA)
self.assertNotEqual(arkane_spc.author, '')
self.assertEqual(arkane_spc.inchi, 'InChI=1S/C2H6/c1-2/h1-2H3')
self.assertEqual(arkane_spc.smiles, 'CC')
self.assertTrue('8 H u0 p0 c0 {2,S}' in arkane_spc.adjacency_list)
self.assertEqual(arkane_spc.label, 'C2H6')
self.assertEqual(arkane_spc.frequency_scale_factor, 0.99) # checks float conversion
self.assertFalse(arkane_spc.use_bond_corrections)
self.assertAlmostEqual(arkane_spc.conformer.modes[2].frequencies.value_si[0], 818.91718, 4) # HarmonicOsc.

@classmethod
def tearDownClass(cls):
"""
A method that is run ONCE after all unit tests in this class.
"""
path = os.path.join(os.path.dirname(os.path.dirname(rmgpy.__file__)),
'examples', 'arkane', 'species')
cls.dump_path = os.path.join(path, 'C2H6')
cls.load_path = os.path.join(path, 'C2H6_from_yaml')
cls.extensions_to_delete = ['pdf', 'txt', 'inp', 'csv']
cls.files_to_delete = ['arkane.log', 'output.py']
cls.files_to_keep = ['C2H6.yml']
for path in [cls.dump_path, cls.load_path]:
for name in os.listdir(path):
item_path = os.path.join(path, name)
if os.path.isfile(item_path):
extension = name.split('.')[-1]
if name in cls.files_to_delete or\
(extension in cls.extensions_to_delete and name not in cls.files_to_keep):
os.remove(item_path)
else:
# This is a sub-directory. remove.
shutil.rmtree(item_path)


class TestGetMass(unittest.TestCase):
"""
Contains unit tests of common.py
Expand Down
Loading

0 comments on commit 3a7b901

Please sign in to comment.