Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enhancing heteroatoms in RMG-Py #1039

Closed
wants to merge 9 commits into from
4 changes: 3 additions & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,9 @@ before_install:
- conda update --yes conda
- cd ..
- git clone https://github.com/ReactionMechanismGenerator/RMG-database.git
- cd RMG-Py
- cd RMG-database
- git checkout sulfur
- cd ../RMG-Py

install:
- conda env create -f environment_linux.yml
Expand Down
2 changes: 1 addition & 1 deletion documentation/source/users/rmg/database/thermo.rst
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ is organized in a hierarchical tree, and is defined at the bottom of the databas
L3: Cb
L4: Cb-H
L4: Cb-Os
L4: Cb-Ss
L4: Cb-S2s
L4: Cb-C
L5: Cb-Cs
L5: Cb-Cds
Expand Down
1 change: 1 addition & 0 deletions rmgpy/data/thermo.py
Original file line number Diff line number Diff line change
Expand Up @@ -1742,6 +1742,7 @@ def __addGroupThermoData(self, thermoData, database, molecule, atom):
data = entry.data
comment = entry.label
break
else: raise DatabaseError("Node {0} points to a non-existant group called {1} in database: {2}".format(node.label, data, database.label))
data.comment = '{0}({1})'.format(database.label, comment)

# This code prints the hierarchy of the found node; useful for debugging
Expand Down
124 changes: 109 additions & 15 deletions rmgpy/molecule/atomtype.py

Large diffs are not rendered by default.

161 changes: 147 additions & 14 deletions rmgpy/molecule/atomtypeTest.py
Original file line number Diff line number Diff line change
Expand Up @@ -182,31 +182,140 @@ def setUp(self):
10 H u0 p0 {4,S}
11 H u0 p0 {5,S}''')

self.mol19 = Molecule().fromAdjacencyList('''1 C u0 p0 c0 {2,D} {3,S} {4,S}
2 S u0 p2 c0 {1,D}
3 H u0 p0 c0 {1,S}
4 H u0 p0 c0 {1,S}''')
self.mol19 = Molecule().fromSMILES('C=S')

self.mol20 = Molecule().fromSMILES('[C-]#[O+]')

self.mol21 = Molecule().fromSMILES('[C-]#[S+]')
self.mol21 = Molecule().fromAdjacencyList('''1 S u0 p3 c-1 {2,S}
2 S u0 p2 c+1 {1,S}''')

self.mol22 = Molecule().fromAdjacencyList('''1 S u0 p3 c0''')

self.mol23 = Molecule().fromAdjacencyList('''1 S u0 p2 c0 {2,S} {5,S}
2 S u0 p1 c+1 {1,S} {3,S} {4,S}
3 C u0 p0 c0 {2,S} {6,S} {7,S} {8,S}
4 O u0 p3 c-1 {2,S}
5 H u0 p0 c0 {1,S}
6 H u0 p0 c0 {3,S}
7 H u0 p0 c0 {3,S}
8 H u0 p0 c0 {3,S}''')

self.mol24 = Molecule().fromAdjacencyList('''1 C u0 p0 c0 {2,D} {4,S} {5,S}
2 S u0 p2 c-1 {1,D} {3,S}
3 O u0 p2 c+1 {2,S}
4 H u0 p0 c0 {1,S}
5 H u0 p0 c0 {1,S}''')

self.mol25 = Molecule().fromAdjacencyList('''1 S u0 p1 c0 {2,S} {5,S} {7,S} {8,S}
2 O u0 p2 c0 {1,S} {3,S}
3 S u0 p1 c0 {2,S} {4,S} {9,D}
4 O u0 p2 c0 {3,S} {6,S}
5 H u0 p0 c0 {1,S}
6 H u0 p0 c0 {4,S}
7 H u0 p0 c0 {1,S}
8 H u0 p0 c0 {1,S}
9 O u0 p2 c0 {3,D}''')

self.mol26 = Molecule().fromAdjacencyList('''1 O u0 p3 c-1 {2,S}
2 S u0 p1 c+1 {1,S} {3,D}
3 O u0 p2 c0 {2,D}''')

#self.mol27 = Molecule().fromAdjacencyList('''1 S u0 p1 c0 {2,B} {5,B}
# 2 C u0 p0 c0 {1,B} {3,B} {6,S}
# 3 C u0 p0 c0 {2,B} {4,B} {7,S}
# 4 C u0 p0 c0 {3,B} {5,B} {8,S}
# 5 C u0 p0 c0 {1,B} {4,B} {9,S}
# 6 H u0 p0 c0 {2,S}
# 7 H u0 p0 c0 {3,S}
# 8 H u0 p0 c0 {4,S}
# 9 H u0 p0 c0 {5,S}''')

self.mol28 = Molecule().fromAdjacencyList('''1 O u0 p2 c0 {2,D}
2 S u0 p1 c0 {1,D} {3,D}
3 C u0 p0 c0 {2,D} {4,S} {7,S}
4 C u0 p0 c0 {3,S} {5,T}
5 S u0 p1 c0 {4,T} {6,S}
6 S u0 p0 c0 {5,S} {8,S} {9,S} {10,S} {11,S} {12,S}
7 H u0 p0 c0 {3,S}
8 H u0 p0 c0 {6,S}
9 H u0 p0 c0 {6,S}
10 H u0 p0 c0 {6,S}
11 H u0 p0 c0 {6,S}
12 H u0 p0 c0 {6,S}''')

self.mol29 = Molecule().fromAdjacencyList('''1 C u0 p1 c-1 {2,T}
2 S u0 p1 c+1 {1,T}''')

self.mol30 = Molecule().fromAdjacencyList('''1 S u0 p0 c0 {2,D} {3,S} {4,S} {5,S} {6,S}
2 O u0 p2 c0 {1,D}
3 H u0 p0 c0 {1,S}
4 H u0 p0 c0 {1,S}
5 H u0 p0 c0 {1,S}
6 H u0 p0 c0 {1,S}''')

self.mol31 = Molecule().fromAdjacencyList('''1 O u0 p3 c-1 {2,S}
2 S u0 p0 c+2 {1,S} {3,S} {4,D}
3 O u0 p3 c-1 {2,S}
4 O u0 p2 c0 {2,D}''')

self.mol32 = Molecule().fromAdjacencyList('''1 O u0 p2 c0 {2,D}
2 S u0 p0 c0 {1,D} {3,D} {4,S} {5,S}
3 O u0 p2 c0 {2,D}
4 O u0 p2 c0 {2,S} {6,S}
5 O u0 p2 c0 {2,S} {7,S}
6 H u0 p0 c0 {4,S}
7 H u0 p0 c0 {5,S}''')

self.mol22 = Molecule().fromAdjacencyList('''1 N u0 p2 c-1 {2,S} {3,S}
self.mol33 = Molecule().fromAdjacencyList('''1 O u0 p3 c-1 {2,S}
2 S u0 p0 c+1 {1,S} {3,D} {4,D}
3 O u0 p2 c0 {2,D}
4 O u0 p2 c0 {2,D}''')

self.mol34 = Molecule().fromAdjacencyList('''1 O u0 p2 c0 {2,D}
2 S u0 p0 c0 {1,D} {3,D} {4,D}
3 O u0 p2 c0 {2,D}
4 O u0 p2 c0 {2,D}''')

self.mol35 = Molecule().fromAdjacencyList('''1 S u0 p0 c0 {2,T} {3,S} {4,S} {5,S}
2 N u0 p1 c0 {1,T}
3 H u0 p0 c0 {1,S}
4 H u0 p0 c0 {1,S}
5 H u0 p0 c0 {1,S}''')

self.mol36 = Molecule().fromAdjacencyList('''1 S u0 p0 c0 {2,T} {3,D} {4,S}
2 N u0 p1 c0 {1,T}
3 O u0 p2 c0 {1,D}
4 H u0 p0 c0 {1,S}''')

self.mol37 = Molecule().fromAdjacencyList('''1 N u0 p1 c0 {2,T}
2 S u0 p0 c0 {1,T} {3,T}
3 N u0 p1 c0 {2,T}''')

self.mol38 = Molecule().fromSMILES('O=S=O')

self.mol39 = Molecule().fromAdjacencyList('''1 N u0 p2 c-1 {2,S} {3,S}
2 H u0 p0 c0 {1,S}
3 N u0 p0 c+1 {1,S} {4,T}
4 C u0 p0 c0 {3,T} {5,S}
5 H u0 p0 c0 {4,S}''')

self.mol23 = Molecule().fromAdjacencyList('''1 N u0 p0 c+1 {2,S} {3,T}
self.mol40 = Molecule().fromAdjacencyList('''1 N u0 p0 c+1 {2,S} {3,T}
2 H u0 p0 c0 {1,S}
3 N u0 p0 c+1 {1,T} {4,S}
4 N u0 p3 c-2 {3,S}''')


self.mol24 = Molecule().fromAdjacencyList('''1 N u0 p2 c0 {2,S}
self.mol41 = Molecule().fromAdjacencyList('''1 N u0 p2 c0 {2,S}
2 H u0 p0 c0 {1,S}''')


self.mol42 = Molecule().fromAdjacencyList('''1 N u0 p1 c0 {2,T}
2 N u0 p0 c+1 {1,T} {3,S}
3 S u0 p2 c-1 {2,S} {4,S} {5,S}
4 O u1 p2 c0 {3,S}
5 O u1 p2 c0 {3,S}''')


def atomType(self, mol, atomID):
atom = mol.atoms[atomID]
type = getAtomType(atom, mol.getBonds(atom))
Expand Down Expand Up @@ -237,10 +346,10 @@ def testNitrogenTypes(self):
"""
Test that getAtomType() returns appropriate nitrogen atom types.
"""
self.assertEqual(self.atomType(self.mol23, 3), 'N1sc')
self.assertEqual(self.atomType(self.mol24, 0), 'N1s')
self.assertEqual(self.atomType(self.mol40, 3), 'N1sc')
self.assertEqual(self.atomType(self.mol41, 0), 'N1s')
self.assertEqual(self.atomType(self.mol5, 3), 'N1d')
self.assertEqual(self.atomType(self.mol22, 0), 'N2s')
self.assertEqual(self.atomType(self.mol39, 0), 'N2s')
self.assertEqual(self.atomType(self.mol9, 0), 'N3s')
self.assertEqual(self.atomType(self.mol10, 0), 'N3s')
self.assertEqual(self.atomType(self.mol11, 0), 'N3s')
Expand All @@ -252,6 +361,7 @@ def testNitrogenTypes(self):
self.assertEqual(self.atomType(self.mol5, 2), 'N5d')
self.assertEqual(self.atomType(self.mol14, 1), 'N5dd')
self.assertEqual(self.atomType(self.mol15, 1), 'N5t')
self.assertEqual(self.atomType(self.mol39, 2), 'N5t')
self.assertEqual(self.atomType(self.mol18, 0), 'N5b')

def testOxygenTypes(self):
Expand All @@ -276,9 +386,31 @@ def testSulfurTypes(self):
"""
Test that getAtomType() returns appropriate sulfur atom types.
"""
self.assertEqual(self.atomType(self.mol4, 8), 'Ss')
self.assertEqual(self.atomType(self.mol4, 10), 'Sd')
self.assertEqual(self.atomType(self.mol21, 1), 'St')
self.assertEqual(self.atomType(self.mol21, 0), 'S0s')
self.assertEqual(self.atomType(self.mol22, 0), 'Sa')
self.assertEqual(self.atomType(self.mol23, 0), 'S2s')
#self.assertEqual(self.atomType(self.mol21, 1), 'S2sp')
#self.assertEqual(self.atomType(self.mol42, 2), 'S2sn')
self.assertEqual(self.atomType(self.mol19, 1), 'S2d')
#self.assertEqual(self.atomType(self.mol24, 1), 'S2dc')
self.assertEqual(self.atomType(self.mol25, 0), 'S4s')
#self.assertEqual(self.atomType(self.mol23, 1), 'S4sc')
self.assertEqual(self.atomType(self.mol25, 2), 'S4d')
#self.assertEqual(self.atomType(self.mol26, 1), 'S4dc')
#self.assertEqual(self.atomType(self.mol27, 0), 'S4b') # RMG correctly can't represent heteroatoms in aromatics. See RMG-Py issue #982
self.assertEqual(self.atomType(self.mol28, 1), 'S4dd')
self.assertEqual(self.atomType(self.mol38, 1), 'S4dd')
self.assertEqual(self.atomType(self.mol28, 4), 'S4t')
#self.assertEqual(self.atomType(self.mol29, 1), 'S4tc')
self.assertEqual(self.atomType(self.mol28, 5), 'S6s')
self.assertEqual(self.atomType(self.mol30, 0), 'S6d')
#self.assertEqual(self.atomType(self.mol31, 1), 'S6dc')
self.assertEqual(self.atomType(self.mol32, 1), 'S6dd')
#self.assertEqual(self.atomType(self.mol33, 1), 'S6ddc')
self.assertEqual(self.atomType(self.mol34, 1), 'S6ddd')
self.assertEqual(self.atomType(self.mol35, 0), 'S6t')
self.assertEqual(self.atomType(self.mol36, 0), 'S6td')
self.assertEqual(self.atomType(self.mol37, 1), 'S6tt')

def testOtherTypes(self):
"""
Expand All @@ -292,3 +424,4 @@ def testOtherTypes(self):

if __name__ == '__main__':
unittest.main(testRunner=unittest.TextTestRunner(verbosity=2))

3 changes: 2 additions & 1 deletion rmgpy/molecule/atomtypedatabase.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,8 @@ def create_atom_types():
#bivalent:
bivalent = []
for type in [Xs, Xd]:
bivalent.extend(create_types(type, ['O', 'S']))
#bivalent.extend(create_types(type, ['O', 'S']))
bivalent.extend(create_types(type, ['O']))

for at in bivalent: at.lp = 2

Expand Down
6 changes: 3 additions & 3 deletions rmgpy/molecule/group.py
Original file line number Diff line number Diff line change
Expand Up @@ -1383,7 +1383,7 @@ def createAndConnectAtom(self, atomtypes, connectingAtom, bondOrders):

def addExplicitLigands(self):
"""
This function Od/Sd ligand to CO or CS atomtypes if they are not already there.
This function Od/S2d ligand to CO or CS atomtypes if they are not already there.

Returns a 'True' if the group was modified otherwise returns 'False'
"""
Expand All @@ -1408,7 +1408,7 @@ def addExplicitLigands(self):
if self.atoms[atomIndex].atomType[0] is atomTypes['CO']:
atomtypes = ['Od']
elif self.atoms[atomIndex].atomType[0] is atomTypes['CS']:
atomtypes = ['Sd']
atomtypes = ['S2d']
self.createAndConnectAtom(atomtypes, self.atoms[atomIndex], [2])

return modified
Expand All @@ -1418,7 +1418,7 @@ def standardizeGroup(self):
This function modifies groups to make them have a standard AdjList form.

Currently it makes atomtypes as specific as possible and makes CO/CS atomtypes
have explicit Od/Sd ligands. Other functions can be added as necessary
have explicit Od/S2d ligands. Other functions can be added as necessary

Returns a 'True' if the group was modified otherwise returns 'False'
"""
Expand Down
4 changes: 3 additions & 1 deletion rmgpy/molecule/pathfinder.pxd
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ cpdef dict compute_atom_distance(list atom_indices, Molecule mol)

cpdef list findAllDelocalizationPaths(Atom atom1)

cpdef list findAllDelocalizationPathsLonePairRadical(Atom atom1)
cpdef list findAllDelocalizationPathsLonePairRadicalCharge(Atom atom1)

cpdef list findAllDelocalizationPathsLonePairRadicalDoubleBond(Atom atom1)

cpdef list findAllDelocalizationPathsN5dd_N5ts(Atom atom1)
48 changes: 43 additions & 5 deletions rmgpy/molecule/pathfinder.py
Original file line number Diff line number Diff line change
Expand Up @@ -260,7 +260,7 @@ def findAllDelocalizationPaths(atom1):
paths.append([atom1, atom2, atom3, bond12, bond23])
return paths

def findAllDelocalizationPathsLonePairRadical(atom1):
def findAllDelocalizationPathsLonePairRadicalCharge(atom1):
"""
Find all the delocalization paths of lone electron pairs next to the radical center indicated
by `atom1`. Used to generate resonance isomers.
Expand All @@ -271,9 +271,8 @@ def findAllDelocalizationPathsLonePairRadical(atom1):
# No paths if atom1 is not a radical
if atom1.radicalElectrons <= 0:
return []

# In a first step we only consider nitrogen and oxygen atoms as possible radical centers
if not ((atom1.lonePairs == 0 and atom1.isNitrogen()) or(atom1.lonePairs == 2 and atom1.isOxygen())):
# In a first step we only consider nitrogen, sulfur and oxygen atoms as possible radical centers
if not ((atom1.lonePairs == 0 and atom1.isNitrogen()) or(atom1.lonePairs == 2 and atom1.isOxygen()) or (atom1.lonePairs <= 1 and atom1.isSulfur())):
return []

# Find all delocalization paths
Expand All @@ -282,11 +281,50 @@ def findAllDelocalizationPathsLonePairRadical(atom1):
# Only single bonds are considered
if bond12.isSingle():
# Neighboring atom must posses a lone electron pair to loose it
if ((atom2.lonePairs == 1 and atom2.isNitrogen()) or (atom2.lonePairs == 3 and atom2.isOxygen())) and (atom2.radicalElectrons == 0):
if ((atom2.lonePairs == 1 and atom2.isNitrogen()) or (atom2.lonePairs == 3 and atom2.isOxygen()) or (atom2.lonePairs == 2 and atom2.isSulfur())) and (atom2.radicalElectrons == 0):
paths.append([atom1, atom2])

return paths

def findAllDelocalizationPathsLonePairRadicalDoubleBond(atom1):
"""
Find all the delocalization paths of lone electron pairs next to the radical center indicated
by `atom1`. Used to generate resonance isomers.
"""
cython.declare(paths=list)
cython.declare(atom2=Atom, bond12=Bond)

# No paths if atom1 is not a radical
if atom1.radicalElectrons <= 0:
return []
if atom1.charge != 0:
return []

# Find all delocalization paths
#radical splits a lone pair on a sulfur atom
paths = []
for atom2, bond12 in atom1.edges.items():
if atom2.charge != 0:
continue
# Only single bonds are considered
if bond12.isSingle():
if (atom2.lonePairs >= 1 and atom2.isSulfur()) and (atom2.radicalElectrons == 0):
paths.append([atom1, atom2, bond12, 1])

#sulfur radical resonates to form a lone pair and radical on the adjacent atom
#only consider sulfur radicals that have less than 2 lone pairs (i.e. can form another lone pair)
if not (atom1.lonePairs <= 1 and atom1.isSulfur()):
return paths

#cycle through adjacent atoms until you find one that is not a radical itself and connected to atom1 by a double or triple bond
for atom2, bond12 in atom1.edges.items():
if atom2.charge != 0:
continue
if bond12.isDouble() or bond12.isTriple():
if atom2.radicalElectrons == 0:
paths.append([atom1, atom2, bond12, 2])
return paths

def findAllDelocalizationPathsN5dd_N5ts(atom1):
"""
Find all the resonance structures of nitrogen atoms with two double bonds (N5dd)
Expand Down
4 changes: 3 additions & 1 deletion rmgpy/molecule/resonance.pxd
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@ cpdef list _generateResonanceStructures(list molList, list methodList, bint keep

cpdef list generateAdjacentResonanceStructures(Molecule mol)

cpdef list generateLonePairRadicalResonanceStructures(Molecule mol)
cpdef list generateLonePairRadicalResonanceChargeStructures(Molecule mol)

cpdef list generateLonePairRadicalResonanceDoubleBondStructures(Molecule mol)

cpdef list generateN5dd_N5tsResonanceStructures(Molecule mol)

Expand Down
Loading