Skip to content

Commit e7bdd32

Browse files
authored
Merge pull request #1276 from ReactionMechanismGenerator/fix_calculate_degeneracy_bug
Update calculateDegeneracy to deal with identical reactants
2 parents bb77680 + f0846ca commit e7bdd32

File tree

5 files changed

+222
-45
lines changed

5 files changed

+222
-45
lines changed

rmgpy/data/kinetics/common.py

Lines changed: 13 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -156,8 +156,8 @@ def filter_reactions(reactants, products, reactionList):
156156
warnings.warn("The filter_reactions method is no longer used and may be removed in a future version.", DeprecationWarning)
157157

158158
# Convert from molecules to species and generate resonance isomers.
159-
reactants = ensure_species(reactants, resonance=True)
160-
products = ensure_species(products, resonance=True)
159+
ensure_species(reactants, resonance=True)
160+
ensure_species(products, resonance=True)
161161

162162
reactions = reactionList[:]
163163

@@ -197,11 +197,10 @@ def filter_reactions(reactants, products, reactionList):
197197

198198
def ensure_species(input_list, resonance=False, keepIsomorphic=False):
199199
"""
200-
Given an input list of molecules or species, return a list with only
201-
species objects.
200+
The input list of :class:`Species` or :class:`Molecule` objects is modified
201+
in place to only have :class:`Species` objects. Returns None.
202202
"""
203-
output_list = []
204-
for item in input_list:
203+
for index, item in enumerate(input_list):
205204
if isinstance(item, Molecule):
206205
new_item = Species(molecule=[item])
207206
elif isinstance(item, Species):
@@ -210,9 +209,7 @@ def ensure_species(input_list, resonance=False, keepIsomorphic=False):
210209
raise TypeError('Only Molecule or Species objects can be handled.')
211210
if resonance:
212211
new_item.generate_resonance_structures(keepIsomorphic=keepIsomorphic)
213-
output_list.append(new_item)
214-
215-
return output_list
212+
input_list[index] = new_item
216213

217214

218215
def generate_molecule_combos(input_species):
@@ -231,13 +228,15 @@ def generate_molecule_combos(input_species):
231228

232229
def ensure_independent_atom_ids(input_species, resonance=True):
233230
"""
234-
Given a list or tuple of :class:`Species` objects, ensure that atom ids are
235-
independent across all of the species. Optionally, the `resonance` argument
236-
can be set to False to not generate resonance structures.
231+
Given a list or tuple of :class:`Species` or :class:`Molecule` objects,
232+
ensure that atom ids are independent.
233+
The `resonance` argument can be set to False to not generate
234+
resonance structures.
237235
238-
Modifies the input species in place, nothing is returned.
236+
Modifies the list in place (replacing :class:`Molecule` with :class:`Species`).
237+
Returns None.
239238
"""
240-
239+
ensure_species(input_species, resonance=resonance)
241240
# Method to check that all species' atom ids are different
242241
def independent_ids():
243242
num_atoms = 0

rmgpy/data/kinetics/database.py

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -364,7 +364,7 @@ def generate_reactions(self, reactants, products=None, only_families=None, reson
364364
reactionList = []
365365
if only_families is None:
366366
reactionList.extend(self.generate_reactions_from_libraries(reactants, products))
367-
reactionList.extend(self.generate_reactions_from_families(reactants, products, only_families=None, resonance=True))
367+
reactionList.extend(self.generate_reactions_from_families(reactants, products, only_families=None, resonance=resonance))
368368
return reactionList
369369

370370
def generate_reactions_from_libraries(self, reactants, products=None):
@@ -386,7 +386,7 @@ def generate_reactions_from_library(self, library, reactants, products=None):
386386
provided `reactants`, which can be either :class:`Molecule` objects or
387387
:class:`Species` objects.
388388
"""
389-
reactants = ensure_species(reactants)
389+
ensure_species(reactants)
390390

391391
reaction_list = []
392392
for entry in library.entries.values():
@@ -434,10 +434,9 @@ def generate_reactions_from_families(self, reactants, products=None, only_famili
434434
elif reactants[0].isIsomorphic(reactants[1]):
435435
same_reactants = True
436436

437-
# Convert to Species objects if necessary
438-
reactants = ensure_species(reactants)
439-
440-
# Label reactant atoms for proper degeneracy calculation
437+
# Label reactant atoms for proper degeneracy calculation (cannot be in tuple)
438+
if isinstance(reactants, tuple):
439+
reactants = list(reactants)
441440
ensure_independent_atom_ids(reactants, resonance=resonance)
442441

443442
combos = generate_molecule_combos(reactants)

rmgpy/data/kinetics/family.py

Lines changed: 34 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,8 @@
4646
from rmgpy.molecule.resonance import generate_aromatic_resonance_structures
4747
from rmgpy.species import Species
4848

49-
from .common import saveEntry, ensure_species, find_degenerate_reactions, generate_molecule_combos
49+
from .common import saveEntry, ensure_species, find_degenerate_reactions, generate_molecule_combos,\
50+
ensure_independent_atom_ids
5051
from .depository import KineticsDepository
5152
from .groups import KineticsGroups
5253
from .rules import KineticsRules
@@ -1545,22 +1546,28 @@ def calculateDegeneracy(self, reaction):
15451546
`ignoreSameReactants= True` to this method.
15461547
"""
15471548
reaction.degeneracy = 1
1548-
1549-
# find combinations of resonance isomers
1550-
specReactants = ensure_species(reaction.reactants, resonance=True, keepIsomorphic=True)
1551-
molecule_combos = generate_molecule_combos(specReactants)
1549+
# Check if the reactants are the same
1550+
# If they refer to the same memory address, then make a deep copy so
1551+
# they can be manipulated independently
1552+
reactants = reaction.reactants
1553+
same_reactants = False
1554+
if len(reactants) == 2:
1555+
if reactants[0] is reactants[1]:
1556+
reactants[1] = reactants[1].copy(deep=True)
1557+
same_reactants = True
1558+
elif reactants[0].isIsomorphic(reactants[1]):
1559+
same_reactants = True
1560+
1561+
# Label reactant atoms for proper degeneracy calculation
1562+
ensure_independent_atom_ids(reactants, resonance=True)
1563+
molecule_combos = generate_molecule_combos(reactants)
15521564

15531565
reactions = []
15541566
for combo in molecule_combos:
15551567
reactions.extend(self.__generateReactions(combo, products=reaction.products, forward=True))
15561568

1557-
# Check if the reactants are the same
1558-
sameReactants = False
1559-
if len(specReactants) == 2 and specReactants[0].isIsomorphic(specReactants[1]):
1560-
sameReactants = True
1561-
15621569
# remove degenerate reactions
1563-
reactions = find_degenerate_reactions(reactions, sameReactants, kinetics_family=self)
1570+
reactions = find_degenerate_reactions(reactions, same_reactants, kinetics_family=self)
15641571

15651572
# remove reactions with different templates (only for TemplateReaction)
15661573
if isinstance(reaction, TemplateReaction):
@@ -1692,7 +1699,7 @@ def __generateReactions(self, reactants, products=None, forward=True, prod_reson
16921699
# If products is given, remove reactions from the reaction list that
16931700
# don't generate the given products
16941701
if products is not None:
1695-
products = ensure_species(products, resonance=prod_resonance)
1702+
ensure_species(products, resonance=prod_resonance)
16961703

16971704
rxnList0 = rxnList[:]
16981705
rxnList = []
@@ -2125,6 +2132,15 @@ def addAtomLabelsForReaction(self, reaction, output_with_resonance = True):
21252132
# make sure we start with reaction with species objects
21262133
reaction.ensure_species(reactant_resonance=False, product_resonance=False)
21272134

2135+
reactants = reaction.reactants
2136+
products = reaction.products
2137+
# ensure all species are independent references
2138+
if len(reactants + products) > len(set([id(s) for s in reactants + products])):
2139+
logging.debug('Copying reactants and products for reaction {} since they have identical species references'.format(reaction))
2140+
# not all species are independent
2141+
reactants = [s.copy(deep=True) for s in reactants]
2142+
products = [s.copy(deep=True) for s in products]
2143+
21282144
# get all possible pairs of resonance structures
21292145
reactant_pairs = list(itertools.product(*[s.molecule for s in reaction.reactants]))
21302146
product_pairs = list(itertools.product(*[s.molecule for s in reaction.products]))
@@ -2145,20 +2161,22 @@ def addAtomLabelsForReaction(self, reaction, output_with_resonance = True):
21452161

21462162
# place the molecules in reaction's species object
21472163
# this prevents overwriting of attributes of species objects by this method
2148-
for species in reaction.products:
2164+
for index, species in enumerate(products):
21492165
for labeled_molecule in labeled_products:
21502166
if species.isIsomorphic(labeled_molecule):
21512167
species.molecule = [labeled_molecule]
2168+
reaction.products[index] = species
21522169
break
21532170
else:
2154-
raise ActionError('Could not find isomorphic molecule to fit the original product {}'.format(species))
2155-
for species in reaction.reactants:
2171+
raise ActionError('Could not find isomorphic molecule to fit the original product {} from reaction {}'.format(species, reaction))
2172+
for index, species in enumerate(reactants):
21562173
for labeled_molecule in labeled_reactants:
21572174
if species.isIsomorphic(labeled_molecule):
21582175
species.molecule = [labeled_molecule]
2176+
reaction.reactants[index] = species
21592177
break
21602178
else:
2161-
raise ActionError('Could not find isomorphic molecule to fit the original reactant {}'.format(species))
2179+
raise ActionError('Could not find isomorphic molecule to fit the original reactant {} from reaction {}'.format(species, reaction))
21622180

21632181
if output_with_resonance:
21642182
# convert the molecules to species objects with resonance structures

rmgpy/data/kinetics/kineticsTest.py

Lines changed: 166 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -688,6 +688,21 @@ def test_ensure_independent_atom_ids(self):
688688
# checks second resonance structure id
689689
self.assertNotEqual(s2.molecule[1].atoms[0].id, -1)
690690

691+
def test_ensure_independent_atom_ids_no_resonance(self):
692+
"""
693+
Ensure ensure_independent_atom_ids does not generate resonance
694+
"""
695+
s1 = Species().fromSMILES('CCC')
696+
s2 = Species().fromSMILES('C=C[CH]C')
697+
self.assertEqual(s2.molecule[0].atoms[0].id, -1)
698+
699+
ensure_independent_atom_ids([s1, s2],resonance=False)
700+
# checks resonance structures
701+
self.assertEqual(len(s2.molecule),1)
702+
# checks that atom ids are changed
703+
for atom in s2.molecule[0].atoms:
704+
self.assertNotEqual(atom.id, -1)
705+
691706
def testSaveEntry(self):
692707
"""
693708
tests that save entry can run
@@ -839,25 +854,171 @@ def test_generate_reactions_from_families_product_resonance(self):
839854
self.assertEqual(len(reaction_list), 1)
840855
self.assertEqual(reaction_list[0].degeneracy, 2)
841856

842-
reaction_list = self.database.kinetics.generate_reactions_from_families(reactants, products, only_families=['H_Abstraction'], resonance=False)
843857

858+
859+
def test_generate_reactions_from_families_product_resonance2(self):
860+
"""Test that we can specify the no product resonance structure when generating reactions"""
861+
reactants = [
862+
Molecule().fromSMILES('CCC=C'),
863+
Molecule().fromSMILES('[H]'),
864+
]
865+
products = [
866+
Molecule().fromSMILES('CC=C[CH2]'),
867+
Molecule().fromSMILES('[H][H]'),
868+
]
869+
870+
reaction_list = self.database.kinetics.generate_reactions_from_families(reactants, products, only_families=['H_Abstraction'], resonance=False)
844871
self.assertEqual(len(reaction_list), 0)
845872

873+
self.assertTrue(isinstance(products[0],Species))
874+
self.assertEqual(len(products[0].molecule),1)
875+
846876
def test_generate_reactions_from_libraries(self):
847877
"""Test that we can generate reactions from libraries"""
848878
reactants = [
849879
Molecule().fromSMILES('CC=O'),
850880
Molecule().fromSMILES('[H]'),
851881
]
852-
products = [
853-
Molecule().fromSMILES('[CH2]C=O'),
854-
Molecule().fromSMILES('[H][H]'),
855-
]
856882

857883
reaction_list = self.database.kinetics.generate_reactions_from_libraries(reactants)
858884

859885
self.assertEqual(len(reaction_list), 3)
860886

887+
def test_generate_reactions_from_libraries2(self):
888+
"""Test that we can generate reactions from libraries specifying products"""
889+
reactants = [
890+
Molecule().fromSMILES('CC=O'),
891+
Molecule().fromSMILES('[H]'),
892+
]
893+
products = [
894+
Molecule().fromSMILES('[CH2]C=O'),
895+
Molecule().fromSMILES('[H][H]'),
896+
]
861897
reaction_list_2 = self.database.kinetics.generate_reactions_from_libraries(reactants, products)
862898

863899
self.assertEqual(len(reaction_list_2), 1)
900+
901+
def test_add_atom_labels_for_reaction(self):
902+
"""Test that addAtomLabelsForReaction can identify reactions with resonance
903+
The molecule [CH]=C=C has resonance in this reaction"""
904+
from rmgpy.data.rmg import getDB
905+
reactants = [
906+
Molecule().fromSMILES('C=C=C'),
907+
Molecule().fromSMILES('[CH]=C=C'),
908+
]
909+
products = [
910+
Molecule().fromSMILES('C#C[CH2]'),
911+
Molecule().fromSMILES('C#CC'),
912+
]
913+
reaction = TemplateReaction(reactants =reactants,
914+
products = products,
915+
family = 'H_Abstraction')
916+
reaction.ensure_species(reactant_resonance=True, product_resonance=True)
917+
family = getDB('kinetics').families['H_Abstraction']
918+
family.addAtomLabelsForReaction(reaction, output_with_resonance=False)
919+
920+
# test that the reaction has labels
921+
found_labels = []
922+
for species in reaction.reactants:
923+
for atom in species.molecule[0].atoms:
924+
if atom.label != '':
925+
found_labels.append(atom.label)
926+
self.assertEqual(len(found_labels), 3)
927+
self.assertIn('*1',found_labels)
928+
self.assertIn('*2',found_labels)
929+
self.assertIn('*3',found_labels)
930+
931+
# test for the products too
932+
found_labels = []
933+
for species in reaction.products:
934+
for atom in species.molecule[0].atoms:
935+
if atom.label != '':
936+
found_labels.append(atom.label)
937+
self.assertEqual(len(found_labels), 3)
938+
self.assertIn('*1',found_labels)
939+
self.assertIn('*2',found_labels)
940+
self.assertIn('*3',found_labels)
941+
942+
def test_add_atom_labels_for_reaction_2(self):
943+
"""Test that addAtomLabelsForReaction can identify reactions with identical references
944+
The molecule [CH]=C=C has resonance in this reaction"""
945+
from rmgpy.data.rmg import getDB
946+
s1 = Species().fromSMILES('C=C=C')
947+
s2 = Species().fromSMILES('C=C=[CH]')
948+
s3 = Species().fromSMILES('C#CC')
949+
s2.generate_resonance_structures()
950+
reactants = [s1,s2]
951+
products = [s2,s3]
952+
reaction = TemplateReaction(reactants =reactants,
953+
products = products,
954+
family = 'H_Abstraction')
955+
family = getDB('kinetics').families['H_Abstraction']
956+
print reaction.reactants
957+
print reaction.products
958+
family.addAtomLabelsForReaction(reaction, output_with_resonance=False)
959+
960+
# test that the reaction has labels
961+
found_labels = []
962+
for species in reaction.reactants:
963+
for atom in species.molecule[0].atoms:
964+
if atom.label != '':
965+
found_labels.append(atom.label)
966+
self.assertEqual(len(found_labels), 3,'wrong number of labels found {0}'.format(found_labels))
967+
self.assertIn('*1',found_labels)
968+
self.assertIn('*2',found_labels)
969+
self.assertIn('*3',found_labels)
970+
971+
# test for the products too
972+
found_labels = []
973+
for species in reaction.products:
974+
for atom in species.molecule[0].atoms:
975+
if atom.label != '':
976+
found_labels.append(atom.label)
977+
self.assertEqual(len(found_labels), 3)
978+
self.assertIn('*1',found_labels)
979+
self.assertIn('*2',found_labels)
980+
self.assertIn('*3',found_labels)
981+
982+
def test_add_atom_labels_for_reaction_3(self):
983+
"""Test that addAtomLabelsForReaction can identify reactions with resonance and isotopes"""
984+
from rmgpy.data.rmg import getDB
985+
mr0 = Molecule().fromAdjacencyList('1 C u0 p0 c0 i13 {3,D} {4,S} {5,S}\n2 *1 C u0 p0 c0 {3,D} {6,S} {7,S}\n3 C u0 p0 c0 {1,D} {2,D}\n4 H u0 p0 c0 {1,S}\n5 H u0 p0 c0 {1,S}\n6 H u0 p0 c0 {2,S}\n7 *4 H u0 p0 c0 {2,S}\n')
986+
mr1a = Molecule().fromAdjacencyList('multiplicity 2\n1 C u0 p0 c0 i13 {2,D} {4,S} {5,S}\n2 C u0 p0 c0 {1,D} {3,D}\n3 *1 C u1 p0 c0 {2,D} {6,S}\n4 H u0 p0 c0 {1,S}\n5 H u0 p0 c0 {1,S}\n6 H u0 p0 c0 {3,S}\n')
987+
mr1b = Molecule().fromAdjacencyList('multiplicity 2\n1 C u1 p0 c0 i13 {2,S} {4,S} {5,S}\n2 C u0 p0 c0 {1,S} {3,T}\n3 *1 C u0 p0 c0 {2,T} {6,S}\n4 H u0 p0 c0 {1,S}\n5 H u0 p0 c0 {1,S}\n6 H u0 p0 c0 {3,S}\n')
988+
mp1a = Molecule().fromAdjacencyList('multiplicity 2\n1 C u0 p0 c0 {2,D} {4,S} {5,S}\n2 C u0 p0 c0 {1,D} {3,D}\n3 *1 C u1 p0 c0 i13 {2,D} {6,S}\n4 H u0 p0 c0 {1,S}\n5 H u0 p0 c0 {1,S}\n6 H u0 p0 c0 {3,S}\n')
989+
mp1b = Molecule().fromAdjacencyList('multiplicity 2\n1 C u1 p0 c0 {2,S} {4,S} {5,S}\n2 C u0 p0 c0 {1,S} {3,T}\n3 *1 C u0 p0 c0 i13 {2,T} {6,S}\n4 H u0 p0 c0 {1,S}\n5 H u0 p0 c0 {1,S}\n6 H u0 p0 c0 {3,S}\n')
990+
s1 = Species(molecule = [mr0])
991+
s2 = Species(molecule = [mr1a,mr1b])
992+
s3 = Species(molecule = [mp1a,mp1b])
993+
reactants = [s1,s2]
994+
products = [s1,s3]
995+
reaction = TemplateReaction(reactants =reactants,
996+
products = products,
997+
family = 'H_Abstraction')
998+
family = getDB('kinetics').families['H_Abstraction']
999+
print reaction.reactants
1000+
print reaction.products
1001+
family.addAtomLabelsForReaction(reaction, output_with_resonance=False)
1002+
1003+
# test that the reaction has labels
1004+
found_labels = []
1005+
for species in reaction.reactants:
1006+
for atom in species.molecule[0].atoms:
1007+
if atom.label != '':
1008+
found_labels.append(atom.label)
1009+
self.assertEqual(len(found_labels), 3,'wrong number of labels found {0}'.format(found_labels))
1010+
self.assertIn('*1',found_labels)
1011+
self.assertIn('*2',found_labels)
1012+
self.assertIn('*3',found_labels)
1013+
1014+
# test for the products too
1015+
found_labels = []
1016+
for species in reaction.products:
1017+
for atom in species.molecule[0].atoms:
1018+
if atom.label != '':
1019+
found_labels.append(atom.label)
1020+
self.assertEqual(len(found_labels), 3)
1021+
self.assertIn('*1',found_labels)
1022+
self.assertIn('*2',found_labels)
1023+
self.assertIn('*3',found_labels)
1024+

0 commit comments

Comments
 (0)