Coverage for chempropstereo/stereochemistry/cistrans.py: 94%
53 statements
« prev ^ index » next coverage.py v7.7.1, created at 2025-03-22 21:04 +0000
« prev ^ index » next coverage.py v7.7.1, created at 2025-03-22 21:04 +0000
1"""Module for tagging cis/trans stereogenic bonds in molecules.
3.. module:: stereochemistry.cistrans
4.. moduleauthor:: Charlles Abreu <craabreu@mit.edu>
5"""
7import enum
9import numpy as np
10from rdkit import Chem
12from . import base, utils
15class StemArrangement(base.SpatialArrangement):
16 """Enumeration for cis/trans arrangements in double bonds.
18 Attributes
19 ----------
20 NONE : int
21 Not a stereobond.
22 CIS : int
23 The substituents are on the same side.
24 TRANS : int
25 The substituents are on opposite sides.
27 Examples
28 --------
29 >>> from rdkit import Chem
30 >>> from chempropstereo import stereochemistry
31 >>> mol = Chem.MolFromSmiles("N/C(O)=C(S)/C")
32 >>> stereochemistry.tag_cis_trans_stereobonds(mol)
33 >>> bond = mol.GetBondWithIdx(2)
34 >>> StemArrangement.get_from(bond)
35 <StemArrangement.TRANS: 2>
36 >>> bond = mol.GetBondWithIdx(0)
37 >>> StemArrangement.get_from(bond)
38 <StemArrangement.NONE: 0>
40 """
42 tag = enum.nonmember("canonicalCisTransTag")
44 NONE = 0
45 CIS = 1
46 TRANS = 2
49class BranchRank(base.Rank):
50 """Enumeration for branch ranks.
52 Attributes
53 ----------
54 NONE : int
55 Not a branch.
56 MAJOR : int
57 The major branch.
58 MINOR : int
59 The minor branch.
61 Examples
62 --------
63 >>> from rdkit import Chem
64 >>> from chempropstereo import stereochemistry
65 >>> mol = Chem.MolFromSmiles("N/C(O)=C(S)/C")
66 >>> stereochemistry.tag_cis_trans_stereobonds(mol)
67 >>> bond0, bond1, bond2 = map(mol.GetBondWithIdx, range(3))
68 >>> describe_stereobond(bond2)
69 'N0 O2 C1 (TRANS) C3 C5 S4'
70 >>> BranchRank.from_bond(bond0)
71 <BranchRank.NONE: 0>
72 >>> BranchRank.from_bond(bond0, end_is_center=True)
73 <BranchRank.MAJOR: 1>
74 >>> BranchRank.from_bond(bond1)
75 <BranchRank.MINOR: 2>
76 >>> BranchRank.from_bond(bond1, end_is_center=True)
77 <BranchRank.NONE: 0>
79 """
81 tag = enum.nonmember("canonicalCisTransTag")
83 NONE = 0
84 MAJOR = 1
85 MINOR = 2
88def describe_stereobond(bond: Chem.Bond) -> str:
89 """Describe a cis/trans stereobond.
91 Parameters
92 ----------
93 bond : Chem.Bond
94 The bond to describe.
96 Returns
97 -------
98 str
99 A string description of the cis/trans stereobond.
101 Examples
102 --------
103 >>> from rdkit import Chem
104 >>> from chempropstereo import stereochemistry
105 >>> mol = Chem.MolFromSmiles("N/C(O)=C(S)/C")
106 >>> stereochemistry.tag_cis_trans_stereobonds(mol)
107 >>> stereochemistry.describe_stereobond(mol.GetBondWithIdx(2))
108 'N0 O2 C1 (TRANS) C3 C5 S4'
109 >>> stereochemistry.describe_stereobond(mol.GetBondWithIdx(0))
110 'N0 C1 is not a stereobond'
112 """
113 begin, end = bond.GetBeginAtom(), bond.GetEndAtom()
114 descriptions = [utils.describe_atom(atom) for atom in (begin, end)]
115 arrangement = StemArrangement.get_from(bond)
116 if arrangement == StemArrangement.NONE:
117 return " ".join(descriptions) + " is not a stereobond"
118 return (
119 " ".join(map(utils.describe_atom, BranchRank.get_neighbors(begin)))
120 + " "
121 + f" ({arrangement.name}) ".join(descriptions)
122 + " "
123 + " ".join(map(utils.describe_atom, BranchRank.get_neighbors(end)))
124 )
127def _clean_cis_trans_stereobond(bond: Chem.Bond) -> None:
128 if bond.HasProp(StemArrangement.tag):
129 bond.ClearProp(StemArrangement.tag)
130 for atom in (bond.GetBeginAtom(), bond.GetEndAtom()):
131 atom.ClearProp(BranchRank.tag)
134def tag_cis_trans_stereobonds(mol: Chem.Mol, force: bool = False) -> None:
135 r"""Tag cis/trans stereobonds in a molecule based on their spatial arrangement.
137 Parameters
138 ----------
139 mol
140 The molecule whose cis/trans stereobonds are to be tagged.
141 force
142 Whether to overwrite existing stereobond tags (default is False).
144 Examples
145 --------
146 >>> from chempropstereo import stereochemistry
147 >>> from rdkit import Chem
148 >>> def desc(atom):
149 ... return f"{atom.GetSymbol()}{atom.GetIdx()}"
150 >>> for smi in ["N/C(O)=C(S)/C", "N/C(O)=C(C)\\S", "O\\C(N)=C(S)/C"]:
151 ... mol = Chem.MolFromSmiles(smi)
152 ... stereochemistry.tag_cis_trans_stereobonds(mol)
153 ... for bond in mol.GetBonds():
154 ... arrangement = stereochemistry.StemArrangement.get_from(bond)
155 ... if arrangement != stereochemistry.StemArrangement.NONE:
156 ... print(stereochemistry.describe_stereobond(bond))
157 N0 O2 C1 (TRANS) C3 C5 S4
158 N0 O2 C1 (TRANS) C3 C4 S5
159 N2 O0 C1 (TRANS) C3 C5 S4
161 """
162 if mol.HasProp("hasCanonicalStereobonds") and not force:
163 return
164 Chem.SetBondStereoFromDirections(mol)
165 has_stereobonds = False
166 for bond in mol.GetBonds():
167 tag = bond.GetStereo()
168 if tag in (Chem.BondStereo.STEREOCIS, Chem.BondStereo.STEREOTRANS):
169 has_stereobonds = True
170 connected_atoms = [bond.GetBeginAtom(), bond.GetEndAtom()]
171 indices = [atom.GetIdx() for atom in connected_atoms]
172 neighbors = [
173 [neighbor.GetIdx() for neighbor in atom.GetNeighbors()]
174 for atom in connected_atoms
175 ]
176 ranks = np.fromiter(
177 Chem.CanonicalRankAtomsInFragment(mol, sum(neighbors, [])), dtype=int
178 )
179 ranked_neighbor_indices = [
180 [i for i in np.argsort(ranks[atoms]) if atoms[i] not in indices]
181 for atoms in neighbors
182 ]
183 stereo_atoms = list(bond.GetStereoAtoms())
184 flip = False
185 for atoms, indices in zip(neighbors, ranked_neighbor_indices):
186 if atoms[indices[0]] not in stereo_atoms:
187 flip = not flip
188 if (tag == Chem.BondStereo.STEREOTRANS) == flip:
189 arrangement = StemArrangement.CIS
190 else:
191 arrangement = StemArrangement.TRANS
192 bond.SetIntProp(StemArrangement.tag, arrangement)
193 for atom, indices in zip(connected_atoms, ranked_neighbor_indices):
194 atom.SetProp(BranchRank.tag, utils.concat(arrangement, *indices))
195 else:
196 _clean_cis_trans_stereobond(bond)
197 mol.SetBoolProp("hasCanonicalStereobonds", has_stereobonds)