Coverage for chempropstereo/stereochemistry/tetrahedral.py: 98%
42 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 tetrahedral stereogenic centers in molecules.
3.. module:: stereochemistry.tetrahedral
4.. moduleauthor:: Charlles Abreu <craabreu@mit.edu>
5"""
7import enum
9from rdkit import Chem
11from . import base, utils
13_CIP_CODES = {False: 0, "R": 1, "S": 2}
16class ScanDirection(base.SpatialArrangement):
17 """Enumeration for scan directions in canonicalized tetrahedral stereocenters.
19 Attributes
20 ----------
21 NONE : int
22 Not a stereocenter.
23 CW : int
24 Second, third, and fourth vertices must be scanned clockwise.
25 CCW : int
26 Second, third, and fourth vertices must be scanned counterclockwise.
28 Examples
29 --------
30 >>> from rdkit import Chem
31 >>> from chempropstereo import stereochemistry
32 >>> mol = Chem.MolFromSmiles("C[C@H](N)O")
33 >>> stereochemistry.tag_tetrahedral_stereocenters(mol)
34 >>> atom = mol.GetAtomWithIdx(1)
35 >>> stereochemistry.describe_stereocenter(atom)
36 'C1 (CW) O3 N2 C0'
37 >>> ScanDirection.get_from(mol.GetAtomWithIdx(1))
38 <ScanDirection.CW: 1>
39 >>> ScanDirection.get_from(mol.GetAtomWithIdx(2))
40 <ScanDirection.NONE: 0>
42 """
44 tag = enum.nonmember("canonicalChiralTag")
46 NONE = 0
47 CW = 1
48 CCW = 2
51class VertexRank(base.Rank):
52 """Enumeration of vertex ranks for tetrahedral stereochemistry.
54 Attributes
55 ----------
56 NONE : int
57 Not a tetrahedral vertex.
58 FIRST : int
59 The first vertex in canonical order.
60 SECOND : int
61 The second vertex in canonical order.
62 THIRD : int
63 The third vertex in canonical order.
64 FOURTH : int
65 The fourth vertex in canonical order.
68 Examples
69 --------
70 >>> from rdkit import Chem
71 >>> from chempropstereo import stereochemistry
72 >>> mol = Chem.MolFromSmiles("C[C@H](N)O")
73 >>> stereochemistry.tag_tetrahedral_stereocenters(mol)
74 >>> bond = mol.GetBondWithIdx(1)
75 >>> VertexRank.from_bond(bond)
76 <VertexRank.SECOND: 2>
77 >>> VertexRank.from_bond(bond, end_is_center=True)
78 <VertexRank.NONE: 0>
80 """
82 tag = enum.nonmember("canonicalChiralTag")
84 NONE = 0
85 FIRST = 1
86 SECOND = 2
87 THIRD = 3
88 FOURTH = 4
91def get_cip_code(atom: Chem.Atom) -> int:
92 """Get the CIP code of an atom as an integer.
94 Parameters
95 ----------
96 atom
97 The atom to get the CIP code of.
99 Returns
100 -------
101 int
102 The CIP code of the atom: 0 for no CIP code, 1 for R, 2 for S.
104 Examples
105 --------
106 >>> from chempropstereo import stereochemistry
107 >>> from rdkit import Chem
108 >>> mol1 = Chem.MolFromSmiles("C[C@H](N)O")
109 >>> [stereochemistry.get_cip_code(atom) for atom in mol1.GetAtoms()]
110 [0, 1, 0, 0]
111 >>> mol2 = Chem.MolFromSmiles("C[C@@H](N)O")
112 >>> [stereochemistry.get_cip_code(atom) for atom in mol2.GetAtoms()]
113 [0, 2, 0, 0]
115 """
116 return _CIP_CODES[atom.HasProp("_CIPCode") and atom.GetProp("_CIPCode")]
119def describe_stereocenter(atom: Chem.Atom) -> str:
120 """Describe a tetrahedral stereocenter.
122 Parameters
123 ----------
124 atom : Chem.Atom
125 The atom to describe.
127 Returns
128 -------
129 str
130 A string description of the tetrahedral stereocenter.
132 Examples
133 --------
134 >>> from rdkit import Chem
135 >>> from chempropstereo import stereochemistry
136 >>> mol = Chem.MolFromSmiles("C[C@H](N)O")
137 >>> stereochemistry.tag_tetrahedral_stereocenters(mol)
138 >>> stereochemistry.describe_stereocenter(mol.GetAtomWithIdx(1))
139 'C1 (CW) O3 N2 C0'
140 >>> stereochemistry.describe_stereocenter(mol.GetAtomWithIdx(2))
141 'N2 is not a stereocenter'
143 """
144 direction = ScanDirection.get_from(atom)
145 if direction == ScanDirection.NONE:
146 return f"{utils.describe_atom(atom)} is not a stereocenter"
147 return f"{utils.describe_atom(atom)} ({direction.name}) " + " ".join(
148 map(utils.describe_atom, VertexRank.get_neighbors(atom))
149 )
152def tag_tetrahedral_stereocenters(mol: Chem.Mol, force: bool = False) -> None:
153 """Tag tetrahedral stereocenters in a molecule.
155 Tetrahedral stereocenters are tagged as clockwise or counterclockwise based
156 on their neighbors arranged in a descending order of their canonical ranks.
158 Parameters
159 ----------
160 mol
161 The molecule whose tetrahedral stereocenters are to be tagged.
162 force
163 Whether to overwrite existing chiral tags (default is False).
165 Examples
166 --------
167 >>> from chempropstereo import stereochemistry
168 >>> from rdkit import Chem
169 >>> for smi in ["C[C@H](N)O", "C[C@@H](O)N"]:
170 ... mol = Chem.MolFromSmiles(smi)
171 ... stereochemistry.tag_tetrahedral_stereocenters(mol)
172 ... for atom in mol.GetAtoms():
173 ... direction = stereochemistry.ScanDirection.get_from(atom)
174 ... if direction != stereochemistry.ScanDirection.NONE:
175 ... print(stereochemistry.describe_stereocenter(atom))
176 C1 (CW) O3 N2 C0
177 C1 (CW) O2 N3 C0
179 """
180 if mol.HasProp("hasCanonicalStereocenters") and not force:
181 return
182 has_stereocenters = False
183 for atom in mol.GetAtoms():
184 tag = atom.GetChiralTag()
185 if tag in (
186 Chem.ChiralType.CHI_TETRAHEDRAL_CW,
187 Chem.ChiralType.CHI_TETRAHEDRAL_CCW,
188 ):
189 has_stereocenters = True
190 neighbors = [neighbor.GetIdx() for neighbor in atom.GetNeighbors()]
191 all_ranks = list(
192 Chem.CanonicalRankAtomsInFragment(
193 mol, neighbors, includeChirality=False
194 )
195 )
196 neighbor_ranks = [all_ranks[idx] for idx in neighbors]
197 # Sorting ranks in descending order keeps explicit hydrogens at the end
198 order, flip = utils.argsort_descending_with_parity(*neighbor_ranks)
199 if (tag == Chem.ChiralType.CHI_TETRAHEDRAL_CCW) == flip:
200 direction = ScanDirection.CW
201 else:
202 direction = ScanDirection.CCW
203 atom.SetProp(ScanDirection.tag, utils.concat(direction, *order))
204 elif atom.HasProp(ScanDirection.tag):
205 atom.ClearProp(ScanDirection.tag)
206 mol.SetBoolProp("hasCanonicalStereocenters", has_stereocenters)