Skip to content

Commit efb4d1d

Browse files
committed
Merge pull request #364 from bslakman/master
Fix to solvation thermo for lone pairs
2 parents 1df1c3f + c9625a5 commit efb4d1d

File tree

4 files changed

+241
-90
lines changed

4 files changed

+241
-90
lines changed

documentation/source/users/rmg/liquids.rst

Lines changed: 27 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ dipole interactions related to the polarizability of the solvent) [Vitha2006]_,
108108
`lL` term accounts for the contribution from cavity formation and dispersion (dispersion interactions are
109109
known to scale with solute volume [Vitha2006]_, [Abraham1999]_. The `eE` term, like the `sS` term,
110110
accounts for residual contributions from dipolarity/polarizability related interactions for solutes
111-
whose blend of dipolarity/polarizability differs from that implicitly built into the `S` parameter [Vitha2006]_, [Abraham1990]_, [Jalan2010]_.
111+
whose blend of dipolarity/polarizability differs from that implicitly built into the `S` parameter [Vitha2006]_, [Abraham1999]_, [Jalan2010]_.
112112
The `aA` and `bB` terms account for the contribution of hydrogen bonding between the solute and
113113
the surrounding solvent molecules. H-bonding interactions require two terms as the solute (or solvent)
114114
can act as acceptor (donor) and vice versa. The descriptor `A` is a measure of the solute's ability
@@ -249,18 +249,30 @@ This is an example of an input file for a liquid-phase system::
249249
saveSimulationProfiles=True,
250250
)
251251

252-
.. [Vitha2006] \ Vitha2006
253-
.. [Abrham1999] \ Abraham1999
254-
.. [Jalan2010] \ Jalan2010
255-
.. [Poole2009] \ Poole2009
256-
.. [Platts1999] \ Platts1999
257-
.. [Mintz2007] \ Mintz2007
258-
.. [Mintz2007a] \ Mintz2007a
259-
.. [Mintz2007b] \ Mintz2007b
260-
.. [Mintz2007c] \ Mintz2007c
261-
.. [Mintz2007d] \ Mintz2007d
262-
.. [Mintz2008] \ Mintz2008
263-
.. [Mintz2008a] \ Mintz2008a
264-
.. [Mintz2009] \ Mintz2009
265-
.. [Rice1985] \ Rice1985
252+
.. [Vitha2006] \ M. Vitha and P.W. Carr. "The chemical interpretation and practice of linear solvation energy relationships in chromatography." *J. Chromatogr. A.* **1126(1-2)**, p. 143-194 (2006).
266253
254+
.. [Abraham1999] \ M.H. Abraham et al. "Correlation and estimation of gas-chloroform and water-chloroformpartition coefficients by a linear free energy relationship method." *J. Pharm. Sci.* **88(7)**, p. 670-679 (1999).
255+
256+
.. [Jalan2010] \ A. Jalan et al. "Predicting solvation energies for kinetic modeling." *Annu. Rep.Prog. Chem., Sect. C* **106**, p. 211-258 (2010).
257+
258+
.. [Poole2009] \ C.F. Poole et al. "Determination of solute descriptors by chromatographic methods." *Anal. Chim. Acta* **652(1-2)** p. 32-53 (2009).
259+
260+
.. [Platts1999] \ J. Platts and D. Butina. "Estimation of molecular linear free energy relation descriptorsusing a group contribution approach." *J. Chem. Inf. Comput. Sci.* **39**, p. 835-845 (1999).
261+
262+
.. [Mintz2007] \ C. Mintz et al. "Enthalpy of solvation correlations for gaseous solutes dissolved inwater and in 1-octanol based on the Abraham model." *J. Chem. Inf. Model.* **47(1)**, p. 115-121 (2007).
263+
264+
.. [Mintz2007a] \ C. Mintz et al. "Enthalpy of solvation corrections for gaseous solutes dissolved in benzene and in alkane solvents based on the Abraham model." *QSAR Comb. Sci.* **26(8)**, p. 881-888 (2007).
265+
266+
.. [Mintz2007b] \ C. Mintz et al. "Enthalpy of solvation correlations for gaseous solutes dissolved in toluene and carbon tetrachloride based on the Abraham model." *J. Sol. Chem.* **36(8)**, p. 947-966 (2007).
267+
268+
.. [Mintz2007c] \ C. Mintz et al. "Enthalpy of solvation correlations for gaseous solutes dissolved indimethyl sulfoxide and propylene carbonate based on the Abraham model." *Thermochim. Acta* **459(1-2)**, p, 17-25 (2007).
269+
270+
.. [Mintz2007d] \ C. Mintz et al. "Enthalpy of solvation correlations for gaseous solutes dissolved inchloroform and 1,2-dichloroethane based on the Abraham model." *Fluid Phase Equilib.* **258(2)**, p. 191-198 (2007).
271+
272+
.. [Mintz2008] \ C. Mintz et al. "Enthalpy of solvation correlations for gaseous solutes dissolved inlinear alkanes (C5-C16) based on the Abraham model." *QSAR Comb. Sci.* **27(2)**, p. 179-186 (2008).
273+
274+
.. [Mintz2008a] \ C. Mintz et al. "Enthalpy of solvation correlations for gaseous solutes dissolved inalcohol solvents based on the Abraham model." *QSAR Comb. Sci.* **27(5)**, p. 627-635 (2008).
275+
276+
.. [Mintz2009] \ C. Mintz et al. "Enthalpy of solvation correlations for organic solutes and gasesdissolved in acetonitrile and acetone." *Thermochim. Acta* **484(1-2)**, p. 65-69 (2009).
277+
278+
.. [Rice1985] \ S.A. Rice. "Diffusion-limited reactions". In *Comprehensive Chemical Kinetics*, EditorsC.H. Bamford, C.F.H. Tipper and R.G. Compton. **25**, (1985).

rmgpy/data/solvation.py

Lines changed: 151 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@
3838
from copy import deepcopy
3939
from base import Database, Entry, makeLogicNode, DatabaseError
4040

41-
from rmgpy.molecule import Molecule, Atom, Bond, Group
41+
from rmgpy.molecule import Molecule, Atom, Bond, Group, atomTypes
4242

4343
################################################################################
4444

@@ -484,13 +484,14 @@ def loadGroups(self, path):
484484
Load the solute database from the given `path` on disk, where `path`
485485
points to the top-level folder of the solute database.
486486
487-
Two sets of groups for additivity, atom-centered ('abraham') and non atom-centered
488-
('nonacentered').
487+
Three sets of groups for additivity, atom-centered ('abraham'), non atom-centered
488+
('nonacentered'), and radical corrections ('radical')
489489
"""
490490
logging.info('Loading Platts additivity group database from {0}...'.format(path))
491491
self.groups = {}
492492
self.groups['abraham'] = SoluteGroups(label='abraham').load(os.path.join(path, 'abraham.py' ), self.local_context, self.global_context)
493493
self.groups['nonacentered'] = SoluteGroups(label='nonacentered').load(os.path.join(path, 'nonacentered.py' ), self.local_context, self.global_context)
494+
self.groups['radical'] = SoluteGroups(label='radical').load(os.path.join(path, 'radical.py' ), self.local_context, self.global_context)
494495

495496
def save(self, path):
496497
"""
@@ -519,6 +520,7 @@ def saveGroups(self, path):
519520
if not os.path.exists(path): os.mkdir(path)
520521
self.groups['abraham'].save(os.path.join(path, 'abraham.py'))
521522
self.groups['nonacentered'].save(os.path.join(path, 'nonacentered.py'))
523+
self.groups['radical'].save(os.path.join(path, 'radical.py'))
522524

523525
def loadOld(self, path):
524526
"""
@@ -672,7 +674,117 @@ def getSoluteDataFromGroups(self, species):
672674
soluteData.comment = "Average of {0}".format(" and ".join(comments))
673675

674676
return soluteData
677+
678+
def saturateRadicals(self, molecule):
679+
saturatedStruct = molecule.copy(deep=True)
680+
681+
# Saturate structure by replacing all radicals with bonds to
682+
# hydrogen atoms
683+
added = {}
684+
for atom in saturatedStruct.atoms:
685+
for i in range(atom.radicalElectrons):
686+
H = Atom('H')
687+
bond = Bond(atom, H, 'S')
688+
saturatedStruct.addAtom(H)
689+
saturatedStruct.addBond(bond)
690+
if atom not in added:
691+
added[atom] = []
692+
added[atom].append([H, bond])
693+
atom.decrementRadical()
694+
695+
# Update the atom types of the saturated structure (not sure why
696+
# this is necessary, because saturating with H shouldn't be
697+
# changing atom types, but it doesn't hurt anything and is not
698+
# very expensive, so will do it anyway)
699+
saturatedStruct.updateConnectivityValues()
700+
saturatedStruct.sortVertices()
701+
saturatedStruct.updateAtomTypes()
702+
saturatedStruct.updateLonePairs()
703+
saturatedStruct.multiplicity = 1
704+
705+
return saturatedStruct, added
706+
707+
def transformLonePairs(self, molecule):
708+
"""
709+
Changes lone pairs in a molecule to two radicals for purposes of finding
710+
solute data via group additivity. Transformed for each atom based on valency.
711+
"""
712+
saturatedStruct = molecule.copy(deep=True)
713+
addedToPairs = {}
714+
715+
for atom in saturatedStruct.atoms:
716+
addedToPairs[atom] = 0
717+
if atom.lonePairs > 0:
718+
charge = atom.charge # Record this so we can conserve it when checking
719+
bonds = saturatedStruct.getBonds(atom)
720+
sumBondOrders = 0
721+
for key, bond in bonds.iteritems():
722+
if bond.order == 'S': sumBondOrders += 1
723+
if bond.order == 'D': sumBondOrders += 2
724+
if bond.order == 'T': sumBondOrders += 3
725+
if bond.order == 'B': sumBondOrders += 1.5 # We should always have 2 'B' bonds (but what about Cbf?)
726+
if atomTypes['Val4'] in atom.atomType.generic: # Carbon, Silicon
727+
while(atom.radicalElectrons + charge + sumBondOrders != 4):
728+
atom.decrementLonePairs()
729+
atom.incrementRadical()
730+
atom.incrementRadical()
731+
addedToPairs[atom] += 1
732+
if atomTypes['Val5'] in atom.atomType.generic: # Nitrogen
733+
while(atom.radicalElectrons + charge + sumBondOrders != 3):
734+
atom.decrementLonePairs()
735+
atom.incrementRadical()
736+
atom.incrementRadical()
737+
addedToPairs[atom] += 1
738+
if atomTypes['Val6'] in atom.atomType.generic: # Oxygen, sulfur
739+
while(atom.radicalElectrons + charge + sumBondOrders != 2):
740+
atom.decrementLonePairs()
741+
atom.incrementRadical()
742+
atom.incrementRadical()
743+
addedToPairs[atom] += 1
744+
if atomTypes['Val7'] in atom.atomType.generic: # Chlorine
745+
while(atom.radicalElectrons + charge + sumBondOrders != 1):
746+
atom.decrementLonePairs()
747+
atom.incrementRadical()
748+
atom.incrementRadical()
749+
addedToPairs[atom] += 1
750+
751+
saturatedStruct.updateConnectivityValues()
752+
saturatedStruct.sortVertices()
753+
saturatedStruct.updateAtomTypes()
754+
saturatedStruct.updateLonePairs()
755+
saturatedStruct.updateMultiplicity()
756+
757+
return saturatedStruct, addedToPairs
758+
759+
def removeHBonding(self, saturatedStruct, addedToRadicals, addedToPairs, soluteData):
675760

761+
# Remove hydrogen bonds and restore the radical
762+
for atom in addedToRadicals:
763+
for H, bond in addedToRadicals[atom]:
764+
saturatedStruct.removeBond(bond)
765+
saturatedStruct.removeAtom(H)
766+
atom.incrementRadical()
767+
768+
# Change transformed lone pairs back
769+
for atom in addedToPairs:
770+
if addedToPairs[atom] > 0:
771+
for pair in range(1, addedToPairs[atom]):
772+
saturatedStruct.decrementRadical()
773+
saturatedStruct.decrementRadical()
774+
saturatedStruct.incrementLonePairs()
775+
776+
# Update Abraham 'A' H-bonding parameter for unsaturated struct
777+
for atom in saturatedStruct.atoms:
778+
# Iterate over heavy (non-hydrogen) atoms
779+
if atom.isNonHydrogen() and atom.radicalElectrons > 0:
780+
for electron in range(1, atom.radicalElectrons):
781+
# Get solute data for radical group
782+
try:
783+
self.__addGroupSoluteData(soluteData, self.groups['radical'], saturatedStruct, {'*':atom})
784+
except KeyError: pass
785+
786+
return soluteData
787+
676788
def estimateSoluteViaGroupAdditivity(self, molecule):
677789
"""
678790
Return the set of Abraham solute parameters corresponding to a given
@@ -693,73 +805,43 @@ def estimateSoluteViaGroupAdditivity(self, molecule):
693805
L = 0.13,
694806
A = 0.003
695807
)
696-
697-
if sum([atom.radicalElectrons for atom in molecule.atoms]) > 0: # radical species
698-
699-
# Make a copy of the structure so we don't change the original
700-
saturatedStruct = molecule.copy(deep=True)
701-
702-
# Saturate structure by replacing all radicals with bonds to
703-
# hydrogen atoms
704-
added = {}
705-
for atom in saturatedStruct.atoms:
706-
for i in range(atom.radicalElectrons):
707-
H = Atom('H')
708-
bond = Bond(atom, H, 'S')
709-
saturatedStruct.addAtom(H)
710-
saturatedStruct.addBond(bond)
711-
if atom not in added:
712-
added[atom] = []
713-
added[atom].append([H, bond])
714-
atom.decrementRadical()
715-
716-
# Update the atom types of the saturated structure (not sure why
717-
# this is necessary, because saturating with H shouldn't be
718-
# changing atom types, but it doesn't hurt anything and is not
719-
# very expensive, so will do it anyway)
720-
saturatedStruct.updateConnectivityValues()
721-
saturatedStruct.sortVertices()
722-
saturatedStruct.updateAtomTypes()
723-
saturatedStruct.updateLonePairs()
724-
saturatedStruct.multiplicity = 1
725-
726-
# Get solute descriptor estimates for saturated form of structure
727-
soluteData = self.estimateSoluteViaGroupAdditivity(saturatedStruct)
728-
assert soluteData is not None, "Solute data of saturated {0} of molecule {1} is None!".format(saturatedStruct, molecule)
729-
730-
# For each radical site, get radical correction
731-
# Only one radical site should be considered at a time; all others
732-
# should be saturated with hydrogen atoms
733-
for atom in added:
734-
735-
# Remove the added hydrogen atoms and bond and restore the radical
736-
for H, bond in added[atom]:
737-
saturatedStruct.removeBond(bond)
738-
saturatedStruct.removeAtom(H)
739-
atom.incrementRadical()
740-
741-
saturatedStruct.updateConnectivityValues()
742-
743-
else: # non-radical species
744-
# Generate estimate of solute data
745-
for atom in molecule.atoms:
746-
# Iterate over heavy (non-hydrogen) atoms
747-
if atom.isNonHydrogen():
748-
# Get initial solute data from main group database. Every atom must
749-
# be found in the main abraham database
750-
try:
751-
self.__addGroupSoluteData(soluteData, self.groups['abraham'], molecule, {'*':atom})
752-
except KeyError:
753-
logging.error("Couldn't find in main abraham database:")
754-
logging.error(molecule)
755-
logging.error(molecule.toAdjacencyList())
756-
raise
757-
# Get solute data for non-atom centered groups (being found in this group
758-
# database is optional)
759-
try:
760-
self.__addGroupSoluteData(soluteData, self.groups['nonacentered'], molecule, {'*':atom})
761-
except KeyError: pass
762808

809+
addedToRadicals = {} # Dictionary of key = atom, value = dictionary of {H atom: bond}
810+
addedToPairs = {} # Dictionary of key = atom, value = # lone pairs changed
811+
saturatedStruct = molecule.copy(deep=True)
812+
813+
# Convert lone pairs to radicals, then saturate with H.
814+
815+
# Change lone pairs to radicals based on valency
816+
if sum([atom.lonePairs for atom in saturatedStruct.atoms]) > 0: # molecule contains lone pairs
817+
saturatedStruct, addedToPairs = self.transformLonePairs(saturatedStruct)
818+
819+
# Now saturate radicals with H
820+
if sum([atom.radicalElectrons for atom in saturatedStruct.atoms]) > 0: # radical species
821+
saturatedStruct, addedToRadicals = self.saturateRadicals(saturatedStruct)
822+
823+
# Saturated structure should now have no unpaired electrons, and only "expected" lone pairs
824+
# based on the valency
825+
for atom in saturatedStruct.atoms:
826+
# Iterate over heavy (non-hydrogen) atoms
827+
if atom.isNonHydrogen():
828+
# Get initial solute data from main group database. Every atom must
829+
# be found in the main abraham database
830+
try:
831+
self.__addGroupSoluteData(soluteData, self.groups['abraham'], saturatedStruct, {'*':atom})
832+
except KeyError:
833+
logging.error("Couldn't find in main abraham database:")
834+
logging.error(saturatedStruct)
835+
logging.error(saturatedStruct.toAdjacencyList())
836+
raise
837+
# Get solute data for non-atom centered groups (being found in this group
838+
# database is optional)
839+
try:
840+
self.__addGroupSoluteData(soluteData, self.groups['nonacentered'], saturatedStruct, {'*':atom})
841+
except KeyError: pass
842+
843+
soluteData = self.removeHBonding(saturatedStruct, addedToRadicals, addedToPairs, soluteData)
844+
763845
return soluteData
764846

765847
def __addGroupSoluteData(self, soluteData, database, molecule, atom):

rmgpy/data/solvationTest.py

Lines changed: 62 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -77,12 +77,7 @@ def testSoluteGeneration(self):
7777
"Test we can estimate Abraham solute parameters correctly using group contributions"
7878

7979
self.testCases = [
80-
# from RMG-Java test runs by RWest (mostly in agreement with Jalan et. al. supplementary data)
8180
['1,2-ethanediol', 'C(CO)O', 0.823, 0.685, 0.327, 2.572, 0.693, None],
82-
# a nitrogen case
83-
#['acetonitrile', 'CC#N', 0.9, 0.33, 0.237, 1.739, 0.04, None],
84-
# a sulfur case
85-
#['ethanethiol', 'CCS', 0.35, 0.24, 0.392, 2.173, 0, None]
8681
]
8782

8883
for name, smiles, S, B, E, L, A, V in self.testCases:
@@ -94,6 +89,68 @@ def testSoluteGeneration(self):
9489
self.assertAlmostEqual(soluteData.L, L, places=2)
9590
self.assertAlmostEqual(soluteData.A, A, places=2)
9691

92+
def testLonePairSoluteGeneration(self):
93+
"Test we can obtain solute parameters via group additivity for a molecule with lone pairs"
94+
molecule=Molecule().fromAdjacencyList(
95+
"""
96+
CH2_singlet
97+
multiplicity 1
98+
1 C u0 p1 c0 {2,S} {3,S}
99+
2 H u0 p0 c0 {1,S}
100+
3 H u0 p0 c0 {1,S}
101+
""")
102+
species = Species(molecule=[molecule])
103+
soluteData = self.database.getSoluteDataFromGroups(species)
104+
self.assertTrue(soluteData is not None)
105+
106+
def testSoluteDataGenerationAmmonia(self):
107+
"Test we can obtain solute parameters via group additivity for ammonia"
108+
molecule=Molecule().fromAdjacencyList(
109+
"""
110+
1 N u0 p1 c0 {2,S} {3,S} {4,S}
111+
2 H u0 p0 c0 {1,S}
112+
3 H u0 p0 c0 {1,S}
113+
4 H u0 p0 c0 {1,S}
114+
""")
115+
species = Species(molecule=[molecule])
116+
soluteData = self.database.getSoluteDataFromGroups(species)
117+
self.assertTrue(soluteData is not None)
118+
119+
def testSoluteDataGenerationAmide(self):
120+
"Test that we can obtain solute parameters via group additivity for an amide"
121+
molecule=Molecule().fromAdjacencyList(
122+
"""
123+
1 N u0 p1 {2,S} {3,S} {4,S}
124+
2 H u0 {1,S}
125+
3 C u0 {1,S} {6,S} {7,S} {8,S}
126+
4 C u0 {1,S} {5,D} {9,S}
127+
5 O u0 p2 {4,D}
128+
6 H u0 {3,S}
129+
7 H u0 {3,S}
130+
8 H u0 {3,S}
131+
9 H u0 {4,S}
132+
""")
133+
species = Species(molecule=[molecule])
134+
soluteData = self.database.getSoluteDataFromGroups(species)
135+
self.assertTrue(soluteData is not None)
136+
137+
def testRadicalandLonePairGeneration(self):
138+
"""
139+
Test we can obtain solute parameters via group additivity for a molecule with both lone
140+
pairs and a radical
141+
"""
142+
molecule=Molecule().fromAdjacencyList(
143+
"""
144+
[C]OH
145+
multiplicity 2
146+
1 C u1 p1 c0 {2,S}
147+
2 O u0 p2 c0 {1,S} {3,S}
148+
3 H u0 p0 c0 {2,S}
149+
""")
150+
species = Species(molecule=[molecule])
151+
soluteData = self.database.getSoluteDataFromGroups(species)
152+
self.assertTrue(soluteData is not None)
153+
97154
def testCorrectionGeneration(self):
98155
"Test we can estimate solvation thermochemistry."
99156
self.testCases = [

0 commit comments

Comments
 (0)