Skip to content

Commit 3f3703a

Browse files
committed
RMG-Py v2.2.1 release
2 parents 508291c + c80b028 commit 3f3703a

File tree

9 files changed

+314
-122
lines changed

9 files changed

+314
-122
lines changed

documentation/source/users/rmg/releaseNotes.rst

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,22 @@
44
Release Notes
55
*************
66

7+
RMG-Py Version 2.2.1
8+
====================
9+
Date July 23, 2018
10+
11+
This release is minor patch which fixes a number of issues discovered after 2.2.0.
12+
13+
- Collision limit checking:
14+
- RMG will now output a list of collision limit violations for the generated model
15+
16+
- Fixes:
17+
- Ambiguous chemical formulas in SMILES lookup leading to incorrect SMILES generation
18+
- Fixed issue with reading geometries from QChem output files
19+
- React flags for reaction filter were not properly updated on each iteration
20+
- Fixed issue with inconsistent symmetry number calculation
21+
22+
723
RMG-Py Version 2.2.0
824
====================
925
Date: July 5, 2018

rmgpy/cantherm/qchem.py

Lines changed: 39 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
import logging
3434
import os.path
3535
import rmgpy.constants as constants
36+
from rmgpy.exceptions import InputError
3637
from rmgpy.cantherm.common import checkConformerEnergy
3738
from rmgpy.statmech import IdealGasTranslation, NonlinearRotor, LinearRotor, HarmonicOscillator, Conformer
3839
################################################################################
@@ -115,35 +116,41 @@ def loadGeometry(self):
115116
"""
116117
atom = []; coord = []; number = [];
117118

118-
f = open(self.path, 'r')
119-
line = f.readline()
120-
while line != '':
121-
if 'Final energy is' in line:
122-
print 'found a sucessfully completed Qchem Geometry Optimization Job'
123-
line = f.readline()
124-
atom = []; coord = []
119+
120+
with open(self.path) as f:
121+
log = f.read().splitlines()
122+
123+
#First check that the Qchem job file (not necessarily a geometry optimization)
124+
#has successfully completed, if not an error is thrown
125+
completed_job = False
126+
for line in reversed(log):
127+
if 'Total job time:' in line:
128+
logging.debug('Found a sucessfully completed Qchem Job')
129+
completed_job = True
125130
break
126-
line = f.readline()
127-
found = 0
128-
while line != '':
131+
132+
if not completed_job:
133+
raise InputError('Could not find a successfully completed Qchem job in Qchem output file {0}'.format(self.path))
134+
135+
#Now look for the geometry.
136+
#Will return the final geometry in the file under Standard Nuclear Orientation.
137+
geometry_flag = False
138+
for i in reversed(xrange(len(log))):
139+
line = log[i]
129140
if 'Standard Nuclear Orientation' in line:
130-
found += 1
131-
for i in range(3): line = f.readline() # skip lines
132-
while '----------------------------------------------------' not in line:
133-
data = line.split()
134-
atom.append((data[1]))
135-
coord.append([float(data[2]), float(data[3]), float(data[4])])
136-
line = f.readline()
137-
# Read the next line in the file
138-
line = f.readline()
139-
# Read the next line in the file
140-
line = f.readline()
141-
if found ==1: break
142-
line = f.readline()
143-
#print coord
144-
f.close()
141+
atom, coord, number = [], [], []
142+
for line in log[(i+3):]:
143+
if '------------' not in line:
144+
data = line.split()
145+
atom.append(data[1])
146+
coord.append([float(c) for c in data [2:]])
147+
geometry_flag = True
148+
else:
149+
break
150+
if geometry_flag:
151+
break
152+
145153
coord = numpy.array(coord, numpy.float64)
146-
mass = numpy.array(coord, numpy.float64)
147154
# Assign appropriate mass to each atom in molecule
148155
# These values were taken from "Atomic Weights and Isotopic Compositions" v3.0 (July 2010) from NIST
149156

@@ -173,7 +180,9 @@ def loadGeometry(self):
173180
number.append('17')
174181
else:
175182
raise NotImplementedError('Atomic atom {0:d} not yet supported in loadGeometry().'.format(atom[i]))
176-
number = numpy.array(number, numpy.int)
183+
number = numpy.array(number, numpy.int)
184+
if len(number) == 0 or len(coord) == 0 or len(mass) == 0:
185+
raise InputError('Unable to read the numbers and types of atoms from Qchem output file {0}'.format(self.path))
177186
return coord, number, mass
178187

179188
def loadConformer(self, symmetry=None, spinMultiplicity=0, opticalIsomers=1, symfromlog=None, label=''):
@@ -301,7 +310,7 @@ def loadEnergy(self,frequencyScaleFactor=1.):
301310
if E0 is not None:
302311
return E0
303312
else:
304-
raise Exception('Unable to find energy in Qchem output file.')
313+
raise InputError('Unable to find energy in Qchem output file.')
305314

306315
def loadZeroPointEnergy(self,frequencyScaleFactor=1.):
307316
"""
@@ -331,7 +340,7 @@ def loadZeroPointEnergy(self,frequencyScaleFactor=1.):
331340
if ZPE is not None:
332341
return ZPE
333342
else:
334-
raise Exception('Unable to find zero-point energy in Qchem output file.')
343+
raise InputError('Unable to find zero-point energy in Qchem output file.')
335344

336345
def loadScanEnergies(self):
337346
"""
@@ -394,4 +403,4 @@ def loadNegativeFrequency(self):
394403
if frequency < 0:
395404
return frequency
396405
else:
397-
raise Exception('Unable to find imaginary frequency in QChem output file {0}'.format(self.path))
406+
raise InputError('Unable to find imaginary frequency in QChem output file {0}'.format(self.path))

rmgpy/data/thermoTest.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -424,7 +424,6 @@ def testNewThermoGeneration(self):
424424
self.assertAlmostEqual(Cp, thermoData.getHeatCapacity(T) / 4.184, places=1,
425425
msg="Cp{3} error for {0}. Expected {1} but calculated {2}.".format(smiles, Cp, thermoData.getHeatCapacity(T) / 4.184, T))
426426

427-
@work_in_progress
428427
def testSymmetryNumberGeneration(self):
429428
"""
430429
Test we generate symmetry numbers correctly.

rmgpy/molecule/vf2.pyx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,14 @@ cdef class VF2:
166166
graph1.restore_vertex_order()
167167
graph2.restore_vertex_order()
168168

169+
# We're done, so clear the mappings to prevent downstream effects
170+
for vertex1 in graph1.vertices:
171+
vertex1.mapping = None
172+
vertex1.terminal = False
173+
for vertex2 in graph2.vertices:
174+
vertex2.mapping = None
175+
vertex2.terminal = False
176+
169177
cdef bint match(self, int callDepth) except -2:
170178
"""
171179
Recursively search for pairs of vertices to match, until all vertices

rmgpy/molecule/vf2Test.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,15 @@ def test_feasible(self):
8181
self.assertTrue(self.vf2.feasible(atom1, atom2))
8282
else: # different connectivity values should return false
8383
self.assertFalse(self.vf2.feasible(atom1, atom2))
84+
85+
def test_clear_mapping(self):
86+
"""Test that vertex mapping is cleared after isomorphism."""
87+
self.vf2.isIsomorphic(self.mol, self.mol2, None)
88+
89+
for atom in self.mol.atoms:
90+
self.assertIsNone(atom.mapping)
91+
self.assertFalse(atom.terminal)
92+
8493
################################################################################
8594

8695
if __name__ == '__main__':

rmgpy/reaction.pxd

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,4 +113,12 @@ cdef class Reaction:
113113

114114
cpdef ensure_species(self, bint reactant_resonance=?, bint product_resonance=?)
115115

116+
cpdef list check_collision_limit_violation(self, float t_min, float t_max, float p_min, float p_max)
117+
118+
cpdef calculate_coll_limit(self, float temp, bint reverse=?)
119+
120+
cpdef get_reduced_mass(self, bint reverse=?)
121+
122+
cpdef get_mean_sigma_and_epsilon(self, bint reverse=?)
123+
116124
cpdef bint isomorphic_species_lists(list list1, list list2, bint check_identical=?, bint only_check_label=?)

rmgpy/reaction.py

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1146,6 +1146,106 @@ def ensure_species(self, reactant_resonance=False, product_resonance=True):
11461146
except AttributeError:
11471147
pass
11481148

1149+
def check_collision_limit_violation(self, t_min, t_max, p_min, p_max):
1150+
"""
1151+
Warn if a core reaction violates the collision limit rate in either the forward or reverse direction
1152+
at the relevant extreme T/P conditions. Assuming a monotonic behaviour of the kinetics.
1153+
Returns a list with the reaction object and the direction in which the violation was detected.
1154+
"""
1155+
conditions = [[t_min, p_min]]
1156+
if t_min != t_max:
1157+
conditions.append([t_max, p_min])
1158+
if self.kinetics.isPressureDependent() and p_max != p_min:
1159+
conditions.append([t_min, p_max])
1160+
if t_min != t_max:
1161+
conditions.append([t_max, p_max])
1162+
logging.debug("Checking whether reaction {0} violates the collision rate limit...".format(self))
1163+
violator_list = []
1164+
kf_list = []
1165+
kr_list = []
1166+
collision_limit_f = []
1167+
collision_limit_r = []
1168+
for condition in conditions:
1169+
if len(self.reactants) >= 2:
1170+
try:
1171+
collision_limit_f.append(self.calculate_coll_limit(temp=condition[0], reverse=False))
1172+
except ValueError:
1173+
continue
1174+
else:
1175+
kf_list.append(self.getRateCoefficient(condition[0], condition[1]))
1176+
if len(self.products) >= 2:
1177+
try:
1178+
collision_limit_r.append(self.calculate_coll_limit(temp=condition[0], reverse=True))
1179+
except ValueError:
1180+
continue
1181+
else:
1182+
kr_list.append(self.generateReverseRateCoefficient().getRateCoefficient(condition[0], condition[1]))
1183+
if len(self.reactants) >= 2:
1184+
for i, k in enumerate(kf_list):
1185+
if k > collision_limit_f[i]:
1186+
ratio = k / collision_limit_f[i]
1187+
condition = '{0} K, {1:.1f} bar'.format(conditions[i][0], conditions[i][1]/1e5)
1188+
violator_list.append([self, 'forward', ratio, condition])
1189+
if len(self.products) >= 2:
1190+
for i, k in enumerate(kr_list):
1191+
if k > collision_limit_r[i]:
1192+
ratio = k / collision_limit_r[i]
1193+
condition = '{0} K, {1:.1f} bar'.format(conditions[i][0], conditions[i][1]/1e5)
1194+
violator_list.append([self, 'reverse', ratio, condition])
1195+
return violator_list
1196+
1197+
def calculate_coll_limit(self, temp, reverse=False):
1198+
"""
1199+
Calculate the collision limit rate for the given temperature
1200+
implemented as recommended in Wang et al. doi 10.1016/j.combustflame.2017.08.005 (Eq. 1)
1201+
"""
1202+
reduced_mass = self.get_reduced_mass(reverse)
1203+
sigma, epsilon = self.get_mean_sigma_and_epsilon(reverse)
1204+
Tr = temp * constants.kB * constants.Na / epsilon
1205+
reduced_coll_integral = 1.16145 * Tr ** (-0.14874) + 0.52487 * math.exp(-0.7732 * Tr) + 2.16178 * math.exp(
1206+
-2.437887 * Tr)
1207+
k_coll = (math.sqrt(8 * math.pi * constants.kB * temp * constants.Na / reduced_mass) * sigma ** 2
1208+
* reduced_coll_integral * constants.Na)
1209+
return k_coll
1210+
1211+
def get_reduced_mass(self, reverse=False):
1212+
"""
1213+
Returns the reduced mass of the reactants if reverse is ``False``
1214+
Returns the reduced mass of the products if reverse is ``True``
1215+
"""
1216+
if reverse:
1217+
mass_list = [spc.molecule[0].getMolecularWeight() for spc in self.products]
1218+
else:
1219+
mass_list = [spc.molecule[0].getMolecularWeight() for spc in self.reactants]
1220+
reduced_mass = reduce((lambda x, y: x * y), mass_list) / sum(mass_list)
1221+
return reduced_mass
1222+
1223+
def get_mean_sigma_and_epsilon(self, reverse=False):
1224+
"""
1225+
Calculates the collision diameter (sigma) using an arithmetic mean
1226+
Calculates the well depth (epsilon) using a geometric mean
1227+
If reverse is ``False`` the above is calculated for the reactants, otherwise for the products
1228+
"""
1229+
sigmas = []
1230+
epsilons = []
1231+
if reverse:
1232+
for spc in self.products:
1233+
trans = spc.getTransportData()
1234+
sigmas.append(trans.sigma.value_si)
1235+
epsilons.append(trans.epsilon.value_si)
1236+
num_of_spcs = len(self.products)
1237+
else:
1238+
for spc in self.reactants:
1239+
trans = spc.getTransportData()
1240+
sigmas.append(trans.sigma.value_si)
1241+
epsilons.append(trans.epsilon.value_si)
1242+
num_of_spcs = len(self.reactants)
1243+
if any([x == 0 for x in sigmas + epsilons]):
1244+
raise ValueError
1245+
mean_sigmas = sum(sigmas) / num_of_spcs
1246+
mean_epsilons = reduce((lambda x, y: x * y), epsilons) ** (1 / len(epsilons))
1247+
return mean_sigmas, mean_epsilons
1248+
11491249
def isomorphic_species_lists(list1, list2, check_identical=False, only_check_label=False):
11501250
"""
11511251
This method compares whether lists of species or molecules are isomorphic

0 commit comments

Comments
 (0)