chython 2.4__cp312-cp312-win_amd64.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.
Files changed (171) hide show
  1. chython/__init__.py +31 -0
  2. chython/_functions.py +69 -0
  3. chython/algorithms/__init__.py +18 -0
  4. chython/algorithms/_isomorphism.cp312-win_amd64.pyd +0 -0
  5. chython/algorithms/_isomorphism.pyx +189 -0
  6. chython/algorithms/aromatics/__init__.py +27 -0
  7. chython/algorithms/aromatics/_rules.py +124 -0
  8. chython/algorithms/aromatics/kekule.py +530 -0
  9. chython/algorithms/aromatics/test/__init__.py +18 -0
  10. chython/algorithms/aromatics/test/test_kekule.py +160 -0
  11. chython/algorithms/aromatics/test/test_thiele.py +263 -0
  12. chython/algorithms/aromatics/thiele.py +226 -0
  13. chython/algorithms/calculate2d/__init__.py +23 -0
  14. chython/algorithms/calculate2d/clean2d.js +1 -0
  15. chython/algorithms/calculate2d/molecule.py +149 -0
  16. chython/algorithms/calculate2d/reaction.py +80 -0
  17. chython/algorithms/depict.py +523 -0
  18. chython/algorithms/fingerprints/__init__.py +48 -0
  19. chython/algorithms/fingerprints/linear.py +208 -0
  20. chython/algorithms/fingerprints/morgan.py +137 -0
  21. chython/algorithms/fingerprints/test/__init__.py +18 -0
  22. chython/algorithms/fingerprints/test/test_linear.py +127 -0
  23. chython/algorithms/fingerprints/test/test_morgan.py +79 -0
  24. chython/algorithms/isomorphism.py +667 -0
  25. chython/algorithms/mapping/__init__.py +28 -0
  26. chython/algorithms/mapping/_groups.py +94 -0
  27. chython/algorithms/mapping/_reactions.py +72 -0
  28. chython/algorithms/mapping/attention.py +216 -0
  29. chython/algorithms/mapping/fixmapper.py +84 -0
  30. chython/algorithms/mapping/groups.py +137 -0
  31. chython/algorithms/mcs.py +202 -0
  32. chython/algorithms/morgan.py +82 -0
  33. chython/algorithms/rings.py +551 -0
  34. chython/algorithms/smiles.py +556 -0
  35. chython/algorithms/standardize/__init__.py +30 -0
  36. chython/algorithms/standardize/_charged.py +115 -0
  37. chython/algorithms/standardize/_groups.py +926 -0
  38. chython/algorithms/standardize/_metal_organics.py +189 -0
  39. chython/algorithms/standardize/_reagents.py +44 -0
  40. chython/algorithms/standardize/_salts.py +68 -0
  41. chython/algorithms/standardize/molecule.py +479 -0
  42. chython/algorithms/standardize/reaction.py +429 -0
  43. chython/algorithms/standardize/resonance.py +196 -0
  44. chython/algorithms/standardize/salts.py +128 -0
  45. chython/algorithms/standardize/saturation.py +378 -0
  46. chython/algorithms/standardize/test/__init__.py +18 -0
  47. chython/algorithms/standardize/test/test_groups.py +118 -0
  48. chython/algorithms/stereo.py +1222 -0
  49. chython/algorithms/tautomers/__init__.py +171 -0
  50. chython/algorithms/tautomers/_acid.py +59 -0
  51. chython/algorithms/tautomers/_base.py +116 -0
  52. chython/algorithms/tautomers/_keto_enol.py +107 -0
  53. chython/algorithms/tautomers/acid_base.py +214 -0
  54. chython/algorithms/tautomers/heteroarenes.py +108 -0
  55. chython/algorithms/tautomers/keto_enol.py +148 -0
  56. chython/algorithms/tautomers/test/__init__.py +18 -0
  57. chython/algorithms/tautomers/test/test_tautomers.py +80 -0
  58. chython/algorithms/test/__init__.py +18 -0
  59. chython/algorithms/test/test_isomorphism.py +58 -0
  60. chython/algorithms/test/test_smiles.py +76 -0
  61. chython/algorithms/x3dom.py +355 -0
  62. chython/containers/__init__.py +43 -0
  63. chython/containers/_pack_v2.cp312-win_amd64.pyd +0 -0
  64. chython/containers/_pack_v2.pyx +271 -0
  65. chython/containers/_unpack_v0v2.cp312-win_amd64.pyd +0 -0
  66. chython/containers/_unpack_v0v2.pyx +300 -0
  67. chython/containers/bonds.py +255 -0
  68. chython/containers/cgr.py +115 -0
  69. chython/containers/graph.py +200 -0
  70. chython/containers/molecule.py +695 -0
  71. chython/containers/query.py +66 -0
  72. chython/containers/reaction.py +324 -0
  73. chython/exceptions.py +114 -0
  74. chython/files/MRVrw.py +523 -0
  75. chython/files/PDBrw.py +257 -0
  76. chython/files/RDFrw.py +298 -0
  77. chython/files/SDFrw.py +221 -0
  78. chython/files/__init__.py +29 -0
  79. chython/files/_convert.py +207 -0
  80. chython/files/_mapping.py +117 -0
  81. chython/files/_xyz.cp312-win_amd64.pyd +0 -0
  82. chython/files/_xyz.pyx +74 -0
  83. chython/files/daylight/__init__.py +24 -0
  84. chython/files/daylight/parser.py +157 -0
  85. chython/files/daylight/smarts.py +107 -0
  86. chython/files/daylight/smiles.py +245 -0
  87. chython/files/daylight/test/__init__.py +18 -0
  88. chython/files/daylight/test/test_daylight_smarts.py +289 -0
  89. chython/files/daylight/test/test_daylight_smiles.py +101 -0
  90. chython/files/daylight/test/test_parser.py +118 -0
  91. chython/files/daylight/test/test_tokenize.py +77 -0
  92. chython/files/daylight/tokenize.py +376 -0
  93. chython/files/libinchi/__init__.py +22 -0
  94. chython/files/libinchi/libinchi.dll +0 -0
  95. chython/files/libinchi/wrapper.py +555 -0
  96. chython/files/mdl/__init__.py +25 -0
  97. chython/files/mdl/emol.py +209 -0
  98. chython/files/mdl/erxn.py +67 -0
  99. chython/files/mdl/mol.py +144 -0
  100. chython/files/mdl/read.py +225 -0
  101. chython/files/mdl/rxn.py +67 -0
  102. chython/files/mdl/stereo.py +63 -0
  103. chython/files/mdl/write.py +165 -0
  104. chython/files/xyz.py +78 -0
  105. chython/periodictable/__init__.py +69 -0
  106. chython/periodictable/base/__init__.py +25 -0
  107. chython/periodictable/base/dynamic.py +149 -0
  108. chython/periodictable/base/element.py +452 -0
  109. chython/periodictable/base/groups.py +90 -0
  110. chython/periodictable/base/periods.py +46 -0
  111. chython/periodictable/base/query.py +473 -0
  112. chython/periodictable/base/vector.py +119 -0
  113. chython/periodictable/groupI.py +252 -0
  114. chython/periodictable/groupII.py +223 -0
  115. chython/periodictable/groupIII.py +1154 -0
  116. chython/periodictable/groupIV.py +210 -0
  117. chython/periodictable/groupIX.py +196 -0
  118. chython/periodictable/groupV.py +192 -0
  119. chython/periodictable/groupVI.py +183 -0
  120. chython/periodictable/groupVII.py +162 -0
  121. chython/periodictable/groupVIII.py +161 -0
  122. chython/periodictable/groupX.py +166 -0
  123. chython/periodictable/groupXI.py +164 -0
  124. chython/periodictable/groupXII.py +158 -0
  125. chython/periodictable/groupXIII.py +239 -0
  126. chython/periodictable/groupXIV.py +262 -0
  127. chython/periodictable/groupXV.py +276 -0
  128. chython/periodictable/groupXVI.py +457 -0
  129. chython/periodictable/groupXVII.py +324 -0
  130. chython/periodictable/groupXVIII.py +260 -0
  131. chython/reactor/__init__.py +24 -0
  132. chython/reactor/base.py +212 -0
  133. chython/reactor/deprotection.py +565 -0
  134. chython/reactor/groups.py +128 -0
  135. chython/reactor/reactions/__init__.py +157 -0
  136. chython/reactor/reactions/_amidation.py +58 -0
  137. chython/reactor/reactions/_amine_isocyanate.py +52 -0
  138. chython/reactor/reactions/_buchwald_hartwig.py +50 -0
  139. chython/reactor/reactions/_esterification.py +49 -0
  140. chython/reactor/reactions/_macmillan.py +43 -0
  141. chython/reactor/reactions/_reductive_amination.py +50 -0
  142. chython/reactor/reactions/_sonogashira.py +49 -0
  143. chython/reactor/reactions/_sulfonamidation.py +52 -0
  144. chython/reactor/reactions/_suzuki_miyaura.py +88 -0
  145. chython/reactor/reactions/ufe.py +99 -0
  146. chython/reactor/reactor.py +170 -0
  147. chython/reactor/retro/__init__.py +77 -0
  148. chython/reactor/retro/_amidation.py +39 -0
  149. chython/reactor/retro/_aryl_amination.py +32 -0
  150. chython/reactor/retro/_mitsunobu.py +49 -0
  151. chython/reactor/retro/_sonogashira.py +49 -0
  152. chython/reactor/retro/_suzuki_miyaura.py +54 -0
  153. chython/reactor/scaffold.py +122 -0
  154. chython/reactor/test/__init__.py +18 -0
  155. chython/reactor/test/test_deprotection.py +57 -0
  156. chython/reactor/test/test_reactor.py +40 -0
  157. chython/reactor/test/test_scaffold.py +57 -0
  158. chython/reactor/test/test_transformer.py +40 -0
  159. chython/reactor/transformer.py +62 -0
  160. chython/utils/__init__.py +57 -0
  161. chython/utils/free_wilson.py +114 -0
  162. chython/utils/grid.py +130 -0
  163. chython/utils/rdkit.py +165 -0
  164. chython/utils/retro.py +139 -0
  165. chython/utils/svg.py +57 -0
  166. chython/utils/test/__init__.py +18 -0
  167. chython/utils/test/test_rdkit.py +71 -0
  168. chython-2.4.dist-info/LICENSE +165 -0
  169. chython-2.4.dist-info/METADATA +101 -0
  170. chython-2.4.dist-info/RECORD +171 -0
  171. chython-2.4.dist-info/WHEEL +4 -0
@@ -0,0 +1,355 @@
1
+ # -*- coding: utf-8 -*-
2
+ #
3
+ # Copyright 2020-2024 Ramil Nugmanov <nougmanoff@protonmail.com>
4
+ # Copyright 2020 Dinar Batyrshin <batyrshin-dinar@mail.ru>
5
+ # This file is part of chython.
6
+ #
7
+ # chython is free software; you can redistribute it and/or modify
8
+ # it under the terms of the GNU Lesser General Public License as published by
9
+ # the Free Software Foundation; either version 3 of the License, or
10
+ # (at your option) any later version.
11
+ #
12
+ # This program is distributed in the hope that it will be useful,
13
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
14
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15
+ # GNU Lesser General Public License for more details.
16
+ #
17
+ # You should have received a copy of the GNU Lesser General Public License
18
+ # along with this program; if not, see <https://www.gnu.org/licenses/>.
19
+ #
20
+ from math import acos, sqrt
21
+ from typing import TYPE_CHECKING, Union
22
+ from .depict import _render_config
23
+
24
+
25
+ if TYPE_CHECKING:
26
+ from chython import MoleculeContainer
27
+
28
+
29
+ def plane_normal(nmx, nmy, nmz, nox, noy, noz):
30
+ # return normal to plane of two vectors nm and no
31
+ # m <--- n
32
+ # \
33
+ # v
34
+ # o
35
+ return nmy * noz - nmz * noy, nox * nmz - nmx * noz, nmx * noy - nmy * nox
36
+
37
+
38
+ def unit_vector(nmx, nmy, nmz):
39
+ nmd = sqrt(nmx ** 2 + nmy ** 2 + nmz ** 2)
40
+ return nmx / nmd, nmy / nmd, nmz / nmd
41
+
42
+
43
+ def get_angle(nx, ny, nz, mx, my, mz):
44
+ ch = (nx * mx + ny * my + nz * mz) ** 2
45
+ zn = (nx ** 2 + ny ** 2 + nz ** 2) * (mx ** 2 + my ** 2 + mz ** 2)
46
+ if ch < .0001:
47
+ return 1.
48
+ elif zn < .0001:
49
+ return .0
50
+ else:
51
+ return sqrt(1 - ch / zn)
52
+
53
+
54
+ def vector_normal(nmx, nmy, nmz):
55
+ # return normal to vector nm
56
+ if not -.0001 < nmx < .0001:
57
+ return (- nmy - nmz) / nmx, 1, 1
58
+ elif not -.0001 < nmy < .0001:
59
+ return 1, (- nmx - nmz) / nmy, 1
60
+ else:
61
+ return 1, 1, (- nmx - nmy) / nmz
62
+
63
+
64
+ class JupyterWidget:
65
+ def __init__(self, xml, width, height):
66
+ self.xml = xml
67
+ self.width = width
68
+ self.height = height
69
+
70
+ def _repr_html_(self):
71
+ return ("<script type='text/javascript' src='https://www.x3dom.org/download/x3dom.js'></script>"
72
+ "<link rel='stylesheet' type='text/css' href='https://www.x3dom.org/download/x3dom.css'>"
73
+ f'<div style="width: {self.width}; height: {self.height}">{self.xml}</div>')
74
+
75
+ def __html__(self):
76
+ return self._repr_html_()
77
+
78
+
79
+ def _render_aromatic_bond(n_x, n_y, n_z, m_x, m_y, m_z, c_x, c_y, c_z):
80
+ aromatic_space = _render_config['aromatic_space']
81
+
82
+ # n aligned xyz
83
+ nc_x, nc_y, nc_z = c_x - n_x, c_y - n_y, c_z - n_z
84
+ mc_x, mc_y, mc_z = c_x - m_x, c_y - m_y, c_z - m_z
85
+
86
+ nc_ln = sqrt(nc_x ** 2 + nc_y ** 2 + nc_z ** 2)
87
+ mc_ln = sqrt(mc_x ** 2 + mc_y ** 2 + mc_z ** 2)
88
+ sin1 = get_angle(m_x - n_x, m_y - n_y, m_z - n_z, nc_x, nc_y, nc_z)
89
+ sin2 = get_angle(n_x - m_x, n_y - m_y, n_z - m_z, mc_x, mc_y, mc_z)
90
+
91
+ if sin1 < .0001 or sin2 < .0001 or nc_ln < .0001 or mc_ln < .0001:
92
+ return
93
+ else:
94
+ coef1 = aromatic_space / (nc_ln * sin1)
95
+ coef2 = aromatic_space / (mc_ln * sin2)
96
+ return nc_x * coef1, nc_y * coef1, nc_z * coef1, mc_x * coef2, mc_y * coef2, mc_z * coef2
97
+
98
+
99
+ def _render_dashes(nx, ny, nz, nmx, nmy, nmz, nm_ln, r_angle=None):
100
+ bond_radius = _render_config['bond_radius']
101
+ bond_color = _render_config['bond_color']
102
+
103
+ if r_angle is None:
104
+ dash1, dash2 = _render_config['aromatic_dashes']
105
+ r_angle = acos(nmy / nm_ln)
106
+ else:
107
+ dash1, dash2 = _render_config['dashes']
108
+
109
+ xml = []
110
+ dashes_sum = dash1 + dash2
111
+ if dashes_sum < .0001:
112
+ raise ValueError('Dashes should be nonzero')
113
+
114
+ d = dashes_sum / nm_ln
115
+ dx, dy, dz = nmx * d, nmy * d, nmz * d
116
+ b = int((nm_ln - dash1) // dashes_sum)
117
+ t = (nm_ln - (b * dashes_sum)) / nm_ln
118
+ nx, ny, nz = nx + nmx * t / 2, ny + nmy * t / 2, nz + nmz * t / 2
119
+ for _ in range(b):
120
+ xml.append(f" <transform translation='{nx:.2f} {ny:.2f} {nz:.2f}' rotation='{nmz:.2f} 0 "
121
+ f"{-nmx:.2f} {r_angle:.2f}'>\n <shape>\n <appearance>\n"
122
+ f" <material diffusecolor='{bond_color}'>\n </material>\n"
123
+ f" </appearance>\n <cylinder radius='{bond_radius}' height='{dash1:.2f}'>\n"
124
+ " </cylinder>\n </shape>\n </transform>\n")
125
+ nx += dx
126
+ ny += dy
127
+ nz += dz
128
+ xml.append(f" <transform translation='{nx:.2f} {ny:.2f} {nz:.2f}' rotation='{nmz:.2f} 0 "
129
+ f"{-nmx:.2f} {r_angle:.2f}'>\n <shape>\n <appearance>\n"
130
+ f" <material diffusecolor='{bond_color}'>\n </material>\n"
131
+ f" </appearance>\n <cylinder radius='{bond_radius}' height='{dash1:.2f}'>\n"
132
+ " </cylinder>\n </shape>\n </transform>\n")
133
+ return xml
134
+
135
+
136
+ class X3domMolecule:
137
+ __slots__ = ()
138
+
139
+ def depict3d(self: Union['MoleculeContainer', 'X3domMolecule'], index: int = 0) -> str:
140
+ """Get X3DOM XML string.
141
+
142
+ :param index: index of conformer
143
+ """
144
+ if not hasattr(self, '_conformers'):
145
+ raise ValueError('No conformers stored within structure')
146
+ try:
147
+ xyz = self._conformers[index]
148
+ except IndexError:
149
+ raise IndexError('Invalid conformer index')
150
+
151
+ mx = sum(x for x, _, _ in xyz.values()) / len(xyz)
152
+ my = sum(y for _, y, _ in xyz.values()) / len(xyz)
153
+ mz = sum(z for _, _, z in xyz.values()) / len(xyz)
154
+ xyz = {n: (x - mx, y - my, z - mz) for n, (x, y, z) in xyz.items()}
155
+ atoms = self.__render_atoms(xyz)
156
+ bonds = self.__render_bonds(xyz)
157
+ return f'<x3d width=100% height=100%>\n <scene>\n{atoms}{bonds} </scene>\n</x3d>'
158
+
159
+ def view3d(self, index: int = 0, width='600px', height='400px'):
160
+ """
161
+ Jupyter widget for 3D visualization.
162
+
163
+ :param index: index of conformer
164
+ :param width: widget width
165
+ :param height: widget height
166
+ """
167
+ return JupyterWidget(self.depict3d(index), width, height)
168
+
169
+ def __render_atoms(self: 'MoleculeContainer', xyz):
170
+ font = _render_config['font_size']
171
+ carbon = _render_config['carbon']
172
+ radius = _render_config['atom_radius']
173
+ colors = _render_config['atoms_colors']
174
+ mapping_color = _render_config['mapping_color']
175
+
176
+ if radius < 0:
177
+ multiplier = -radius
178
+ radius = 0
179
+ elif not radius:
180
+ multiplier = .2
181
+
182
+ atoms = []
183
+ if carbon:
184
+ for n, a in self.atoms():
185
+ r = radius or a.atomic_radius * multiplier
186
+ fr = r * 0.71
187
+ atoms.append(f" <transform translation='{' '.join(format(x, '.2f') for x in xyz[n])}'>\n"
188
+ " <billboard axisOfRotation='0 0 0'>\n <group>\n"
189
+ f" <transform translation='{fr:.2f} {fr:.2f} 0'>\n"
190
+ ' <shape>\n <appearance>\n'
191
+ f" <material diffuseColor='{mapping_color}'/>\n"
192
+ f" </appearance>\n <text string='{a.atomic_symbol}'>\n"
193
+ f" <fontstyle family='sans' size='{font:.2f}' justify='first'/>\n"
194
+ " </text>\n </shape>\n </transform>\n"
195
+ " <shape>\n <appearance>\n"
196
+ f" <material diffuseColor='{colors[a.atomic_number - 1]}'/>\n"
197
+ f" </appearance>\n <sphere radius='{r:.2f}'/>\n"
198
+ " </shape>\n </group>\n </billboard>\n </transform>\n")
199
+ else:
200
+ for n, a in self.atoms():
201
+ r = radius or a.atomic_radius * multiplier
202
+ atoms.append(f" <transform translation='{' '.join(format(x, '.2f') for x in xyz[n])}'>\n"
203
+ " <shape>\n <appearance>\n"
204
+ f" <material diffuseColor='{colors[a.atomic_number - 1]}'/>\n"
205
+ f" </appearance>\n <sphere radius='{r:.2f}'/>\n"
206
+ " </shape>\n </transform>\n")
207
+ return ''.join(atoms)
208
+
209
+ def __render_bonds(self: 'MoleculeContainer', xyz):
210
+ bonds = self._bonds
211
+
212
+ bond_color = _render_config['bond_color']
213
+ bond_radius = _render_config['bond_radius']
214
+ double_space = _render_config['double_space']
215
+ triple_space = _render_config['triple_space']
216
+ r1 = triple_space * sqrt(3) / 3
217
+ r2 = triple_space * sqrt(3) / 6
218
+
219
+ xml = []
220
+ lengths = {}
221
+ doubles = {}
222
+ half_triple = triple_space / 2
223
+ for n, m, bond in self.bonds():
224
+ nx, ny, nz = xyz[n]
225
+ mx, my, mz = xyz[m]
226
+
227
+ nmx, nmy, nmz = mx - nx, my - ny, mz - nz
228
+ length = sqrt(nmx ** 2 + nmy ** 2 + nmz ** 2)
229
+ if length < .001:
230
+ continue
231
+
232
+ rotation_angle = acos(nmy / length)
233
+ lengths[(n, m)] = lengths[(m, n)] = (length, rotation_angle)
234
+ x, y, z = nx + nmx / 2, ny + nmy / 2, nz + nmz / 2
235
+ if bond in (1, 4):
236
+ xml.append(f" <transform translation='{x:.2f} {y:.2f} {z:.2f}' rotation='{nmz:.2f} 0 "
237
+ f"{-nmx:.2f} {rotation_angle:.2f}'>\n <shape>\n <appearance>\n"
238
+ f" <material diffusecolor='{bond_color}'>\n </material>\n"
239
+ f" </appearance>\n <cylinder radius='{bond_radius}' height='{length:.2f}'>\n"
240
+ " </cylinder>\n </shape>\n </transform>\n")
241
+ elif bond == 2:
242
+ if n in doubles:
243
+ # normal for plane n m o
244
+ norm_x, norm_y, norm_z = plane_normal(nmx, nmy, nmz, *doubles[n])
245
+ elif m in doubles:
246
+ # normal for plane n m o
247
+ norm_x, norm_y, norm_z = plane_normal(nmx, nmy, nmz, *doubles[m])
248
+ else:
249
+ third = next((x for x in bonds[n] if x != m), None)
250
+ if third:
251
+ ox, oy, oz = xyz[third]
252
+ nox, noy, noz = ox - nx, oy - ny, oz - nz
253
+ else:
254
+ third = next((x for x in bonds[m] if x != n), None)
255
+ if third:
256
+ ox, oy, oz = xyz[third]
257
+ nox, noy, noz = ox - nx, oy - ny, oz - nz
258
+ else:
259
+ nox, noy, noz = vector_normal(nmx, nmy, nmz)
260
+
261
+ # normal for plane n m o
262
+ normx, normy, normz = unit_vector(*plane_normal(nmx, nmy, nmz, nox, noy, noz))
263
+
264
+ # normal for plane n m normal
265
+ norm_x, norm_y, norm_z = plane_normal(nmx, nmy, nmz, normx, normy, normz)
266
+
267
+ doubles[n] = doubles[m] = (norm_x, norm_y, norm_z)
268
+ norm_dist = sqrt(norm_x ** 2 + norm_y ** 2 + norm_z ** 2)
269
+
270
+ if norm_dist < .0001:
271
+ coef = double_space * 10000
272
+ else:
273
+ coef = double_space / norm_dist
274
+
275
+ dx, dy, dz = norm_x * coef, norm_y * coef, norm_z * coef
276
+ xml.append(
277
+ f" <transform translation='{x + dx:.2f} {y + dy:.2f} {z + dz:.2f}' rotation='{nmz:.2f} 0 "
278
+ f"{-nmx:.2f} {rotation_angle:.2f}'>\n <shape>\n <appearance>\n"
279
+ f" <material diffusecolor='{bond_color}'>\n </material>\n"
280
+ f" </appearance>\n <cylinder radius='{bond_radius}' height='{length:.2f}'>\n"
281
+ " </cylinder>\n </shape>\n </transform>\n")
282
+ xml.append(
283
+ f" <transform translation='{x - dx:.2f} {y - dy:.2f} {z - dz:.2f}' rotation='{nmz:.2f} 0 "
284
+ f"{-nmx:.2f} {rotation_angle:.2f}'>\n <shape>\n <appearance>\n"
285
+ f" <material diffusecolor='{bond_color}'>\n </material>\n"
286
+ f" </appearance>\n <cylinder radius='{bond_radius}' height='{length:.2f}'>\n"
287
+ " </cylinder>\n </shape>\n </transform>\n")
288
+ elif bond == 3:
289
+ nox, noy, noz = vector_normal(nmx, nmy, nmz)
290
+
291
+ # normal for plane n m o
292
+ normx, normy, normz = unit_vector(*plane_normal(nmx, nmy, nmz, nox, noy, noz))
293
+ vecrx, vecry, vecrz = normx * r1, normy * r1, normz * r1
294
+
295
+ # normal for plane n m normal
296
+ norm_x, norm_y, norm_z = unit_vector(*plane_normal(nmx, nmy, nmz, normx, normy, normz))
297
+ vecx, vecy, vecz = norm_x * half_triple, norm_y * half_triple, norm_z * half_triple
298
+
299
+ xml.append(f" <transform translation='{x + vecrx:.2f} {y + vecry:.2f} {z + vecrz:.2f}'"
300
+ f" rotation='{nmz:.2f} 0 {-nmx:.2f} {rotation_angle:.2f}'>\n <shape>\n"
301
+ f" <appearance>\n <material diffusecolor='{bond_color}'>\n"
302
+ f" </material>\n </appearance>\n <cylinder radius='{bond_radius}'"
303
+ f" height='{length:.2f}'>\n </cylinder>\n </shape>\n </transform>\n")
304
+
305
+ xx, yy, zz = x - normx * r2, y - normy * r2, z - normz * r2
306
+ xml.append(f" <transform translation='{xx - vecx:.2f} {yy - vecy:.2f} {zz - vecz:.2f}'"
307
+ f" rotation='{nmz:.2f} 0 {-nmx:.2f} {rotation_angle:.2f}'>\n <shape>\n"
308
+ f" <appearance>\n <material diffusecolor='{bond_color}'>\n"
309
+ f" </material>\n </appearance>\n <cylinder radius='{bond_radius}'"
310
+ f" height='{length:.2f}'>\n </cylinder>\n </shape>\n </transform>\n")
311
+ xml.append(f" <transform translation='{xx + vecx:.2f} {yy + vecy:.2f} {zz + vecz:.2f}'"
312
+ f" rotation='{nmz:.2f} 0 {-nmx:.2f} {rotation_angle:.2f}'>\n <shape>\n"
313
+ f" <appearance>\n <material diffusecolor='{bond_color}'>\n"
314
+ f" </material>\n </appearance>\n <cylinder radius='{bond_radius}'"
315
+ f" height='{length:.2f}'>\n </cylinder>\n </shape>\n </transform>\n")
316
+ else:
317
+ xml.extend(_render_dashes(nx, ny, nz, nmx, nmy, nmz, length, r_angle=rotation_angle))
318
+
319
+ for ring in self.aromatic_rings:
320
+ cx = sum(xyz[n][0] for n in ring) / len(ring)
321
+ cy = sum(xyz[n][1] for n in ring) / len(ring)
322
+ cz = sum(xyz[n][2] for n in ring) / len(ring)
323
+
324
+ for n, m in zip(ring, ring[1:]):
325
+ nx, ny, nz = xyz[n]
326
+ mx, my, mz = xyz[m]
327
+
328
+ aromatic = _render_aromatic_bond(nx, ny, nz, mx, my, mz, cx, cy, cz)
329
+ if aromatic:
330
+ veca_x, veca_y, veca_z, vecb_x, vecb_y, vecb_z = aromatic
331
+ ax, ay, az = nx + veca_x, ny + veca_y, nz + veca_z
332
+ abx, aby, abz = mx + vecb_x - ax, my + vecb_y - ay, mz + vecb_z - az
333
+ ab_ln = sqrt(abx ** 2 + aby ** 2 + abz ** 2)
334
+ if ab_ln < .0001:
335
+ continue
336
+ else:
337
+ xml.extend(_render_dashes(ax, ay, az, abx, aby, abz, ab_ln))
338
+
339
+ i, j = ring[-1], ring[0]
340
+ nx, ny, nz = xyz[i]
341
+ mx, my, mz = xyz[j]
342
+ aromatic = _render_aromatic_bond(nx, ny, nz, mx, my, mz, cx, cy, cz)
343
+ if aromatic:
344
+ veca_x, veca_y, veca_z, vecb_x, vecb_y, vecb_z = aromatic
345
+ ax, ay, az = nx + veca_x, ny + veca_y, nz + veca_z
346
+ abx, aby, abz = mx + vecb_x - ax, my + vecb_y - ay, mz + vecb_z - az
347
+ ab_ln = sqrt(abx ** 2 + aby ** 2 + abz ** 2)
348
+ if ab_ln < .0001:
349
+ continue
350
+ else:
351
+ xml.extend(_render_dashes(ax, ay, az, abx, aby, abz, ab_ln))
352
+ return ''.join(xml)
353
+
354
+
355
+ __all__ = ['X3domMolecule']
@@ -0,0 +1,43 @@
1
+ # -*- coding: utf-8 -*-
2
+ #
3
+ # Copyright 2017-2024 Ramil Nugmanov <nougmanoff@protonmail.com>
4
+ # This file is part of chython.
5
+ #
6
+ # chython is free software; you can redistribute it and/or modify
7
+ # it under the terms of the GNU Lesser General Public License as published by
8
+ # the Free Software Foundation; either version 3 of the License, or
9
+ # (at your option) any later version.
10
+ #
11
+ # This program is distributed in the hope that it will be useful,
12
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
13
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
+ # GNU Lesser General Public License for more details.
15
+ #
16
+ # You should have received a copy of the GNU Lesser General Public License
17
+ # along with this program; if not, see <https://www.gnu.org/licenses/>.
18
+ #
19
+ from typing import Union
20
+ from zlib import decompress
21
+ from .bonds import *
22
+ from .cgr import *
23
+ from .molecule import *
24
+ from .query import *
25
+ from .reaction import *
26
+
27
+
28
+ def unpach(data: bytes, /, *, compressed=True) -> Union[MoleculeContainer, ReactionContainer]:
29
+ if compressed:
30
+ data = decompress(data)
31
+ try:
32
+ return MoleculeContainer.unpack(data, compressed=False)
33
+ except ValueError:
34
+ pass
35
+ # second try
36
+ return ReactionContainer.unpack(data, compressed=False)
37
+
38
+
39
+ unpack = unpach
40
+
41
+
42
+ __all__ = [x for x in locals() if x.endswith('Container')]
43
+ __all__.extend(['Bond', 'QueryBond', 'unpack', 'unpach'])
@@ -0,0 +1,271 @@
1
+ # -*- coding: utf-8 -*-
2
+ #
3
+ # Copyright 2022-2025 Ramil Nugmanov <nougmanoff@protonmail.com>
4
+ # This file is part of chython.
5
+ #
6
+ # chython is free software; you can redistribute it and/or modify
7
+ # it under the terms of the GNU Lesser General Public License as published by
8
+ # the Free Software Foundation; either version 3 of the License, or
9
+ # (at your option) any later version.
10
+ #
11
+ # This program is distributed in the hope that it will be useful,
12
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
13
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
+ # GNU Lesser General Public License for more details.
15
+ #
16
+ # You should have received a copy of the GNU Lesser General Public License
17
+ # along with this program; if not, see <https://www.gnu.org/licenses/>.
18
+ #
19
+ cimport cython
20
+ from cpython.mem cimport PyMem_Malloc, PyMem_Free
21
+ from libc.math cimport ldexp, frexp
22
+ from libc.string cimport memset
23
+
24
+ # Format V2 specification::
25
+ #
26
+ # Big endian bytes order
27
+ # 8 bit - 0x02 (current format specification)
28
+ # 12 bit - number of atoms
29
+ # 12 bit - cis/trans stereo block size
30
+ #
31
+ # Atom block 9 bytes (repeated):
32
+ # 12 bit - atom number
33
+ # 4 bit - number of neighbors
34
+ # 2 bit tetrahedron sign (00 - not stereo, 10 or 11 - has stereo)
35
+ # 2 bit - allene sign
36
+ # 5 bit - isotope (00000 - not specified, over = isotope - common_isotope + 16)
37
+ # 7 bit - atomic number (<=118)
38
+ # 32 bit - XY float16 coordinates
39
+ # 3 bit - hydrogens (0-7). Note: 7 == None
40
+ # 4 bit - charge (charge + 4. possible range -4 - 4)
41
+ # 1 bit - radical state
42
+ # Connection table: flatten list of neighbors. neighbors count stored in atom block.
43
+ # For example CC(=O)O - {1: [2], 2: [1, 3, 4], 3: [2], 4: [2]} >> [2, 1, 3, 4, 2, 2].
44
+ # Repeated block (equal to bonds count).
45
+ # 24 bit - paired 12 bit numbers.
46
+ # Bonds order block 3 bit per bond zero-padded to full byte at the end.
47
+ # Cis/trans data block (repeated):
48
+ # 24 bit - atoms pair
49
+ # 7 bit - zero padding. in future can be used for extra bond-level stereo, like atropoisomers.
50
+ # 1 bit - sign
51
+
52
+
53
+ @cython.nonecheck(False)
54
+ @cython.boundscheck(False)
55
+ @cython.cdivision(True)
56
+ @cython.wraparound(False)
57
+ def pack(object molecule):
58
+ cdef bint b = True # binary flag
59
+ cdef char charge
60
+ cdef unsigned char atomic_number, ngb_count, isotope, bond, s = 0, buffer_b, buffer_o, stereo, hcr
61
+ cdef unsigned char *data
62
+ cdef unsigned short atoms_count, bonds_count = 0, cis_trans_count, n, m, tn, tm
63
+ cdef unsigned int size, atoms_shift = 4, bonds_shift, order_shift, cis_trans_shift # can be > 2^16
64
+ cdef unsigned char[4096] seen # atom number is 12 bit, thus, can be any value up to 4095. numbers are not continuous
65
+
66
+ cdef bytes py_pack
67
+ cdef dict py_ngb, py_atoms, py_bonds, py_stereo
68
+ cdef tuple py_tuple
69
+ cdef object py_atom, py_bond, py_nan_int, py_obj
70
+
71
+ # map molecule to vars
72
+ py_atoms = molecule._atoms
73
+ py_bonds = molecule._bonds
74
+ py_stereo = molecule._stereo_cis_trans_terminals
75
+
76
+ # calculate elements count
77
+ atoms_count = len(py_atoms)
78
+ cis_trans_count = molecule._cis_trans_count
79
+
80
+ for py_ngb in py_bonds.values():
81
+ bonds_count += len(py_ngb)
82
+ bonds_count /= 2 # graph is bidirected
83
+
84
+ # calculate pack blocks entries
85
+ size = bonds_count * 3 # bonds bits
86
+ if size % 8: # partial byte fill
87
+ size = size / 8 + 1
88
+ else:
89
+ size /= 8
90
+ bonds_shift = 4 + 9 * atoms_count # connection table starting byte
91
+ order_shift = bonds_shift + 3 * bonds_count # bond orders block starting byte
92
+ cis_trans_shift = size + order_shift # cis-trans block starting byte
93
+ size = cis_trans_shift + 4 * cis_trans_count # total pack size
94
+
95
+ # allocate pack in memory
96
+ data = <unsigned char *> PyMem_Malloc(size * sizeof(unsigned char))
97
+ if not data:
98
+ raise MemoryError()
99
+
100
+ memset(seen, 0, 4096 * sizeof(unsigned char)) # erase random data
101
+
102
+ # start pack collection
103
+ data[0] = 2 # header. specification version 2
104
+ data[1] = atoms_count >> 4 # 5-12b of atom count value
105
+ data[2] = atoms_count << 4 | cis_trans_count >> 8 # 1-4b of atom count value, 9-12b of cis-trans count value
106
+ data[3] = cis_trans_count # 1-8b of cis-trans count value
107
+
108
+ for py_obj, py_atom in py_atoms.items():
109
+ py_ngb = py_bonds[py_obj]
110
+ ngb_count = len(py_ngb)
111
+ n = py_obj # cast to C
112
+ seen[n] = 1
113
+ atomic_number = py_atom.atomic_number
114
+
115
+ py_nan_int = py_atom._isotope # direct access
116
+ if py_nan_int is None:
117
+ isotope = 0
118
+ else:
119
+ isotope = <short> py_nan_int - common_isotopes[atomic_number]
120
+
121
+ py_nan_int = py_atom._stereo
122
+ if py_nan_int is None:
123
+ stereo = 0
124
+ # V2 specification
125
+ # 2 bit tetrahedron | 2 bit allene | 0000
126
+ elif py_nan_int:
127
+ if ngb_count == 2: # allene
128
+ stereo = 0x30
129
+ else:
130
+ stereo = 0xc0
131
+ else:
132
+ if ngb_count == 2: # allene
133
+ stereo = 0x20
134
+ else:
135
+ stereo = 0x80
136
+
137
+ # precalculate atom attrs
138
+ # should be done independently, due to possible randomness in dicts order.
139
+ # 3 bit - hydrogens (0-7) | 4 bit - charge | 1 bit - radical
140
+ py_nan_int = py_atom._implicit_hydrogens
141
+ if py_nan_int is None:
142
+ hcr = 0xe0 # 0b11100000
143
+ else:
144
+ hcr = <unsigned char> py_nan_int << 5
145
+
146
+ charge = py_atom._charge
147
+ hcr |= (charge + 4) << 1
148
+ if py_atom._is_radical:
149
+ hcr |= 1
150
+
151
+ data[atoms_shift] = n >> 4 # 5-12b AN
152
+ data[atoms_shift + 1] = n << 4 | ngb_count # 1-4b AN, 4b NC
153
+ data[atoms_shift + 2] = stereo | isotope >> 1 # TS , AS , 4b I
154
+ data[atoms_shift + 3] = isotope << 7 | atomic_number # 1bI , A
155
+
156
+ # 2 float16 big endian
157
+ double_to_float16(py_atom.x, &data[atoms_shift + 4])
158
+ double_to_float16(py_atom.y, &data[atoms_shift + 6])
159
+
160
+ data[atoms_shift + 8] = hcr
161
+ atoms_shift += 9
162
+
163
+ # collect connection table
164
+ for m, py_bond in py_ngb.items():
165
+ if b: # 8 + 4
166
+ data[bonds_shift] = m >> 4
167
+ bonds_shift += 1
168
+ buffer_b = m << 4
169
+ b = False # switch
170
+ else: # 4 + 8
171
+ data[bonds_shift] = buffer_b | m >> 8
172
+ bonds_shift += 1
173
+ data[bonds_shift] = m
174
+ bonds_shift += 1 # next free 3 bytes block
175
+ b = True
176
+
177
+ if not seen[m]:
178
+ bond = <unsigned char> py_bond._order - 1
179
+ # 3 3 2 | 1 3 3 1 | 2 3 3
180
+ if s == 0:
181
+ buffer_o = bond << 5
182
+ s = 1
183
+ elif s == 1:
184
+ buffer_o |= bond << 2
185
+ s = 2
186
+ elif s == 2:
187
+ data[order_shift] = buffer_o | bond >> 1
188
+ order_shift += 1
189
+ buffer_o = bond << 7
190
+ s = 3
191
+ elif s == 3:
192
+ buffer_o |= bond << 4
193
+ s = 4
194
+ elif s == 4:
195
+ buffer_o |= bond << 1
196
+ s = 5
197
+ elif s == 5:
198
+ data[order_shift] = buffer_o | bond >> 2
199
+ order_shift += 1
200
+ buffer_o = bond << 6
201
+ s = 6
202
+ elif s == 6:
203
+ buffer_o |= bond << 3
204
+ s = 7
205
+ else: # 7
206
+ data[order_shift] = buffer_o | bond
207
+ order_shift += 1
208
+ s = 0
209
+
210
+ py_nan_int = py_bond._stereo
211
+ if py_nan_int is not None:
212
+ py_tuple = py_stereo[py_obj]
213
+ tn, tm = py_tuple
214
+ data[cis_trans_shift] = tn >> 4
215
+ data[cis_trans_shift + 1] = tn << 4 | tm >> 8
216
+ data[cis_trans_shift + 2] = tm
217
+ data[cis_trans_shift + 3] = py_nan_int
218
+ cis_trans_shift += 4
219
+
220
+ if s: # flush buffer
221
+ data[order_shift] = buffer_o
222
+
223
+ try:
224
+ py_pack = data[:size]
225
+ finally:
226
+ PyMem_Free(data)
227
+ return py_pack
228
+
229
+
230
+ cdef short[119] common_isotopes
231
+ common_isotopes[:] = [0, -15, -12, -9, -7, -5, -4, -2, 0, 3, 4, 7, 8, 11, 12, 15, 16, 19, 24, 23, 24, 29,
232
+ 32, 35, 36, 39, 40, 43, 43, 48, 49, 54, 57, 59, 63, 64, 68, 69, 72, 73, 75, 77,
233
+ 80, 82, 85, 87, 90, 92, 96, 99, 103, 106, 112, 111, 115, 117, 121, 123, 124, 125,
234
+ 128, 129, 134, 136, 141, 143, 147, 149, 151, 153, 157, 159, 162, 165, 168, 170,
235
+ 174, 176, 179, 181, 185, 188, 191, 193, 193, 194, 206, 207, 210, 211, 216, 215,
236
+ 222, 221, 228, 227, 231, 231, 235, 236, 241, 242, 243, 244, 245, 254, 253, 254,
237
+ 254, 262, 265, 265, 269, 262, 273, 273, 277, 281, 278]
238
+
239
+
240
+ cdef void double_to_float16(double x, unsigned char* p):
241
+ # adopted from cpython source code
242
+ cdef unsigned char sign
243
+ cdef int e
244
+ cdef double f
245
+ cdef unsigned short bits
246
+
247
+ if x == 0.:
248
+ p[0] = p[1] = 0
249
+ return
250
+
251
+ sign = x < 0.
252
+ if sign:
253
+ x = -x
254
+ f = frexp(x, &e)
255
+ e -= 1
256
+ if f < .5 or f >= 1. or e >= 16 or e < -25:
257
+ p[0] = p[1] = 0
258
+ return # ignore big values
259
+
260
+ f *= 2.0
261
+ if e < -14:
262
+ f = ldexp(f, 14 + e)
263
+ e = 0
264
+ else:
265
+ e += 15
266
+ f -= 1.
267
+
268
+ f *= 1024.
269
+ bits = <unsigned short> f | (e << 10) | (sign << 15)
270
+ p[0] = bits >> 8
271
+ p[1] = bits