diff --git a/rmgpy/chemkin.pyx b/rmgpy/chemkin.pyx index 5a626255fa..f162731d56 100644 --- a/rmgpy/chemkin.pyx +++ b/rmgpy/chemkin.pyx @@ -1649,28 +1649,30 @@ def writeKineticsEntry(reaction, speciesList, verbose = True, javaLibrary = Fals if isinstance(kinetics, _kinetics.Arrhenius): string += '{0:<9.6e} {1:<9.3f} {2:<9.3f}'.format( - kinetics.A.value_si/ (kinetics.T0.value_si ** kinetics.n.value_si) * 1.0e6 ** (numReactants - 1), + kinetics.A.value_si / (kinetics.T0.value_si ** kinetics.n.value_si) * kinetics.A.getConversionFactorFromSItoCmMolS(), kinetics.n.value_si, kinetics.Ea.value_si / 4184. ) elif isinstance(kinetics, (_kinetics.Lindemann, _kinetics.Troe)): arrhenius = kinetics.arrheniusHigh string += '{0:<9.3e} {1:<9.3f} {2:<9.3f}'.format( - arrhenius.A.value_si / (arrhenius.T0.value_si ** arrhenius.n.value_si) * 1.0e6 ** (numReactants - 1), + arrhenius.A.value_si / (arrhenius.T0.value_si ** arrhenius.n.value_si) * arrhenius.A.getConversionFactorFromSItoCmMolS(), arrhenius.n.value_si, arrhenius.Ea.value_si / 4184. ) elif isinstance(kinetics, _kinetics.ThirdBody): arrhenius = kinetics.arrheniusLow + assert 0.999 < arrhenius.A.getConversionFactorFromSItoCmMolS() / 1.0e6 ** (numReactants) < 1.001 # debugging; for gas phase only string += '{0:<9.3e} {1:<9.3f} {2:<9.3f}'.format( - arrhenius.A.value_si / (arrhenius.T0.value_si ** arrhenius.n.value_si) * 1.0e6 ** (numReactants), + arrhenius.A.value_si / (arrhenius.T0.value_si ** arrhenius.n.value_si) * arrhenius.A.getConversionFactorFromSItoCmMolS(), arrhenius.n.value_si, arrhenius.Ea.value_si / 4184. ) elif hasattr(kinetics,'highPlimit') and kinetics.highPlimit is not None: arrhenius = kinetics.highPlimit + assert 0.999 < arrhenius.A.getConversionFactorFromSItoCmMolS() / 1.0e6 ** (numReactants - 1) < 1.001 # debugging; for gas phase only string += '{0:<9.3e} {1:<9.3f} {2:<9.3f}'.format( - arrhenius.A.value_si / (arrhenius.T0.value_si ** arrhenius.n.value_si) * 1.0e6 ** (numReactants - 1), + arrhenius.A.value_si / (arrhenius.T0.value_si ** arrhenius.n.value_si) * arrhenius.A.getConversionFactorFromSItoCmMolS(), arrhenius.n.value_si, arrhenius.Ea.value_si / 4184. ) @@ -1699,8 +1701,9 @@ def writeKineticsEntry(reaction, speciesList, verbose = True, javaLibrary = Fals if isinstance(kinetics, (_kinetics.Lindemann, _kinetics.Troe)): # Write low-P kinetics arrhenius = kinetics.arrheniusLow + assert 0.999 < arrhenius.A.getConversionFactorFromSItoCmMolS() / 1.0e6 ** (numReactants) < 1.001 # debugging; for gas phase only string += ' LOW/ {0:<9.3e} {1:<9.3f} {2:<9.3f}/\n'.format( - arrhenius.A.value_si / (arrhenius.T0.value_si ** arrhenius.n.value_si) * 1.0e6 ** (numReactants), + arrhenius.A.value_si / (arrhenius.T0.value_si ** arrhenius.n.value_si) * arrhenius.A.getConversionFactorFromSItoCmMolS(), arrhenius.n.value_si, arrhenius.Ea.value_si / 4184. ) @@ -1714,14 +1717,16 @@ def writeKineticsEntry(reaction, speciesList, verbose = True, javaLibrary = Fals for P, arrhenius in zip(kinetics.pressures.value_si, kinetics.arrhenius): if isinstance(arrhenius, _kinetics.MultiArrhenius): for arrh in arrhenius.arrhenius: + assert 0.999 < arrh.A.getConversionFactorFromSItoCmMolS() / 1.0e6 ** (numReactants - 1) < 1.001 # debugging; for gas phase only string += ' PLOG/ {0:<9.6f} {1:<9.3e} {2:<9.3f} {3:<9.3f}/\n'.format(P / 101325., - arrh.A.value_si / (arrh.T0.value_si ** arrh.n.value_si) * 1.0e6 ** (numReactants - 1), + arrh.A.value_si / (arrh.T0.value_si ** arrh.n.value_si) * arrh.A.getConversionFactorFromSItoCmMolS(), arrh.n.value_si, arrh.Ea.value_si / 4184. ) else: - string += ' PLOG/ {0:<9.3f} {1:<9.3e} {2:<9.3f} {3:<9.3f}/\n'.format(P / 101325., - arrhenius.A.value_si / (arrhenius.T0.value_si ** arrhenius.n.value_si) * 1.0e6 ** (numReactants - 1), + assert 0.999 < arrhenius.A.getConversionFactorFromSItoCmMolS() / 1.0e6 ** (numReactants - 1) < 1.001 # debugging; for gas phase only + string += ' PLOG/ {0:<9.6f} {1:<9.3e} {2:<9.3f} {3:<9.3f}/\n'.format(P / 101325., + arrhenius.A.value_si / (arrhenius.T0.value_si ** arrhenius.n.value_si) * arrhenius.A.getConversionFactorFromSItoCmMolS(), arrhenius.n.value_si, arrhenius.Ea.value_si / 4184. ) @@ -1742,7 +1747,7 @@ def writeKineticsEntry(reaction, speciesList, verbose = True, javaLibrary = Fals for i in range(kinetics.degreeT): for j in range(kinetics.degreeP): coeffs.append(kinetics.coeffs.value_si[i,j]) - coeffs[0] += 6 * (numReactants - 1) + coeffs[0] += 6 * (numReactants - 1) # bypassing the Units.getConversionFactorFromSItoCmMolS() because it's in log10 space? for i in range(len(coeffs)): if i % 5 == 0: string += ' CHEB/' string += ' {0:<12.3e}'.format(coeffs[i]) @@ -1990,31 +1995,23 @@ def saveChemkin(reactionModel, path, verbose_path, dictionaryPath=None, transpor """ Save a Chemkin file for the current model as well as any desired output species and reactions to `path`. If `saveEdgeSpecies` is True, then - a chemkin file and dictionary file for the core and edge species and reactions - will be saved. + a chemkin file and dictionary file for the core AND edge species and reactions + will be saved. It also saves verbose versions of each file. """ - - if saveEdgeSpecies == False: - speciesList = reactionModel.core.species + reactionModel.outputSpeciesList - rxnList = reactionModel.core.reactions + reactionModel.outputReactionList - saveChemkinFile(path, speciesList, rxnList, verbose = False, checkForDuplicates=False) # We should already have marked everything as duplicates by now - logging.info('Saving current model to verbose Chemkin file...') - saveChemkinFile(verbose_path, speciesList, rxnList, verbose = True, checkForDuplicates=False) - if dictionaryPath: - saveSpeciesDictionary(dictionaryPath, speciesList) - if transportPath: - saveTransportFile(transportPath, speciesList) - - else: + if saveEdgeSpecies: speciesList = reactionModel.core.species + reactionModel.edge.species rxnList = reactionModel.core.reactions + reactionModel.edge.reactions - saveChemkinFile(path, speciesList, rxnList, verbose = False, checkForDuplicates=False) - logging.info('Saving current core and edge to verbose Chemkin file...') - saveChemkinFile(verbose_path, speciesList, rxnList, verbose = True, checkForDuplicates=False) - if dictionaryPath: - saveSpeciesDictionary(dictionaryPath, speciesList) - if transportPath: - saveTransportFile(transportPath, speciesList) + else: + speciesList = reactionModel.core.species + reactionModel.outputSpeciesList + rxnList = reactionModel.core.reactions + reactionModel.outputReactionList + + saveChemkinFile(path, speciesList, rxnList, verbose = False, checkForDuplicates=False) # We should already have marked everything as duplicates by now + logging.info('Saving verbose version of Chemkin file...') + saveChemkinFile(verbose_path, speciesList, rxnList, verbose=True, checkForDuplicates=False) + if dictionaryPath: + saveSpeciesDictionary(dictionaryPath, speciesList) + if transportPath: + saveTransportFile(transportPath, speciesList) def saveChemkinFiles(rmg): """ diff --git a/rmgpy/data/kinetics/database.py b/rmgpy/data/kinetics/database.py index 815338f0ad..331a2dacf3 100644 --- a/rmgpy/data/kinetics/database.py +++ b/rmgpy/data/kinetics/database.py @@ -207,7 +207,11 @@ def loadFamilies(self, path, families=None, depositories=None): for label in selected_families: familyPath = os.path.join(path, label) family = KineticsFamily(label=label) - family.load(familyPath, self.local_context, self.global_context, depositoryLabels=depositories) + try: + family.load(familyPath, self.local_context, self.global_context, depositoryLabels=depositories) + except: + logging.error("Error when loading reaction family {!r}".format(familyPath)) + raise self.families[label] = family def loadLibraries(self, path, libraries=None): @@ -532,7 +536,12 @@ def react_molecules(self, molecules, products=None, only_families=None, prod_res reaction_list = [] for label, family in self.families.iteritems(): if only_families is None or label in only_families: - reaction_list.extend(family.generateReactions(molecules, products=products, prod_resonance=prod_resonance)) + try: + reaction_list.extend(family.generateReactions(molecules, products=products, prod_resonance=prod_resonance)) + except: + logging.error("Problem family: {}".format(label)) + logging.error("Problem reactants: {}".format(molecules)) + raise for reactant in molecules: reactant.clearLabeledAtoms() diff --git a/rmgpy/data/thermo.py b/rmgpy/data/thermo.py index 0e13e4bd94..d99e736414 100644 --- a/rmgpy/data/thermo.py +++ b/rmgpy/data/thermo.py @@ -1998,7 +1998,7 @@ def __addGroupThermoData(self, thermoData, database, molecule, atom): """ node0 = database.descendTree(molecule, atom, None) if node0 is None: - raise KeyError('Node not found in database.') + raise KeyError('Node not found in thermo database for atom {0} in molecule {1}.'.format(atom, molecule)) # It's possible (and allowed) that items in the tree may not be in the # library, in which case we need to fall up the tree until we find an diff --git a/rmgpy/molecule/atomtype.py b/rmgpy/molecule/atomtype.py index a921f6f691..64f67cab26 100644 --- a/rmgpy/molecule/atomtype.py +++ b/rmgpy/molecule/atomtype.py @@ -224,8 +224,8 @@ def getFeatures(self): The atomTypes naming convention is: For example: -- N3d is nitrogen with valence=3 (i.e., 3 electronce are able to form bonds or remain as radicals) with one double bond -- S2tc is a charged sulful with valence=2 with a triple bonds +- N3d is nitrogen with valence=3 (i.e., 3 electrons are able to form bonds or remain as radicals) with one double bond +- S2tc is a charged sulfur with valence=2 with a triple bonds - Oa is atomic oxygen, i.e., a closed shell atom Some charged atom types were merged together, and are marked as '*Composite atomType' """ @@ -626,9 +626,9 @@ def getFeatures(self): atomTypes['F' ].setActions(incrementBond=[], decrementBond=[], formBond=['F'], breakBond=['F'], incrementRadical=['F'], decrementRadical=['F'], incrementLonePair=[], decrementLonePair=[]) atomTypes['F1s'].setActions(incrementBond=[], decrementBond=[], formBond=['F1s'], breakBond=['F1s'], incrementRadical=['F1s'], decrementRadical=['F1s'], incrementLonePair=[], decrementLonePair=[]) -#list of elements that do not have more specific atomTypes #these are ordered on priority of picking if we encounter a more general atomType for make allElements=['H', 'C', 'O', 'N', 'S', 'Si', 'Cl', 'Ne', 'Ar', 'He',] +#list of elements that do not have more specific atomTypes nonSpecifics=['H', 'He', 'Ne', 'Ar',] for atomType in atomTypes.values(): diff --git a/rmgpy/molecule/draw.py b/rmgpy/molecule/draw.py index a04fc022d6..99c5f3a41f 100644 --- a/rmgpy/molecule/draw.py +++ b/rmgpy/molecule/draw.py @@ -307,27 +307,31 @@ def __generateCoordinates(self): """ atoms = self.molecule.atoms Natoms = len(atoms) - flag_charge = 0 - for atom in self.molecule.atoms: - if atom.charge != 0: - flag_charge = 1 - break + # Initialize array of coordinates self.coordinates = coordinates = numpy.zeros((Natoms, 2)) - - if flag_charge == 1: - # If there are only one or two atoms to draw, then determining the - # coordinates is trivial - if Natoms == 1: - self.coordinates[0,:] = [0.0, 0.0] - return self.coordinates - elif Natoms == 2: - self.coordinates[0,:] = [-0.5, 0.0] - self.coordinates[1,:] = [0.5, 0.0] - return self.coordinates - + + # If there are only one or two atoms to draw, then determining the + # coordinates is trivial + if Natoms == 1: + self.coordinates[0, :] = [0.0, 0.0] + return self.coordinates + elif Natoms == 2: + self.coordinates[0, :] = [-0.5, 0.0] + self.coordinates[1, :] = [0.5, 0.0] + return self.coordinates + + # Decide whether we can use RDKit or have to generate coordinates ourselves + for atom in self.molecule.atoms: + if atom.charge != 0: + useRDKit = False + break + else: # didn't break + useRDKit = True + + if not useRDKit: if len(self.cycles) > 0: # Cyclic molecule backbone = self.__findCyclicBackbone() @@ -350,7 +354,8 @@ def __generateCoordinates(self): else: angle = math.atan2(vector0[0], vector0[1]) - math.pi / 2 rot = numpy.array([[math.cos(angle), math.sin(angle)], [-math.sin(angle), math.cos(angle)]], numpy.float64) - coordinates = numpy.dot(coordinates, rot) + # need to keep self.coordinates and coordinates referring to the same object + self.coordinates = coordinates = numpy.dot(coordinates, rot) # Center backbone at origin xmin = numpy.min(coordinates[:,0]) @@ -375,8 +380,7 @@ def __generateCoordinates(self): return coordinates else: - - # Use rdkit 2D coordinate generation: + # Use RDKit 2D coordinate generation: # Generate the RDkit molecule from the RDkit molecule, use geometry # in order to match the atoms in the rdmol with the atoms in the @@ -390,7 +394,7 @@ def __generateCoordinates(self): for atom in atoms: index = rdAtomIdx[atom] point = rdmol.GetConformer(0).GetAtomPosition(index) - coordinates[index,:]= [point.x*0.6, point.y*0.6] + coordinates[index,:] = [point.x*0.6, point.y*0.6] # RDKit generates some molecules more vertically than horizontally, # Especially linear ones. This will reflect any molecule taller than diff --git a/rmgpy/molecule/graphTest.py b/rmgpy/molecule/graphTest.py index 659af19f48..266b3b35fe 100644 --- a/rmgpy/molecule/graphTest.py +++ b/rmgpy/molecule/graphTest.py @@ -427,6 +427,47 @@ def test_isomorphism(self): self.assertTrue(graph2.isIsomorphic(graph1)) self.assertTrue(graph2.isSubgraphIsomorphic(graph1)) + + def test_isomorphism_disconnected(self): + """ + Check the graph isomorphism for broken graphs. + + This tries to match graphs with a missing bond, + eg. [ 0-1-2-3-4 5 ] should match [ 0-1-2-3-4 5 ] + """ + + vertices1 = [Vertex() for i in range(6)] + edges1 = [ + Edge(vertices1[0], vertices1[1]), + Edge(vertices1[1], vertices1[2]), + Edge(vertices1[2], vertices1[3]), + Edge(vertices1[3], vertices1[4]), + #Edge(vertices1[4], vertices1[5]), + ] + + vertices2 = [Vertex() for i in range(6)] + edges2 = [ + Edge(vertices2[0], vertices2[1]), + Edge(vertices2[1], vertices2[2]), + Edge(vertices2[2], vertices2[3]), + Edge(vertices2[3], vertices2[4]), + #Edge(vertices2[4], vertices2[5]), + ] + + graph1 = Graph() + for vertex in vertices1: graph1.addVertex(vertex) + for edge in edges1: graph1.addEdge(edge) + + graph2 = Graph() + for vertex in vertices2: graph2.addVertex(vertex) + for edge in edges2: graph2.addEdge(edge) + + self.assertTrue(graph1.isIsomorphic(graph2)) + self.assertTrue(graph1.isSubgraphIsomorphic(graph2)) + self.assertTrue(graph2.isIsomorphic(graph1)) + self.assertTrue(graph2.isSubgraphIsomorphic(graph1)) + self.assertTrue(len(graph1.findSubgraphIsomorphisms(graph2)) > 0) + def test_subgraphIsomorphism(self): """ Check the subgraph isomorphism functions. diff --git a/rmgpy/molecule/vf2.pyx b/rmgpy/molecule/vf2.pyx index 9348e5ff1b..41ba85f419 100644 --- a/rmgpy/molecule/vf2.pyx +++ b/rmgpy/molecule/vf2.pyx @@ -27,7 +27,7 @@ """ This module contains graph ismorphism functions that implement the VF2 -algorithm of Vento and Foggia. +algorithm of Vento and Foggia. http://dx.doi.org/10.1109/TPAMI.2004.75 """ cimport cython @@ -203,6 +203,17 @@ cdef class VF2: return True # Create list of pairs of candidates for inclusion in mapping + """ + 10.1109/TPAMI.2004.75 says: + "The set P(s) will be made of all the node pairs (n,m), + with n belonging to T1out(s) and m to T2out(s), + unless one of these two sets is empty. In this case, + the set P(s) is likewise obtained by considering + T1in(s) and T2in(s), respectively." + + But: for us, bonds are not directional, so ignore Tin(s) + and just use Tout(s) which is what we call "terminals". + """ hasTerminals = False for vertex2 in self.graph2.vertices: if vertex2.ignore: @@ -212,14 +223,32 @@ cdef class VF2: hasTerminals = True break else: - vertex2 = self.graph2.vertices[0] - + """ + "In presence of not connected graphs, for some state s, + all of the above sets may be empty. In this case, + the set of candidate pairs making up P(s) will be + the set Pd(s) of all the pairs of nodes not contained + neither in G1(s) nor in G2(s)." + + So: use nodes not yet mapped. + """ + # Take first unmapped vertex + for vertex2 in self.graph2.vertices: + if vertex2.mapping is None: + break + else: + raise VF2Error("Still seeking candidate pairs but all nodes in graph2 are already mapped.") + for vertex1 in self.graph1.vertices: if vertex1.ignore: continue # If terminals are available, then skip vertices in the first # graph that are not terminals - if hasTerminals and not vertex1.terminal: continue + if hasTerminals and not vertex1.terminal: + continue + # Otherwise take any node that is not already matched + if vertex1.mapping is not None: + continue # Propose a pairing if self.feasible(vertex1, vertex2): # Add proposed match to mapping @@ -230,7 +259,7 @@ cdef class VF2: return True # Undo proposed match self.removeFromMapping(vertex1, vertex2) - + # None of the proposed matches led to a complete isomorphism, so return False return False diff --git a/rmgpy/quantity.pxd b/rmgpy/quantity.pxd index ff1b4adb90..4cce90e666 100644 --- a/rmgpy/quantity.pxd +++ b/rmgpy/quantity.pxd @@ -38,6 +38,8 @@ cdef class Units(object): cpdef double getConversionFactorToSI(self) except -1 cpdef double getConversionFactorFromSI(self) except -1 + + cpdef double getConversionFactorFromSItoCmMolS(self) except -1 ################################################################################ diff --git a/rmgpy/quantity.py b/rmgpy/quantity.py index 73cfe87b94..651cf6a967 100644 --- a/rmgpy/quantity.py +++ b/rmgpy/quantity.py @@ -34,8 +34,10 @@ """ import numpy +import cython import quantities as pq import logging +import re import rmgpy.constants as constants from rmgpy.exceptions import QuantityError @@ -86,7 +88,8 @@ class Units(object): # A dict of conversion factors (to SI) for each of the frequent units # Here we also define that cm^-1 is not to be converted to m^-1 (or Hz, J, K, etc.) conversionFactors = {'cm^-1': 1.0} - + + def __init__(self, units=''): if units in NOT_IMPLEMENTED_UNITS: raise NotImplementedError( @@ -117,6 +120,35 @@ def getConversionFactorFromSI(self): """ return 1.0 / self.getConversionFactorToSI() + # A dict of conversion factors from SI (with same dimensionality as keys) + # to combinations of cm/mol/s which is like SI except cm instead of m. Used as a cache. + conversionFactorsFromSItoCmMolS = {'s^-1': 1.0} + def getConversionFactorFromSItoCmMolS(self): + """ + Return the conversion factor for converting into SI units + only with all lengths in cm, instead of m. + This is useful for outputting chemkin file kinetics. + Depending on the stoichiometry of the reaction the reaction rate + coefficient could be /s, cm^3/mol/s, cm^6/mol^2/s, and for + heterogeneous reactions even more possibilities. + Only lengths are changed. Everything else is in SI, i.e. + moles (not molecules) and seconds (not minutes). + """ + cython.declare(factor=cython.double, metres=cython.int) + try: + factor = Units.conversionFactorsFromSItoCmMolS[self.units] + except KeyError: + # Fall back to (slower) quantities package for units not seen before + dimensionality = pq.Quantity(1.0, self.units).simplified.dimensionality + metres = dimensionality.get(pq.m, 0) + factor = 100 ** metres + + # Cache the conversion factor so we don't ever need to use + # quantities to compute it again + Units.conversionFactorsFromSItoCmMolS[self.units] = factor + return factor + + ################################################################################ class ScalarQuantity(Units): diff --git a/rmgpy/quantityTest.py b/rmgpy/quantityTest.py index 4dfbbaeb76..a32217dfa6 100644 --- a/rmgpy/quantityTest.py +++ b/rmgpy/quantityTest.py @@ -695,6 +695,7 @@ def test_s(self): self.assertAlmostEqual(q.value, 1.0, 6) self.assertAlmostEqual(q.value_si, 1.0, delta=1e-6) self.assertEqual(q.units, "s^-1") + self.assertAlmostEqual(q.getConversionFactorFromSItoCmMolS(), 1.0, places=1) # 1 /s = 1 /s def test_m3permols(self): """ @@ -704,6 +705,7 @@ def test_m3permols(self): self.assertAlmostEqual(q.value, 1.0, 6) self.assertAlmostEqual(q.value_si, 1.0, delta=1e-6) self.assertEqual(q.units, "m^3/(mol*s)") + self.assertAlmostEqual(q.getConversionFactorFromSItoCmMolS(), 1e6, places=1) # 1 m3/mol/s = 1e6 cm3/mol/s def test_m6permol2s(self): """ @@ -713,6 +715,7 @@ def test_m6permol2s(self): self.assertAlmostEqual(q.value, 1.0, 6) self.assertAlmostEqual(q.value_si, 1.0, delta=1e-6) self.assertEqual(q.units, "m^6/(mol^2*s)") + self.assertAlmostEqual(q.getConversionFactorFromSItoCmMolS(), 1e12, places=1) # 1 m6/mol2/s = 1e12 cm6/mol2/s def test_m9permol3s(self): """ @@ -722,6 +725,7 @@ def test_m9permol3s(self): self.assertAlmostEqual(q.value, 1.0, 6) self.assertAlmostEqual(q.value_si, 1.0, delta=1e-6) self.assertEqual(q.units, "m^9/(mol^3*s)") + self.assertAlmostEqual(q.getConversionFactorFromSItoCmMolS(), 1e18, delta=1e3) # 1 m9/mol3/s = 1e18 cm9/mol3/s def test_cm3permols(self): """ @@ -731,6 +735,7 @@ def test_cm3permols(self): self.assertAlmostEqual(q.value, 1.0, 6) self.assertAlmostEqual(q.value_si*1e6, 1.0, delta=1e-6) self.assertEqual(q.units, "cm^3/(mol*s)") + self.assertAlmostEqual(q.getConversionFactorFromSItoCmMolS(), 1e6, places=1) # 1 m3/mol/s = 1 cm3/mol/s def test_cm6permol2s(self): """ @@ -740,6 +745,7 @@ def test_cm6permol2s(self): self.assertAlmostEqual(q.value, 1.0, 6) self.assertAlmostEqual(q.value_si*(1e6)**2, 1.0, delta=1e-6) self.assertEqual(q.units, "cm^6/(mol^2*s)") + self.assertAlmostEqual(q.getConversionFactorFromSItoCmMolS(), 1e12, places=1) # 1 m6/mol2/s = 1e12 cm6/mol2/s def test_cm9permol3s(self): """ @@ -749,6 +755,7 @@ def test_cm9permol3s(self): self.assertAlmostEqual(q.value, 1.0, 6) self.assertAlmostEqual(q.value_si*(1e6)**3, 1.0, delta=1e-6) self.assertEqual(q.units, "cm^9/(mol^3*s)") + self.assertAlmostEqual(q.getConversionFactorFromSItoCmMolS(), 1e18, delta=1e3) # 1 m9/mol3/s = 1e18 cm9/mol3/s def test_cm3permolecules(self): """ @@ -758,6 +765,7 @@ def test_cm3permolecules(self): self.assertAlmostEqual(q.value, 1.0, 6) self.assertAlmostEqual(q.value_si*1e6/constants.Na, 1.0, delta=1e-6) self.assertEqual(q.units, "cm^3/(molecule*s)") + self.assertAlmostEqual(q.getConversionFactorFromSItoCmMolS(), 1e6, delta=1e0) # 1 m3/mol/s = 1e6 cm3/mol/s def test_cm6permolecule2s(self): """ @@ -767,6 +775,7 @@ def test_cm6permolecule2s(self): self.assertAlmostEqual(q.value, 1.0, 6) self.assertAlmostEqual(q.value_si*(1e6/constants.Na)**2, 1.0, delta=1e-6) self.assertEqual(q.units, "cm^6/(molecule^2*s)") + self.assertAlmostEqual(q.getConversionFactorFromSItoCmMolS(), 1e12 , delta=1e0) # 1 m6/mol2/s = 1e12 cm6/mol2/s def test_cm9permolecule3s(self): """ @@ -776,6 +785,8 @@ def test_cm9permolecule3s(self): self.assertAlmostEqual(q.value, 1.0, 6) self.assertAlmostEqual(q.value_si*(1e6/constants.Na)**3, 1.0, delta=1e-6) self.assertEqual(q.units, "cm^9/(molecule^3*s)") + print q.units + self.assertAlmostEqual(q.getConversionFactorFromSItoCmMolS(), 1e18 , delta=1e3) # 1 m9/mole3/s = 1e18 cm9/mol3/s ################################################################################ @@ -1040,3 +1051,6 @@ def test_array_repr(self): self.assertEqual(repr(self.Cp),repr(self.Cp_array)) self.assertEqual(repr(v),repr(self.v)) self.assertEqual(repr(self.v),repr(self.v_array)) + + + diff --git a/rmgpy/rmg/output.py b/rmgpy/rmg/output.py index 3a75c12495..cf65db3dce 100644 --- a/rmgpy/rmg/output.py +++ b/rmgpy/rmg/output.py @@ -92,7 +92,8 @@ def saveOutputHTML(path, reactionModel, partCoreEdge='core'): try: MoleculeDrawer().draw(spec.molecule[0], 'png', fstr) except IndexError: - raise OutputError("{0} species could not be drawn because it did not contain a molecular structure. Please recheck your files.".format(getSpeciesIdentifier(spec))) + logging.error("{0} species could not be drawn because it did not contain a molecular structure. Please recheck your files.".format(getSpeciesIdentifier(spec))) + raise #spec.thermo.comment= # Text wrap the thermo comments # We want to keep species sorted in the original order in which they were added to the RMG core. diff --git a/rmgpy/solver/base.pxd b/rmgpy/solver/base.pxd index 2e299edef8..29f9de8e21 100644 --- a/rmgpy/solver/base.pxd +++ b/rmgpy/solver/base.pxd @@ -46,6 +46,8 @@ cdef class ReactionSystem(DASx): cdef public int numCoreSpecies cdef public int numCoreReactions cdef public int numEdgeSpecies + cdef public int numSurfaceSpecies + cdef public int numSurfaceReactions cdef public int numEdgeReactions cdef public int numPdepNetworks cdef public int neq diff --git a/rmgpy/solver/simple.pyx b/rmgpy/solver/simple.pyx index ce5641c9ed..29a4a66d46 100644 --- a/rmgpy/solver/simple.pyx +++ b/rmgpy/solver/simple.pyx @@ -171,11 +171,10 @@ cdef class SimpleReactor(ReactionSystem): # First call the base class version of the method # This initializes the attributes declared in the base class - ReactionSystem.initializeModel(self, coreSpecies, coreReactions, edgeSpecies, edgeReactions, - surfaceSpecies=surfaceSpecies, surfaceReactions=surfaceReactions, - pdepNetworks=pdepNetworks, atol=atol, rtol=rtol, - sensitivity=sensitivity, sens_atol=sens_atol, sens_rtol=sens_rtol, - filterReactions=filterReactions, conditions=conditions) + ReactionSystem.initializeModel(self, coreSpecies=coreSpecies, coreReactions=coreReactions, edgeSpecies=edgeSpecies, + edgeReactions=edgeReactions, surfaceSpecies=surfaceSpecies, surfaceReactions=surfaceReactions, + pdepNetworks=pdepNetworks, atol=atol, rtol=rtol, sensitivity=sensitivity, sens_atol=sens_atol, + sens_rtol=sens_rtol, filterReactions=filterReactions, conditions=conditions) # Set initial conditions self.set_initial_conditions() diff --git a/rmgpy/thermo/nasa.pyx b/rmgpy/thermo/nasa.pyx index c747dcb450..c966c122ea 100644 --- a/rmgpy/thermo/nasa.pyx +++ b/rmgpy/thermo/nasa.pyx @@ -351,7 +351,9 @@ cdef class NASA(HeatCapacityModel): cpdef ThermoData toThermoData(self): """ - Convert the Wilhoit model to a :class:`ThermoData` object. + Convert the NASAPolynomial model to a :class:`ThermoData` object. + + If Cp0 and CpInf are omitted or 0, they are None in the returned ThermoData. """ from rmgpy.thermo.thermodata import ThermoData diff --git a/setup.py b/setup.py index 4a87fe9260..349a5c977b 100644 --- a/setup.py +++ b/setup.py @@ -193,6 +193,10 @@ def getArkaneExtensionModules(): if os.path.splitext(source)[1] == '.pyx': ext_modules.append(module) +# Remove duplicates while preserving order: +from collections import OrderedDict +ext_modules = list(OrderedDict.fromkeys(ext_modules)) + scripts=['Arkane.py', 'rmg.py', 'scripts/checkModels.py', diff --git a/testing/databaseTest.py b/testing/databaseTest.py index 37baf23ecb..53dd8ae51d 100644 --- a/testing/databaseTest.py +++ b/testing/databaseTest.py @@ -7,6 +7,8 @@ from rmgpy import settings from rmgpy.data.rmg import RMGDatabase from copy import copy +import rmgpy.kinetics +import quantities as pq from rmgpy.data.base import LogicOr from rmgpy.molecule import Group, ImplicitBenzeneError, UnexpectedChargeError from rmgpy.molecule.atomtype import atomTypes @@ -101,7 +103,13 @@ def test_kinetics(self): for depository in family.depositories: test = lambda x: self.kinetics_checkAdjlistsNonidentical(depository) - test_name = "Kinetics {1} Depository: check adjacency lists are nonidentical?".format(family_name, depository.label) + test_name = "Kinetics depository {0}: check adjacency lists are nonidentical?".format(depository.label) + test.description = test_name + self.compat_func_name = test_name + yield test, depository.label + + test = lambda x: self.kinetics_checkRateUnitsAreCorrect(depository, tag='depository') + test_name = "Kinetics depository {0}: check rates have correct units?".format(depository.label) test.description = test_name self.compat_func_name = test_name yield test, depository.label @@ -113,9 +121,15 @@ def test_kinetics(self): test.description = test_name self.compat_func_name = test_name yield test, library_name - + + test = lambda x: self.kinetics_checkRateUnitsAreCorrect(library) + test_name = "Kinetics library {0}: check rates have correct units?".format(library_name) + test.description = test_name + self.compat_func_name = test_name + yield test, library_name + test = lambda x: self.kinetics_checkLibraryRatesAreReasonable(library) - test_name = "Kinetics library {0}: check rates can be evaluated?".format(library_name) + test_name = "Kinetics library {0}: check rates are reasonable?".format(library_name) test.description = test_name self.compat_func_name = test_name yield test, library_name @@ -424,7 +438,123 @@ def kinetics_checkAdjlistsNonidentical(self, database): continue nose.tools.assert_false(speciesList[i].molecule[0].isIsomorphic(speciesList[j].molecule[0], initialMap), "Species {0} and species {1} in {2} database were found to be identical.".format(speciesList[i].label,speciesList[j].label,database.label)) - + + def kinetics_checkRateUnitsAreCorrect(self, database, tag='library'): + """ + This test ensures that every reaction has acceptable units on the A factor. + """ + boo = False + + dimensionalities = { + 1: (1 / pq.s).dimensionality , + 2: (pq.m**3 / pq.mole / pq.s).dimensionality, + 3: ((pq.m**6) / (pq.mole**2) / pq.s).dimensionality, + } + + for entry in database.entries.values(): + k = entry.data + rxn = entry.item + molecularity = len(rxn.reactants) + try: + if isinstance(k, rmgpy.kinetics.Arrhenius): + A = k.A + if pq.Quantity(1.0, A.units).simplified.dimensionality != dimensionalities[molecularity]: + boo = True + logging.error('Reaction {0} from {1} {2}, has invalid units {3}'.format(rxn, tag, database.label, A.units)) + elif isinstance(k, (rmgpy.kinetics.Lindemann, rmgpy.kinetics.Troe )): + A = k.arrheniusHigh.A + if pq.Quantity(1.0, A.units).simplified.dimensionality != dimensionalities[molecularity]: + boo = True + logging.error('Reaction {0} from {1} {2}, has invalid high-pressure limit units {3}'.format(rxn, tag, database.label, A.units)) + elif isinstance(k, (rmgpy.kinetics.Lindemann, rmgpy.kinetics.Troe, rmgpy.kinetics.ThirdBody)): + A = k.arrheniusLow.A + if pq.Quantity(1.0, A.units).simplified.dimensionality != dimensionalities[molecularity+1]: + boo = True + logging.error('Reaction {0} from {1} {2}, has invalid low-pressure limit units {3}'.format(rxn, tag, database.label, A.units)) + elif hasattr(k, 'highPlimit') and k.highPlimit is not None: + A = k.highPlimit.A + if pq.Quantity(1.0, A.units).simplified.dimensionality != dimensionalities[molecularity-1]: + boo = True + logging.error( + 'Reaction {0} from {1} {2}, has invalid high-pressure limit units {3}'.format(rxn, tag, database.label, A.units)) + elif isinstance(k, rmgpy.kinetics.MultiArrhenius): + for num, arrhenius in enumerate(k.arrhenius): + A = arrhenius.A + if pq.Quantity(1.0, A.units).simplified.dimensionality != dimensionalities[molecularity]: + boo = True + logging.error( + 'Reaction {0} from {1} {2}, has invalid units {3} on rate expression {4}'.format( + rxn, tag, database.label, A.units, num + 1) + ) + + elif isinstance(k, rmgpy.kinetics.PDepArrhenius): + for pa, arrhenius in zip(k.pressures.value_si, k.arrhenius): + P = rmgpy.quantity.Pressure(1, k.pressures.units) + P.value_si = pa + + if isinstance(arrhenius, rmgpy.kinetics.MultiArrhenius): + # A PDepArrhenius may have MultiArrhenius within it + # which is distinct (somehow) from MultiPDepArrhenius + for num, arrhenius2 in enumerate(arrhenius.arrhenius): + A = arrhenius2.A + if pq.Quantity(1.0, A.units).simplified.dimensionality != dimensionalities[molecularity]: + boo = True + logging.error( + 'Reaction {0} from {1} {2}, has invalid units {3} on {4!r} rate expression {5}'.format( + rxn, tag, database.label, A.units, P, num + 1) + ) + else: + A = arrhenius.A + if pq.Quantity(1.0, A.units).simplified.dimensionality != dimensionalities[molecularity]: + boo = True + logging.error( + 'Reaction {0} from {1} {2}, has invalid {3!r} units {4}'.format( + rxn, tag, database.label, P, A.units) + ) + + elif isinstance(k, rmgpy.kinetics.MultiPDepArrhenius): + for num, k2 in enumerate(k.arrhenius): + for pa, arrhenius in zip(k2.pressures.value_si, k2.arrhenius): + P = rmgpy.quantity.Pressure(1, k2.pressures.units) + P.value_si = pa + if isinstance(arrhenius, rmgpy.kinetics.MultiArrhenius): + # A MultiPDepArrhenius may have MultiArrhenius within it + for arrhenius2 in arrhenius.arrhenius: + A = arrhenius2.A + if pq.Quantity(1.0, A.units).simplified.dimensionality != dimensionalities[ + molecularity]: + boo = True + logging.error( + 'Reaction {0} from {1} {2}, has invalid units {3} on {4!r} rate expression {5!r}'.format( + rxn, tag, database.label, A.units, P, arrhenius2) + ) + else: + A = arrhenius.A + if pq.Quantity(1.0, A.units).simplified.dimensionality != dimensionalities[ + molecularity]: + boo = True + logging.error( + 'Reaction {0} from {1} {2}, has invalid {3!r} units {4} in rate expression {5}'.format( + rxn, tag, database.label, P, A.units, num) + ) + + + elif isinstance(k, rmgpy.kinetics.Chebyshev): + if pq.Quantity(1.0, k.kunits).simplified.dimensionality != dimensionalities[molecularity]: + boo = True + logging.error( + 'Reaction {0} from {1} {2}, has invalid units {3}'.format( + rxn, tag, database.label, k.kunits) + ) + + else: + logging.warning('Reaction {0} from {1} {2}, did not have units checked.'.format(rxn, tag, database.label)) + except: + logging.error("Error when checking units on reaction {0} from {1} {2} with rate expression {3!r}.".format(rxn, tag, database.label, k)) + raise + if boo: + raise ValueError('{0} {1} has some incorrect units'.format(tag.capitalize(), database.label)) + def kinetics_checkLibraryRatesAreReasonable(self, library): """ This test ensures that every library reaction has reasonable kinetics at 1000 K, 1 bar