rdchiral-plus 0.5.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
rdchiral/__init__.py ADDED
@@ -0,0 +1,29 @@
1
+ from importlib.metadata import PackageNotFoundError, version
2
+
3
+ from rdkit import RDLogger
4
+
5
+ from rdchiral.initialization import rdchiralReactants, rdchiralReaction
6
+ from rdchiral.main import rdchiralRun, rdchiralRunText
7
+ from rdchiral.template_extractor import (
8
+ DEFAULT_EXTRACTED_TEMPLATE,
9
+ extract_from_reaction,
10
+ extract_from_reaction_smiles,
11
+ )
12
+
13
+ lg = RDLogger.logger()
14
+ lg.setLevel(RDLogger.CRITICAL)
15
+
16
+ __all__ = [
17
+ "DEFAULT_EXTRACTED_TEMPLATE",
18
+ "extract_from_reaction",
19
+ "extract_from_reaction_smiles",
20
+ "rdchiralReactants",
21
+ "rdchiralReaction",
22
+ "rdchiralRun",
23
+ "rdchiralRunText",
24
+ ]
25
+
26
+ try:
27
+ __version__ = version("rdchiral_plus")
28
+ except PackageNotFoundError: # pragma: no cover
29
+ __version__ = "unknown"
rdchiral/bonds.py ADDED
@@ -0,0 +1,609 @@
1
+ from typing import Dict, Iterator, List, Optional, Set, Tuple
2
+
3
+ from rdkit import Chem
4
+ from rdkit.Chem.rdchem import BondDir, BondType
5
+
6
+ BondDirOpposite = {
7
+ BondDir.ENDUPRIGHT: BondDir.ENDDOWNRIGHT,
8
+ BondDir.ENDDOWNRIGHT: BondDir.ENDUPRIGHT,
9
+ BondDir.NONE: BondDir.NONE,
10
+ }
11
+ BondDirLabel = {BondDir.ENDUPRIGHT: "\\", BondDir.ENDDOWNRIGHT: "/"}
12
+
13
+
14
+ def bond_dirs_by_mapnum(mol: Chem.Mol) -> Dict[Tuple[int, int], BondDir]:
15
+ """
16
+ Return bond directions keyed by atom-map numbers for a mapped RDKit molecule.
17
+
18
+ Args:
19
+ mol (Chem.Mol): RDKit molecule whose bonds will be inspected.
20
+
21
+ Returns:
22
+ Dict[Tuple[int, int], BondDir]: Mapping from `(mapnum1, mapnum2)` to the
23
+ bond direction for the bond from atom `mapnum1` to atom `mapnum2`.
24
+
25
+ Note:
26
+ Bonds with `BondDir.NONE` are ignored.
27
+ Bonds are also ignored if either endpoint atom has atom-map number 0
28
+ (i.e., is unmapped).
29
+ For each directed entry `(a, b) -> d`, an opposite-direction entry
30
+ `(b, a) -> BondDirOpposite[d]` is also added.
31
+ """
32
+ bond_dirs_by_mapnum: Dict[Tuple[int, int], BondDir] = {}
33
+ for b in mol.GetBonds():
34
+ bond_dir = b.GetBondDir()
35
+ if bond_dir == BondDir.NONE:
36
+ continue
37
+ begin_atom_map_num = b.GetBeginAtom().GetAtomMapNum()
38
+ if not begin_atom_map_num:
39
+ continue
40
+ end_atom_map_num = b.GetEndAtom().GetAtomMapNum()
41
+ if not end_atom_map_num:
42
+ continue
43
+ bond_dirs_by_mapnum[(begin_atom_map_num, end_atom_map_num)] = bond_dir
44
+ bond_dirs_by_mapnum[(end_atom_map_num, begin_atom_map_num)] = BondDirOpposite[
45
+ bond_dir
46
+ ]
47
+ return bond_dirs_by_mapnum
48
+
49
+
50
+ def enumerate_possible_cistrans_defs(
51
+ template_r: Chem.Mol,
52
+ ) -> Tuple[
53
+ Dict[Tuple[int, int, int, int], Tuple[BondDir, BondDir]], Set[Tuple[int, int]]
54
+ ]:
55
+ """
56
+ This function is meant to take a reactant template and fully enumerate
57
+ all the ways in which different double-bonds can have their cis/trans
58
+ chirality specified. This is necessary because double-bond chirality cannot
59
+ be specified using cis/trans (global properties), but must be done using
60
+ ENDUPRIGHT and ENDDOWNRIGHT for the attached single bonds (local properties).
61
+ Now, the next issue is that on each side of the double bond, only one of
62
+ the single bond directions must be specified, and that direction can be
63
+ using either atom order. e.g.,
64
+
65
+ A1 B1
66
+ \\ /
67
+ C = C
68
+ / \
69
+ A2 B2
70
+
71
+ Can be specified by:
72
+ A1-C is an ENDDOWNRIGHT, C-B1 is an ENDUPRIGHT
73
+ A1-C is an ENDDOWNRIGHT, C-B2 is an ENDDOWNRIGHT
74
+ A1-C is an ENDDOWNRIGHT, B1-C is an ENDDOWNRIGHT
75
+ A1-C is an ENDDOWNRIGHT, B2-C is an ENDUPRIGHT
76
+ ...and twelve more definitions using different A1/A2 specs.
77
+
78
+ ALSO - we can think about horizontally reflecting this bond entirely,
79
+ which gets us even more definitions.
80
+
81
+ So, the point of this function is to fully enumerate *all* of the ways
82
+ in which chirality could have been specified. That way, we can take a
83
+ reactant atom and check if its chirality is within the list of acceptable
84
+ definitions to determine if a match was made.
85
+
86
+ The way we do this is by first defining the *local* chirality of a double
87
+ bond, which weights side chains based purely on the unique mapnum numbering.
88
+ Once we have a local cis/trans definition for a double bond, we can enumerate
89
+ the sixteen possible ways that a reactant could match it.
90
+
91
+ Args:
92
+ template_r (Chem.Mol): Reactant template molecule with atom-map numbers.
93
+
94
+ Returns:
95
+ Tuple[Dict[Tuple[int, int, int, int], Tuple[BondDir, BondDir]], Set[Tuple[int, int]]]:
96
+ - First element (required_bond_defs): Mapping from
97
+ (a1, a2, a3, a4) atom-map-number tuples to (BondDir, BondDir) pairs,
98
+ enumerating all valid ways to specify the cis/trans chirality of
99
+ double bonds in the template.
100
+ - Second element (required_bond_defs_coreatoms): Set of
101
+ (mapnum1, mapnum2) pairs identifying the core double-bond atoms
102
+ for which cis/trans specification was possible.
103
+ """
104
+
105
+ required_bond_defs: Dict[Tuple[int, int, int, int], Tuple[BondDir, BondDir]] = {}
106
+ required_bond_defs_coreatoms: Set[Tuple[int, int]] = set()
107
+
108
+ for b in template_r.GetBonds():
109
+ if b.GetBondType() != BondType.DOUBLE:
110
+ continue
111
+
112
+ # Define begin and end atoms of the double bond
113
+ ba = b.GetBeginAtom()
114
+ bb = b.GetEndAtom()
115
+
116
+ # Now check if it is even possible to specify
117
+ if ba.GetDegree() == 1 or bb.GetDegree() == 1:
118
+ continue
119
+
120
+ ba_label = ba.GetAtomMapNum()
121
+ bb_label = bb.GetAtomMapNum()
122
+
123
+ # Save core atoms so we know that cis/trans was POSSIBLE to specify
124
+ required_bond_defs_coreatoms.add((ba_label, bb_label))
125
+ required_bond_defs_coreatoms.add((bb_label, ba_label))
126
+
127
+ # Define heaviest mapnum neighbor for each atom, excluding the other side of the double bond
128
+ ba_neighbor_labels: List[int] = [a.GetAtomMapNum() for a in ba.GetNeighbors()]
129
+ ba_neighbor_labels.remove(bb_label) # remove other side of =
130
+ ba_neighbor_labels_max = max(ba_neighbor_labels)
131
+ bb_neighbor_labels: List[int] = [a.GetAtomMapNum() for a in bb.GetNeighbors()]
132
+ bb_neighbor_labels.remove(ba_label) # remove other side of =
133
+ bb_neighbor_labels_max = max(bb_neighbor_labels)
134
+
135
+ # The direction of the bond being observed might need to be flipped,
136
+ # based on
137
+ # (a) if it is the heaviest atom on this side, and
138
+ # (b) if the begin/end atoms for the directional bond are
139
+ # in the wrong order (i.e., if the double-bonded atom
140
+ # is the begin atom)
141
+ front_spec = None
142
+ back_spec = None
143
+ for bab in ba.GetBonds():
144
+ if bab.GetBondDir() != BondDir.NONE:
145
+ if bab.GetBeginAtom().GetAtomMapNum() == ba_label:
146
+ # Bond is in wrong order - flip
147
+ if bab.GetEndAtom().GetAtomMapNum() != ba_neighbor_labels_max:
148
+ # Defined atom is not the heaviest - flip
149
+ front_spec = bab.GetBondDir()
150
+ break
151
+ front_spec = BondDirOpposite[bab.GetBondDir()]
152
+ break
153
+ if bab.GetBeginAtom().GetAtomMapNum() != ba_neighbor_labels_max:
154
+ # Defined atom is not heaviest
155
+ front_spec = BondDirOpposite[bab.GetBondDir()]
156
+ break
157
+ front_spec = bab.GetBondDir()
158
+ break
159
+ if front_spec is not None:
160
+ for bbb in bb.GetBonds():
161
+ if bbb.GetBondDir() != BondDir.NONE:
162
+ # For the "back" specification, the double-bonded atom *should* be the BeginAtom
163
+ if bbb.GetEndAtom().GetAtomMapNum() == bb_label:
164
+ # Bond is in wrong order - flip
165
+ if bbb.GetBeginAtom().GetAtomMapNum() != bb_neighbor_labels_max:
166
+ # Defined atom is not the heaviest - flip
167
+ back_spec = bbb.GetBondDir()
168
+ break
169
+ back_spec = BondDirOpposite[bbb.GetBondDir()]
170
+ break
171
+ if bbb.GetEndAtom().GetAtomMapNum() != bb_neighbor_labels_max:
172
+ # Defined atom is not heaviest - flip
173
+ back_spec = BondDirOpposite[bbb.GetBondDir()]
174
+ break
175
+ back_spec = bbb.GetBondDir()
176
+ break
177
+
178
+ # Is this an overall unspecified bond? Put it in the dictionary anyway,
179
+ # so there is something to match
180
+ if front_spec is None or back_spec is None:
181
+ # Create a definition over this bond so that reactant MUST be unspecified, too
182
+ for start_atom in ba_neighbor_labels:
183
+ for end_atom in bb_neighbor_labels:
184
+ required_bond_defs[(start_atom, ba_label, bb_label, end_atom)] = (
185
+ BondDir.NONE,
186
+ BondDir.NONE,
187
+ )
188
+ required_bond_defs[(ba_label, start_atom, bb_label, end_atom)] = (
189
+ BondDir.NONE,
190
+ BondDir.NONE,
191
+ )
192
+ required_bond_defs[(start_atom, ba_label, end_atom, bb_label)] = (
193
+ BondDir.NONE,
194
+ BondDir.NONE,
195
+ )
196
+ required_bond_defs[(ba_label, start_atom, end_atom, bb_label)] = (
197
+ BondDir.NONE,
198
+ BondDir.NONE,
199
+ )
200
+ required_bond_defs[(bb_label, end_atom, start_atom, ba_label)] = (
201
+ BondDir.NONE,
202
+ BondDir.NONE,
203
+ )
204
+ required_bond_defs[(end_atom, bb_label, start_atom, ba_label)] = (
205
+ BondDir.NONE,
206
+ BondDir.NONE,
207
+ )
208
+ required_bond_defs[(bb_label, end_atom, ba_label, start_atom)] = (
209
+ BondDir.NONE,
210
+ BondDir.NONE,
211
+ )
212
+ required_bond_defs[(end_atom, bb_label, ba_label, start_atom)] = (
213
+ BondDir.NONE,
214
+ BondDir.NONE,
215
+ )
216
+ continue
217
+
218
+ if front_spec == back_spec:
219
+ b.SetProp("localChirality", "trans")
220
+ else:
221
+ b.SetProp("localChirality", "cis")
222
+
223
+ possible_defs = {}
224
+ for start_atom in ba_neighbor_labels:
225
+ for end_atom in bb_neighbor_labels:
226
+ needs_inversion = (start_atom != ba_neighbor_labels_max) != (
227
+ end_atom != bb_neighbor_labels_max
228
+ )
229
+ for start_atom_dir in [BondDir.ENDUPRIGHT, BondDir.ENDDOWNRIGHT]:
230
+ # When locally trans, BondDir of start shold be same as end,
231
+ # unless we need inversion
232
+ if (front_spec != back_spec) != needs_inversion:
233
+ # locally cis and does not need inversion (True, False)
234
+ # or locally trans and does need inversion (False, True)
235
+ end_atom_dir = BondDirOpposite[start_atom_dir]
236
+ else:
237
+ # locally cis and does need inversion (True, True)
238
+ # or locally trans and does not need inversion (False, False)
239
+ end_atom_dir = start_atom_dir
240
+
241
+ # Enumerate all combinations of atom orders...
242
+ possible_defs[(start_atom, ba_label, bb_label, end_atom)] = (
243
+ start_atom_dir,
244
+ end_atom_dir,
245
+ )
246
+ possible_defs[(ba_label, start_atom, bb_label, end_atom)] = (
247
+ BondDirOpposite[start_atom_dir],
248
+ end_atom_dir,
249
+ )
250
+ possible_defs[(start_atom, ba_label, end_atom, bb_label)] = (
251
+ start_atom_dir,
252
+ BondDirOpposite[end_atom_dir],
253
+ )
254
+ possible_defs[(ba_label, start_atom, end_atom, bb_label)] = (
255
+ BondDirOpposite[start_atom_dir],
256
+ BondDirOpposite[end_atom_dir],
257
+ )
258
+
259
+ possible_defs[(bb_label, end_atom, start_atom, ba_label)] = (
260
+ end_atom_dir,
261
+ start_atom_dir,
262
+ )
263
+ possible_defs[(bb_label, end_atom, ba_label, start_atom)] = (
264
+ end_atom_dir,
265
+ BondDirOpposite[start_atom_dir],
266
+ )
267
+ possible_defs[(end_atom, bb_label, start_atom, ba_label)] = (
268
+ BondDirOpposite[end_atom_dir],
269
+ start_atom_dir,
270
+ )
271
+ possible_defs[(end_atom, bb_label, ba_label, start_atom)] = (
272
+ BondDirOpposite[end_atom_dir],
273
+ BondDirOpposite[start_atom_dir],
274
+ )
275
+
276
+ # Save to the definition of this bond (in either direction)
277
+ required_bond_defs.update(possible_defs)
278
+
279
+ return required_bond_defs, required_bond_defs_coreatoms
280
+
281
+
282
+ def get_atoms_across_double_bonds(
283
+ mol: Chem.Mol,
284
+ ) -> List[Tuple[Tuple[int, int, int, int], Tuple[BondDir, BondDir], bool]]:
285
+ """
286
+ This function takes a molecule and returns a list of cis/trans specifications
287
+ according to the following:
288
+
289
+ (mapnums, dirs)
290
+
291
+ where atoms = (a1, a2, a3, a4) and dirs = (d1, d2)
292
+ and (a1, a2) defines the ENDUPRIGHT/ENDDOWNRIGHT direction of the "front"
293
+ of the bond using d1, and (a3, a4) defines the direction of the "back" of
294
+ the bond using d2.
295
+
296
+ This is used to initialize reactants with a SINGLE definition constraining
297
+ the chirality. Templates have their chirality fully enumerated, so we can
298
+ match this specific definition to the full set of possible definitions
299
+ when determining if a match should be made.
300
+
301
+ NOTE: the atom mapnums are returned. This is so we can later use them
302
+ to get the old_mapno property from the corresponding product atom, which is
303
+ an outcome-specific assignment
304
+
305
+ We also include implicit chirality here based on ring membership, but keep
306
+ track of that separately
307
+
308
+ Args:
309
+ mol (rdkit.Chem.rdchem.Mol): RDKit molecule
310
+
311
+ Returns:
312
+ List[Tuple[Tuple[int, int, int, int], Tuple[BondDir, BondDir], bool]]:
313
+ A list of cis/trans specifications, where each element is a tuple of
314
+ (mapnums, dirs, is_implicit):
315
+ - mapnums (Tuple[int, int, int, int]): Atom-map numbers (a1, a2, a3, a4)
316
+ where (a1, a2) defines the front single-bond direction and (a3, a4)
317
+ defines the back single-bond direction.
318
+ - dirs (Tuple[BondDir, BondDir]): Bond directions for the front and back.
319
+ - is_implicit (bool): True if the cis/trans specification was inferred
320
+ from ring membership rather than explicitly set.
321
+ """
322
+ atoms_across_double_bonds: List[
323
+ Tuple[Tuple[int, int, int, int], Tuple[BondDir, BondDir], bool]
324
+ ] = []
325
+ ring_info: Chem.RingInfo = mol.GetRingInfo()
326
+ atomrings = ring_info.AtomRings()
327
+
328
+ for b in mol.GetBonds():
329
+ if b.GetBondType() != BondType.DOUBLE:
330
+ continue
331
+
332
+ # Define begin and end atoms of the double bond
333
+ ba = b.GetBeginAtom()
334
+ bb = b.GetEndAtom()
335
+
336
+ # Now check if it is even possible to specify
337
+ if ba.GetDegree() == 1 or bb.GetDegree() == 1:
338
+ continue
339
+
340
+ ba_label = ba.GetAtomMapNum()
341
+ bb_label = bb.GetAtomMapNum()
342
+
343
+ # Try to specify front and back direction separately
344
+ front_mapnums: Optional[Tuple[int, int]] = None
345
+ front_dir: Optional[BondDir] = None
346
+ back_mapnums: Optional[Tuple[int, int]] = None
347
+ back_dir: Optional[BondDir] = None
348
+ is_implicit = False
349
+ bab = None
350
+ bbb = None
351
+
352
+ def _bab_generator(atom: Chem.Atom) -> Iterator[Chem.Bond]:
353
+ for z in atom.GetBonds():
354
+ if z.GetBondType() != BondType.DOUBLE:
355
+ yield z
356
+
357
+ for bab in _bab_generator(ba):
358
+ if bab.GetBondDir() != BondDir.NONE:
359
+ front_mapnums = (
360
+ bab.GetBeginAtom().GetAtomMapNum(),
361
+ bab.GetEndAtom().GetAtomMapNum(),
362
+ )
363
+ front_dir = bab.GetBondDir()
364
+ break
365
+
366
+ def _bbb_generator(atom: Chem.Atom) -> Iterator[Chem.Bond]:
367
+ for z in atom.GetBonds():
368
+ if z.GetBondType() != BondType.DOUBLE:
369
+ yield z
370
+
371
+ for bbb in _bbb_generator(bb):
372
+ if bbb.GetBondDir() != BondDir.NONE:
373
+ back_mapnums = (
374
+ bbb.GetBeginAtom().GetAtomMapNum(),
375
+ bbb.GetEndAtom().GetAtomMapNum(),
376
+ )
377
+ back_dir = bbb.GetBondDir()
378
+ break
379
+
380
+ # If impossible to spec, just continue
381
+ if bab is None or bbb is None:
382
+ continue
383
+
384
+ # Did we actually get a specification out?
385
+ if front_dir is None or back_dir is None:
386
+ if b.IsInRing():
387
+ # Implicit cis! Now to figure out right definitions...
388
+ for atomring in atomrings:
389
+ if ba.GetIdx() in atomring and bb.GetIdx() in atomring:
390
+ front_mapnums = (bab.GetOtherAtom(ba).GetAtomMapNum(), ba_label)
391
+ back_mapnums = (bb_label, bbb.GetOtherAtom(bb).GetAtomMapNum())
392
+ if (bab.GetOtherAtomIdx(ba.GetIdx()) in atomring) != (
393
+ bbb.GetOtherAtomIdx(bb.GetIdx()) in atomring
394
+ ):
395
+ # one of these atoms are in the ring, one is outside -> trans
396
+ front_dir = BondDir.ENDUPRIGHT
397
+ back_dir = BondDir.ENDUPRIGHT
398
+ else:
399
+ front_dir = BondDir.ENDUPRIGHT
400
+ back_dir = BondDir.ENDDOWNRIGHT
401
+ is_implicit = True
402
+ break
403
+
404
+ else:
405
+ # Okay no, actually unspecified
406
+ # Specify direction as BondDir.NONE using whatever bab and bbb were at the end fo the loop
407
+ # note: this is why we use "for bab in ___generator___", so that we know the current
408
+ # value of bab and bbb correspond to a single bond we can def. by
409
+ front_mapnums = (
410
+ bab.GetBeginAtom().GetAtomMapNum(),
411
+ bab.GetEndAtom().GetAtomMapNum(),
412
+ )
413
+ front_dir = BondDir.NONE
414
+ back_mapnums = (
415
+ bbb.GetBeginAtom().GetAtomMapNum(),
416
+ bbb.GetEndAtom().GetAtomMapNum(),
417
+ )
418
+ back_dir = BondDir.NONE
419
+
420
+ if front_mapnums is None or back_mapnums is None:
421
+ raise ValueError("Could not find mapnums for double bond")
422
+ if front_dir is None or back_dir is None:
423
+ raise ValueError("Could not find bond directions for double bond")
424
+ # Save this (a1, a2, a3, a4) -> (d1, d2) spec
425
+ atoms_across_double_bonds.append(
426
+ (
427
+ front_mapnums + back_mapnums,
428
+ (front_dir, back_dir),
429
+ is_implicit,
430
+ )
431
+ )
432
+
433
+ return atoms_across_double_bonds
434
+
435
+
436
+ def restore_bond_stereo_to_sp2_atom(
437
+ a: Chem.Atom, bond_dirs_by_mapnum: Dict[Tuple[int, int], BondDir]
438
+ ) -> bool:
439
+ r"""
440
+ Copy over single-bond directions (ENDUPRIGHT, ENDDOWNRIGHT) to
441
+ the single bonds attached to some double-bonded atom, a
442
+
443
+ In some cases, like C=C/O>>C=C/Br, we should assume that stereochem was
444
+ preserved, even though mapnums won't match. There might be some reactions
445
+ where the chirality is inverted (like C=C/O >> C=C\Br), but let's not
446
+ worry about those for now...
447
+
448
+ Args:
449
+ a (rdkit.Chem.rdchem.Atom): RDKit atom with double bond
450
+ bond_dirs_by_mapnum (Dict[Tuple[int, int], BondDir]): Dictionary of
451
+ (begin_mapnum, end_mapnum) -> bond_dir that defines if a bond should
452
+ be ENDUPRIGHT or ENDDOWNRIGHT. The reverse key is also included with
453
+ the reverse bond direction. If the source molecule did not have a
454
+ specified chirality at this double bond, then the mapnum tuples will
455
+ be missing from the dict.
456
+ Returns:
457
+ bool: Returns True if a bond direction was copied
458
+ """
459
+
460
+ for bond_to_spec in a.GetBonds():
461
+ if (
462
+ bond_to_spec.GetOtherAtom(a).GetAtomMapNum(),
463
+ a.GetAtomMapNum(),
464
+ ) in bond_dirs_by_mapnum:
465
+ bond_to_spec.SetBondDir(
466
+ bond_dirs_by_mapnum[
467
+ (
468
+ bond_to_spec.GetBeginAtom().GetAtomMapNum(),
469
+ bond_to_spec.GetEndAtom().GetAtomMapNum(),
470
+ )
471
+ ]
472
+ )
473
+ return True
474
+
475
+ if a.GetDegree() == 2:
476
+ # Either the branch used to define was replaced with H (deg 3 -> deg 2)
477
+ # or the branch used to define was reacted (deg 2 -> deg 2)
478
+ for bond_to_spec in a.GetBonds():
479
+ if bond_to_spec.GetBondType() == BondType.DOUBLE:
480
+ continue
481
+ if not bond_to_spec.GetOtherAtom(a).HasProp("old_mapno"):
482
+ # new atom, deg2->deg2, assume direction preserved
483
+ needs_inversion = False
484
+ else:
485
+ # old atom, just was not used in chirality definition - set opposite
486
+ needs_inversion = True
487
+
488
+ for (i, j), bond_dir in bond_dirs_by_mapnum.items():
489
+ if (
490
+ bond_dir != BondDir.NONE
491
+ and i == bond_to_spec.GetBeginAtom().GetAtomMapNum()
492
+ ):
493
+ if needs_inversion:
494
+ bond_to_spec.SetBondDir(BondDirOpposite[bond_dir])
495
+ else:
496
+ bond_to_spec.SetBondDir(bond_dir)
497
+ return True
498
+
499
+ elif a.GetDegree() == 3:
500
+ # If we lost the branch defining stereochem, it must have been replaced
501
+ for bond_to_spec in a.GetBonds():
502
+ if bond_to_spec.GetBondType() == BondType.DOUBLE:
503
+ continue
504
+ oa = bond_to_spec.GetOtherAtom(a)
505
+ if oa.HasProp("old_mapno") or oa.HasProp("react_atom_idx"):
506
+ # looking at an old atom, which should have opposite direction as removed atom
507
+ needs_inversion = True
508
+ else:
509
+ # looking at a new atom, assume same as removed atom
510
+ needs_inversion = False
511
+
512
+ for (i, j), bond_dir in bond_dirs_by_mapnum.items():
513
+ if (
514
+ bond_dir != BondDir.NONE
515
+ and i == bond_to_spec.GetBeginAtom().GetAtomMapNum()
516
+ ):
517
+ if needs_inversion:
518
+ bond_to_spec.SetBondDir(BondDirOpposite[bond_dir])
519
+ else:
520
+ bond_to_spec.SetBondDir(bond_dir)
521
+ return True
522
+
523
+ return False
524
+
525
+
526
+ def correct_conjugated(
527
+ initial_bond_dirs: Dict[Tuple[int, int], BondDir], outcome: Chem.Mol
528
+ ) -> bool:
529
+ """
530
+ Checks whether the copying over of single-bond directions (ENDUPRIGHT, ENDDOWNRIGHT) was
531
+ corrupted for a conjugated system, where parts of the directions were specified by the template
532
+ and parts were copied from the reactants.
533
+ Args:
534
+ initial_bond_dirs (Dict[Tuple[int, int], BondDir]): Dictionary of
535
+ (begin_mapnum, end_mapnum) -> bond_dir that defines if a bond is
536
+ ENDUPRIGHT or ENDDOWNRIGHT. The reverse key is also included with
537
+ the reverse bond direction. If the source molecule did not have a
538
+ specified chirality at this double bond, then the mapnum tuples
539
+ will be missing from the dict.
540
+ outcome (rdkit.Chem.rdchem.Mol): RDKit molecule
541
+ Returns:
542
+ bool: Returns True if a conjugated system was corrected
543
+ """
544
+
545
+ if not initial_bond_dirs:
546
+ return False
547
+
548
+ conjugated: List[Tuple[int, int]] = []
549
+ for b in outcome.GetBonds():
550
+ if b.GetIsConjugated():
551
+ conjugated.append(
552
+ (b.GetBeginAtom().GetAtomMapNum(), b.GetEndAtom().GetAtomMapNum())
553
+ )
554
+
555
+ if not conjugated:
556
+ return False
557
+
558
+ final_bond_dirs = bond_dirs_by_mapnum(outcome)
559
+
560
+ # connectivity of conjugated systems
561
+ # DFS may be better for large systems
562
+ isolated_conjugated: List[Set[int]] = []
563
+ for i, j in conjugated:
564
+ found = False
565
+ for c in isolated_conjugated:
566
+ if i in c or j in c:
567
+ found = True
568
+ c.add(i)
569
+ c.add(j)
570
+ break
571
+ if not found:
572
+ isolated_conjugated.append({i, j})
573
+
574
+ need_to_change_dirs = {}
575
+ inverted_dirs = []
576
+ new_dirs = []
577
+ for pair in final_bond_dirs:
578
+ if pair in initial_bond_dirs:
579
+ if final_bond_dirs[pair] != initial_bond_dirs[pair]:
580
+ need_to_change_dirs[pair] = initial_bond_dirs[pair]
581
+ inverted_dirs.append(pair)
582
+ else:
583
+ new_dirs.append(pair)
584
+
585
+ if not inverted_dirs:
586
+ return False
587
+
588
+ isolated_conjugated_need_fix = [False] * len(isolated_conjugated)
589
+ for i, conj in enumerate(isolated_conjugated):
590
+ for pair in inverted_dirs:
591
+ if pair[0] in conj or pair[1] in conj:
592
+ isolated_conjugated_need_fix[i] = True
593
+ break
594
+
595
+ for pair in new_dirs:
596
+ for need_fix, conj in zip(isolated_conjugated_need_fix, isolated_conjugated):
597
+ if need_fix and (pair[0] in conj or pair[1] in conj):
598
+ need_to_change_dirs[pair] = BondDirOpposite[final_bond_dirs[pair]]
599
+ break
600
+
601
+ changed = False
602
+ for b in outcome.GetBonds():
603
+ bam = b.GetBeginAtom().GetAtomMapNum()
604
+ bbm = b.GetEndAtom().GetAtomMapNum()
605
+ if (bam, bbm) in need_to_change_dirs:
606
+ b.SetBondDir(need_to_change_dirs[(bam, bbm)])
607
+ changed = True
608
+
609
+ return changed