Skip to content

Commit 9901968

Browse files
authored
Merge pull request ReactionMechanismGenerator#2706 from ReactionMechanismGenerator/vdW_bonds
Vdw bonds
2 parents 0e7ce93 + 056291b commit 9901968

11 files changed

Lines changed: 330 additions & 23 deletions

File tree

rmgpy/data/kinetics/family.py

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1655,8 +1655,7 @@ def _generate_product_structures(self, reactant_structures, maps, forward, relab
16551655
for struct in product_structures:
16561656
if self.is_molecule_forbidden(struct):
16571657
raise ForbiddenStructureException()
1658-
reason = fails_species_constraints(struct)
1659-
if reason:
1658+
if (reason := fails_species_constraints(struct)):
16601659
raise ForbiddenStructureException(
16611660
"Species constraints forbids product species {0}. Please "
16621661
"reformulate constraints, or explicitly "
@@ -1676,11 +1675,17 @@ def is_molecule_forbidden(self, molecule):
16761675
return True
16771676

16781677
# forbid vdw multi-dentate molecules for surface families
1679-
if "surface" in self.label.lower():
1680-
if molecule.get_num_atoms('X') > 1:
1681-
for atom in molecule.atoms:
1682-
if atom.atomtype.label == 'Xv':
1683-
return True
1678+
if "surface" in self.label.lower() and molecule.is_multidentate():
1679+
if "vdwbidentate" in self.label.lower():
1680+
# Within vdWBidentate families, allow vdW in
1681+
# multi-dentate molecules if at least one bond to the surface
1682+
# is covalent.
1683+
if not molecule.has_covalent_surface_bond():
1684+
return True
1685+
else:
1686+
# for all other families, forbid multi-dentate molecules with any vdW bonds
1687+
if molecule.has_vdw_surface_bond():
1688+
return True
16841689

16851690
return False
16861691

rmgpy/molecule/converter.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -99,10 +99,10 @@ def to_rdkit_mol(mol, remove_h=True, return_mapping=False, sanitize=True,
9999
label_dict[index] = atom.label
100100

101101
rd_bonds = Chem.rdchem.BondType
102-
# no vdW bond in RDKit, so "ZERO" or "OTHER" might be OK
102+
# no vdW bond in RDKit, so use UNSPECIFIED
103103
orders = {'S': rd_bonds.SINGLE, 'D': rd_bonds.DOUBLE,
104104
'T': rd_bonds.TRIPLE, 'B': rd_bonds.AROMATIC,
105-
'Q': rd_bonds.QUADRUPLE, 'vdW': rd_bonds.ZERO,
105+
'Q': rd_bonds.QUADRUPLE, 'vdW': rd_bonds.UNSPECIFIED,
106106
'H': rd_bonds.HYDROGEN, 'R': rd_bonds.UNSPECIFIED,
107107
None: rd_bonds.UNSPECIFIED}
108108
# Add the bonds

rmgpy/molecule/draw.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1215,7 +1215,7 @@ def _render_bond(self, atom1, atom2, bond, cr):
12151215
dv *= 1.6
12161216
self._draw_line(cr, x1 - du, y1 - dv, x2 - du, y2 - dv)
12171217
self._draw_line(cr, x1 + du, y1 + dv, x2 + du, y2 + dv, dashed=True)
1218-
elif bond.is_hydrogen_bond():
1218+
elif bond.is_hydrogen_bond() or bond.is_van_der_waals():
12191219
# Draw a dashed line
12201220
self._draw_line(cr, x1, y1, x2, y2, dashed=True, dash_sizes=[0.5, 3.5])
12211221
else:

rmgpy/molecule/molecule.pxd

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,10 @@ cdef class Molecule(Graph):
206206

207207
cpdef int number_of_surface_sites(self) except -1
208208

209+
cpdef bint has_covalent_surface_bond(self)
210+
211+
cpdef bint has_vdw_surface_bond(self)
212+
209213
cpdef bint is_surface_site(self)
210214

211215
cpdef remove_atom(self, Atom atom)

rmgpy/molecule/molecule.py

Lines changed: 47 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@
6767
def _skip_first(in_tuple):
6868
return in_tuple[1:]
6969

70-
bond_orders = {'S': 1, 'D': 2, 'T': 3, 'B': 1.5}
70+
bond_orders = {'S': 1, 'D': 2, 'T': 3, 'B': 1.5, 'vdW': 0}
7171

7272
globals().update({
7373
'bond_orders': bond_orders,
@@ -1219,6 +1219,36 @@ def has_bond(self, atom1, atom2):
12191219
"""
12201220
return self.has_edge(atom1, atom2)
12211221

1222+
def has_covalent_surface_bond(self):
1223+
"""
1224+
Return True if any bond in this molecule connects a surface site (X) via a covalent bond.
1225+
"""
1226+
cython.declare(atom=Atom, bond=Bond)
1227+
for atom in self.atoms:
1228+
if atom.is_surface_site():
1229+
for bond in atom.bonds.values():
1230+
if not bond.is_van_der_waals():
1231+
return True
1232+
return False
1233+
1234+
def has_vdw_surface_bond(self):
1235+
"""
1236+
Return True if any bond in this molecule connects a surface site (X)
1237+
via a van der Waals bond, or there's a surface site with no bonds
1238+
(but at least one other atom in the molecule).
1239+
"""
1240+
cython.declare(atom=Atom, bond=Bond)
1241+
for atom in self.atoms:
1242+
if atom.is_surface_site():
1243+
if not atom.bonds: # if there are no bonds at all
1244+
if len(self.atoms) > 1: # and there's something besides the surface site
1245+
return True # then treat as vdW bonded
1246+
for bond in atom.bonds.values():
1247+
if bond.is_van_der_waals():
1248+
return True
1249+
1250+
return False
1251+
12221252
def contains_surface_site(self):
12231253
"""
12241254
Returns ``True`` iff the molecule contains an 'X' surface site.
@@ -1273,9 +1303,14 @@ def remove_bond(self, bond):
12731303

12741304
def remove_van_der_waals_bonds(self):
12751305
"""
1276-
Remove all van der Waals bonds.
1306+
Remove all van der Waals bonds. For multidentate species,
1307+
vdW bonds are preserved when there are still other
1308+
covalent bonds with the surface present. If no covalent surface bonds are present,
1309+
all vdW bonds are removed.
12771310
"""
12781311
cython.declare(bond=Bond)
1312+
if self.has_covalent_surface_bond():
1313+
return # preserve any vdW bonds if there's also a covalent X
12791314
for bond in self.get_all_edges():
12801315
if bond.is_van_der_waals():
12811316
self.remove_bond(bond)
@@ -3048,9 +3083,13 @@ def is_multidentate(self):
30483083
Return ``True`` if the adsorbate contains at least two binding sites,
30493084
or ``False`` otherwise.
30503085
"""
3051-
cython.declare(atom=Atom)
3052-
if len([atom for atom in self.vertices if atom.is_surface_site()])>=2:
3053-
return True
3086+
cython.declare(atom=Atom, found_one=cython.bint)
3087+
found_one = False
3088+
for atom in self.atoms:
3089+
if atom.is_surface_site():
3090+
if found_one:
3091+
return True
3092+
found_one = True
30543093
return False
30553094

30563095
def get_adatoms(self):
@@ -3076,6 +3115,7 @@ def get_desorbed_molecules(self):
30763115
``*2`` - double bond
30773116
``*3`` - triple bond
30783117
``*4`` - quadruple bond
3118+
``*0`` - vdW bond
30793119
"""
30803120
cython.declare(desorbed_molecules=list, desorbed_molecule=Molecule, sites_to_remove=list, adsorbed_atoms=list,
30813121
site=Atom, numbonds=cython.int, bonded_atom=Atom, bond=Bond, i=cython.int, j=cython.int, atom0=Atom,
@@ -3114,6 +3154,8 @@ def get_desorbed_molecules(self):
31143154
bonded_atom.increment_radical()
31153155
bonded_atom.increment_lone_pairs()
31163156
bonded_atom.label = '*4'
3157+
elif bond.is_van_der_waals():
3158+
bonded_atom.label = '*0'
31173159
else:
31183160
raise NotImplementedError("Can't remove surface bond of type {}".format(bond.order))
31193161
desorbed_molecule.remove_atom(site)

rmgpy/molecule/pathfinder.pxd

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
###############################################################################
2727

2828
from .graph cimport Vertex, Edge, Graph
29+
from .molecule cimport Atom, Bond, Molecule
2930

3031
cpdef list find_butadiene(Vertex start, Vertex end)
3132

@@ -61,4 +62,6 @@ cpdef bint is_atom_able_to_lose_lone_pair(Vertex atom)
6162

6263
cpdef list find_adsorbate_delocalization_paths(Vertex atom1)
6364

64-
cpdef list find_adsorbate_conjugate_delocalization_paths(Vertex atom1)
65+
cpdef list find_adsorbate_conjugate_delocalization_paths(Vertex atom1)
66+
67+
cpdef list find_formate_delocalization_paths(Vertex atom1)

rmgpy/molecule/pathfinder.py

Lines changed: 58 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@
3636

3737
import cython
3838

39-
from rmgpy.molecule.molecule import Atom
39+
from rmgpy.molecule.molecule import Atom, Bond
4040
from rmgpy.molecule.graph import Vertex, Edge
4141

4242
def find_butadiene(start, end):
@@ -494,7 +494,15 @@ def find_adsorbate_delocalization_paths(atom1):
494494
In this transition atom1 and atom4 are surface sites while atom2 and atom3
495495
are carbon or nitrogen atoms.
496496
"""
497-
cython.declare(paths=list, atom2=Vertex, atom3=Vertex, atom4=Vertex, bond12=Edge, bond23=Edge, bond34=Edge)
497+
cython.declare(
498+
paths=list,
499+
atom2=Atom,
500+
atom3=Atom,
501+
atom4=Atom,
502+
bond12=Bond,
503+
bond23=Bond,
504+
bond34=Bond,
505+
)
498506

499507
paths = []
500508
if atom1.is_surface_site():
@@ -521,7 +529,17 @@ def find_adsorbate_conjugate_delocalization_paths(atom1):
521529
and atom4 are carbon or nitrogen atoms.
522530
"""
523531

524-
cython.declare(paths=list, atom2=Vertex, atom3=Vertex, atom4=Vertex, atom5=Vertex, bond12=Edge, bond23=Edge, bond34=Edge, bond45=Edge)
532+
cython.declare(
533+
paths=list,
534+
atom2=Atom,
535+
atom3=Atom,
536+
atom4=Atom,
537+
atom5=Atom,
538+
bond12=Bond,
539+
bond23=Bond,
540+
bond34=Bond,
541+
bond45=Bond,
542+
)
525543

526544
paths = []
527545
if atom1.is_surface_site():
@@ -535,3 +553,40 @@ def find_adsorbate_conjugate_delocalization_paths(atom1):
535553
if atom5.is_surface_site():
536554
paths.append([atom1, atom2, atom3, atom4, atom5, bond12, bond23, bond34, bond45])
537555
return paths
556+
557+
def find_formate_delocalization_paths(atom1):
558+
"""
559+
Find all resonance structures which have a bonding configuration X~O=C-O-X.
560+
Examples:
561+
562+
- [X]~OC(H)O[X]/[X]OC(H)O~[X], where '~' denotes a vdW bond and X is the surface site. The adsorption site X
563+
is always placed on the left-hand side of the adatom and every adatom
564+
is bonded to only one surface site X.
565+
566+
In this transition atom1 and atom5 are surface sites while atom2
567+
and atom4 are oxygen and atom3 is a carbon atom.
568+
"""
569+
570+
cython.declare(
571+
paths=list,
572+
atom2=Atom,
573+
atom3=Atom,
574+
atom4=Atom,
575+
atom5=Atom,
576+
bond12=Bond,
577+
bond23=Bond,
578+
bond34=Bond,
579+
bond45=Bond,
580+
)
581+
paths = []
582+
if atom1.is_surface_site():
583+
for atom2, bond12 in atom1.edges.items():
584+
if atom2.is_oxygen() and bond12.is_van_der_waals():
585+
for atom3, bond23 in atom2.edges.items():
586+
if (atom3.is_carbon() or atom3.is_nitrogen()) and bond23.is_double():
587+
for atom4, bond34 in atom3.edges.items():
588+
if atom2 is not atom4 and atom4.is_oxygen() and bond34.is_single():
589+
for atom5, bond45 in atom4.edges.items():
590+
if atom5.is_surface_site() and bond45.is_single():
591+
paths.append([atom1, atom2, atom3, atom4, atom5, bond12, bond23, bond34, bond45])
592+
return paths

rmgpy/molecule/resonance.pxd

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,3 +73,5 @@ cpdef list generate_adsorbate_shift_down_resonance_structures(Graph mol)
7373
cpdef list generate_adsorbate_shift_up_resonance_structures(Graph mol)
7474

7575
cpdef list generate_adsorbate_conjugate_resonance_structures(Graph mol)
76+
77+
cpdef list generate_adsorbate_formate_resonance_structures(Graph mol)

rmgpy/molecule/resonance.py

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,8 @@ def populate_resonance_algorithms(features=None):
9696
generate_clar_structures,
9797
generate_adsorbate_shift_down_resonance_structures,
9898
generate_adsorbate_shift_up_resonance_structures,
99-
generate_adsorbate_conjugate_resonance_structures
99+
generate_adsorbate_conjugate_resonance_structures,
100+
generate_adsorbate_formate_resonance_structures,
100101
]
101102
else:
102103
# If the molecule is aromatic, then radical resonance has already been considered
@@ -124,6 +125,7 @@ def populate_resonance_algorithms(features=None):
124125
method_list.append(generate_adsorbate_shift_down_resonance_structures)
125126
method_list.append(generate_adsorbate_shift_up_resonance_structures)
126127
method_list.append(generate_adsorbate_conjugate_resonance_structures)
128+
method_list.append(generate_adsorbate_formate_resonance_structures)
127129
return method_list
128130

129131

@@ -1257,3 +1259,43 @@ def generate_adsorbate_conjugate_resonance_structures(mol):
12571259
else:
12581260
structures.append(structure)
12591261
return structures
1262+
1263+
1264+
def generate_adsorbate_formate_resonance_structures(mol):
1265+
"""
1266+
Generate all resonance structures formed by the shift of two
1267+
electrons in a conjugated bonding system of a bidentate adsorbate
1268+
with a bridging atom in between, where one bond to the surface is vdW.
1269+
1270+
Example [X]OC(H)O[X]: [X]~OC(H)O[X] <=> [X]OC(H)O~[X]
1271+
(where '~' denotes a vdW bond)
1272+
"""
1273+
cython.declare(structures=list, paths=list, index=cython.int, structure=Graph)
1274+
cython.declare(atom=Vertex, atom1=Vertex, atom2=Vertex, atom3=Vertex, atom4=Vertex, atom5=Vertex, bond12=Edge, bond23=Edge, bond34=Edge, bond45=Edge)
1275+
cython.declare(v1=Vertex, v2=Vertex)
1276+
1277+
structures = []
1278+
if mol.is_multidentate():
1279+
for atom in mol.vertices:
1280+
paths = pathfinder.find_formate_delocalization_paths(atom)
1281+
for atom1, atom2, atom3, atom4, atom5, bond12, bond23, bond34, bond45 in paths:
1282+
if ((atom2.is_oxygen() and bond12.is_van_der_waals()) and
1283+
(atom4.is_oxygen() and atom5.is_surface_site() and
1284+
bond45.is_single() and bond23.is_double() and bond34.is_single())):
1285+
bond12.increment_order()
1286+
bond23.decrement_order()
1287+
bond34.increment_order()
1288+
bond45.decrement_order()
1289+
structure = mol.copy(deep=True)
1290+
bond12.decrement_order()
1291+
bond23.increment_order()
1292+
bond34.decrement_order()
1293+
bond45.increment_order()
1294+
try:
1295+
structure.update_atomtypes(log_species=False)
1296+
except AtomTypeError:
1297+
pass
1298+
else:
1299+
structures.append(structure)
1300+
1301+
return structures

test/database/databaseTest.py

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1577,7 +1577,22 @@ def make_error_message(reactants, message=""):
15771577
output += "\n" + s.to_adjacency_list(label=s.to_smiles())
15781578
return output
15791579

1580-
if len(sample_reactants) == 1 == len(family.forward_template.reactants):
1580+
expected_reactants = [str(r) for r in family.forward_template.reactants]
1581+
roots_with_samples = [str(k) for k in sample_reactants.keys()]
1582+
roots_without_samples = [r for r in expected_reactants if r not in roots_with_samples]
1583+
if len(sample_reactants) != len(family.forward_template.reactants):
1584+
# One or more reactant roots produced no usable sample molecule (every candidate was
1585+
# forbidden by is_molecule_forbidden, or raised UnexpectedChargeError/
1586+
# ImplicitBenzeneError during make_sample_molecule). Record a descriptive error so it
1587+
# is logged below alongside any per-sample errors, instead of raising opaquely here.
1588+
test1.append(
1589+
f"In family {family_name}, {len(roots_without_samples)} reactant root(s) produced "
1590+
f"no usable sample molecule: {roots_without_samples}. The family template expects "
1591+
f"{len(expected_reactants)} reactant(s) {expected_reactants}; only these root(s) "
1592+
f"yielded samples: {roots_with_samples}. Check the group definitions for the "
1593+
f"missing reactant root(s)."
1594+
)
1595+
elif len(sample_reactants) == 1:
15811596
reactants = list(sample_reactants.values())[0]
15821597
for reactant in reactants:
15831598
try:
@@ -1670,7 +1685,13 @@ def make_error_message(reactants, message=""):
16701685
species = rmgpy.species.Species(index=1, molecule=[molecule])
16711686
species.generate_resonance_structures()
16721687
else:
1673-
raise ValueError(f"Family had {len(sample_reactants)} reactants?: " f"{', '.join(map(str,sample_reactants.keys())) }")
1688+
# Reactant count matches the template but is not 1, 2, or 3 (RMG only supports up to
1689+
# trimolecular). This is not expected for any well-formed family.
1690+
raise ValueError(
1691+
f"In family {family_name}, the number of sampled reactant roots "
1692+
f"({len(sample_reactants)}) matches the template reactant count but is not 1, 2, "
1693+
f"or 3, which is unexpected: {roots_with_samples}."
1694+
)
16741695

16751696
# print out entries skipped from exception we can't currently handle
16761697
if skipped:

0 commit comments

Comments
 (0)