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

1"""Module for tagging cis/trans stereogenic bonds in molecules. 

2 

3.. module:: stereochemistry.cistrans 

4.. moduleauthor:: Charlles Abreu <craabreu@mit.edu> 

5""" 

6 

7import enum 

8 

9import numpy as np 

10from rdkit import Chem 

11 

12from . import base, utils 

13 

14 

15class StemArrangement(base.SpatialArrangement): 

16 """Enumeration for cis/trans arrangements in double bonds. 

17 

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. 

26 

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> 

39 

40 """ 

41 

42 tag = enum.nonmember("canonicalCisTransTag") 

43 

44 NONE = 0 

45 CIS = 1 

46 TRANS = 2 

47 

48 

49class BranchRank(base.Rank): 

50 """Enumeration for branch ranks. 

51 

52 Attributes 

53 ---------- 

54 NONE : int 

55 Not a branch. 

56 MAJOR : int 

57 The major branch. 

58 MINOR : int 

59 The minor branch. 

60 

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> 

78 

79 """ 

80 

81 tag = enum.nonmember("canonicalCisTransTag") 

82 

83 NONE = 0 

84 MAJOR = 1 

85 MINOR = 2 

86 

87 

88def describe_stereobond(bond: Chem.Bond) -> str: 

89 """Describe a cis/trans stereobond. 

90 

91 Parameters 

92 ---------- 

93 bond : Chem.Bond 

94 The bond to describe. 

95 

96 Returns 

97 ------- 

98 str 

99 A string description of the cis/trans stereobond. 

100 

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' 

111 

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 ) 

125 

126 

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) 

132 

133 

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. 

136 

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). 

143 

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 

160 

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)