chython 2.19__cp313-cp313-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 (175) hide show
  1. chython/__init__.py +35 -0
  2. chython/_functions.py +95 -0
  3. chython/_java.py +57 -0
  4. chython/algorithms/__init__.py +18 -0
  5. chython/algorithms/_isomorphism.cp313-win_amd64.pyd +0 -0
  6. chython/algorithms/_isomorphism.pyx +189 -0
  7. chython/algorithms/_rings.cp313-win_amd64.pyd +0 -0
  8. chython/algorithms/_rings.pyx +478 -0
  9. chython/algorithms/aromatics/__init__.py +27 -0
  10. chython/algorithms/aromatics/_rules.py +124 -0
  11. chython/algorithms/aromatics/kekule.py +532 -0
  12. chython/algorithms/aromatics/test/__init__.py +18 -0
  13. chython/algorithms/aromatics/test/test_kekule.py +160 -0
  14. chython/algorithms/aromatics/test/test_thiele.py +263 -0
  15. chython/algorithms/aromatics/thiele.py +222 -0
  16. chython/algorithms/calculate2d/__init__.py +23 -0
  17. chython/algorithms/calculate2d/clean2d.js +1 -0
  18. chython/algorithms/calculate2d/molecule.py +191 -0
  19. chython/algorithms/calculate2d/reaction.py +75 -0
  20. chython/algorithms/conformers.py +106 -0
  21. chython/algorithms/depict.py +596 -0
  22. chython/algorithms/fingerprints/__init__.py +43 -0
  23. chython/algorithms/fingerprints/linear.py +202 -0
  24. chython/algorithms/fingerprints/morgan.py +132 -0
  25. chython/algorithms/fingerprints/test/__init__.py +18 -0
  26. chython/algorithms/fingerprints/test/test_linear.py +127 -0
  27. chython/algorithms/fingerprints/test/test_morgan.py +79 -0
  28. chython/algorithms/groups/__init__.py +212 -0
  29. chython/algorithms/groups/_functional.py +290 -0
  30. chython/algorithms/groups/_oxidations.py +70 -0
  31. chython/algorithms/groups/_protective.py +964 -0
  32. chython/algorithms/groups/_reactions.py +809 -0
  33. chython/algorithms/groups/_reductions.py +88 -0
  34. chython/algorithms/groups/_transformations.py +278 -0
  35. chython/algorithms/groups/test/__init__.py +18 -0
  36. chython/algorithms/groups/test/test_deprotection.py +108 -0
  37. chython/algorithms/groups/test/test_reactions.py +377 -0
  38. chython/algorithms/isomorphism.py +664 -0
  39. chython/algorithms/mapping/__init__.py +188 -0
  40. chython/algorithms/mapping/_groups.py +94 -0
  41. chython/algorithms/mapping/_reactions.py +72 -0
  42. chython/algorithms/mapping/attention.py +266 -0
  43. chython/algorithms/mapping/reconstruct.py +346 -0
  44. chython/algorithms/mcs.py +201 -0
  45. chython/algorithms/morgan.py +77 -0
  46. chython/algorithms/rings.py +178 -0
  47. chython/algorithms/smiles.py +597 -0
  48. chython/algorithms/standardize/__init__.py +30 -0
  49. chython/algorithms/standardize/_groups.py +958 -0
  50. chython/algorithms/standardize/_isomers.py +330 -0
  51. chython/algorithms/standardize/_metal_organics.py +189 -0
  52. chython/algorithms/standardize/_reagents.py +44 -0
  53. chython/algorithms/standardize/_salts.py +68 -0
  54. chython/algorithms/standardize/molecule.py +475 -0
  55. chython/algorithms/standardize/reaction.py +384 -0
  56. chython/algorithms/standardize/resonance.py +191 -0
  57. chython/algorithms/standardize/salts.py +128 -0
  58. chython/algorithms/standardize/saturation.py +373 -0
  59. chython/algorithms/standardize/test/__init__.py +18 -0
  60. chython/algorithms/standardize/test/test_canonicalize.py +66 -0
  61. chython/algorithms/standardize/test/test_groups.py +146 -0
  62. chython/algorithms/standardize/test/test_isomers.py +198 -0
  63. chython/algorithms/stereo.py +1215 -0
  64. chython/algorithms/tautomers/__init__.py +167 -0
  65. chython/algorithms/tautomers/_acid.py +59 -0
  66. chython/algorithms/tautomers/_base.py +116 -0
  67. chython/algorithms/tautomers/_keto_enol.py +107 -0
  68. chython/algorithms/tautomers/acid_base.py +208 -0
  69. chython/algorithms/tautomers/heteroarenes.py +103 -0
  70. chython/algorithms/tautomers/keto_enol.py +143 -0
  71. chython/algorithms/tautomers/test/__init__.py +18 -0
  72. chython/algorithms/tautomers/test/test_tautomers.py +80 -0
  73. chython/algorithms/test/__init__.py +18 -0
  74. chython/algorithms/test/test_isomorphism.py +58 -0
  75. chython/algorithms/test/test_smiles.py +76 -0
  76. chython/algorithms/x3dom.py +350 -0
  77. chython/containers/__init__.py +48 -0
  78. chython/containers/_pack_v2.cp313-win_amd64.pyd +0 -0
  79. chython/containers/_pack_v2.pyx +271 -0
  80. chython/containers/_unpack_v0v2.cp313-win_amd64.pyd +0 -0
  81. chython/containers/_unpack_v0v2.pyx +302 -0
  82. chython/containers/bonds.py +255 -0
  83. chython/containers/cgr.py +115 -0
  84. chython/containers/chimera.py +63 -0
  85. chython/containers/graph.py +199 -0
  86. chython/containers/molecule.py +817 -0
  87. chython/containers/query.py +71 -0
  88. chython/containers/rdkit.py +183 -0
  89. chython/containers/reaction.py +324 -0
  90. chython/containers/test/__init__.py +18 -0
  91. chython/containers/test/test_molecule.py +117 -0
  92. chython/containers/test/test_pack.py +60 -0
  93. chython/containers/test/test_rdkit.py +70 -0
  94. chython/containers/test/test_reaction.py +40 -0
  95. chython/exceptions.py +114 -0
  96. chython/files/MRVrw.py +532 -0
  97. chython/files/PDBrw.py +255 -0
  98. chython/files/RDFrw.py +309 -0
  99. chython/files/SDFrw.py +221 -0
  100. chython/files/__init__.py +30 -0
  101. chython/files/_convert.py +207 -0
  102. chython/files/_mapping.py +117 -0
  103. chython/files/_xyz.cp313-win_amd64.pyd +0 -0
  104. chython/files/_xyz.pyx +74 -0
  105. chython/files/daylight/__init__.py +24 -0
  106. chython/files/daylight/parser.py +157 -0
  107. chython/files/daylight/smarts.py +107 -0
  108. chython/files/daylight/smiles.py +260 -0
  109. chython/files/daylight/test/__init__.py +18 -0
  110. chython/files/daylight/test/test_daylight_smarts.py +289 -0
  111. chython/files/daylight/test/test_daylight_smiles.py +101 -0
  112. chython/files/daylight/test/test_parser.py +118 -0
  113. chython/files/daylight/test/test_tokenize.py +77 -0
  114. chython/files/daylight/tokenize.py +376 -0
  115. chython/files/libinchi/__init__.py +22 -0
  116. chython/files/libinchi/libinchi.dll +0 -0
  117. chython/files/libinchi/wrapper.py +554 -0
  118. chython/files/mdl/__init__.py +25 -0
  119. chython/files/mdl/emol.py +251 -0
  120. chython/files/mdl/erxn.py +67 -0
  121. chython/files/mdl/mol.py +186 -0
  122. chython/files/mdl/read.py +223 -0
  123. chython/files/mdl/rxn.py +67 -0
  124. chython/files/mdl/stereo.py +63 -0
  125. chython/files/mdl/write.py +201 -0
  126. chython/files/opsin.py +35 -0
  127. chython/files/test/__init__.py +0 -0
  128. chython/files/test/test_inchi.py +27 -0
  129. chython/files/test/test_mrv.py +47 -0
  130. chython/files/test/test_sdf_rdf.py +64 -0
  131. chython/files/test/test_xyz.py +20 -0
  132. chython/files/xyz.py +78 -0
  133. chython/periodictable/__init__.py +69 -0
  134. chython/periodictable/base/__init__.py +25 -0
  135. chython/periodictable/base/dynamic.py +149 -0
  136. chython/periodictable/base/element.py +451 -0
  137. chython/periodictable/base/groups.py +90 -0
  138. chython/periodictable/base/periods.py +46 -0
  139. chython/periodictable/base/query.py +473 -0
  140. chython/periodictable/base/vector.py +119 -0
  141. chython/periodictable/groupI.py +252 -0
  142. chython/periodictable/groupII.py +223 -0
  143. chython/periodictable/groupIII.py +1154 -0
  144. chython/periodictable/groupIV.py +210 -0
  145. chython/periodictable/groupIX.py +196 -0
  146. chython/periodictable/groupV.py +192 -0
  147. chython/periodictable/groupVI.py +183 -0
  148. chython/periodictable/groupVII.py +162 -0
  149. chython/periodictable/groupVIII.py +161 -0
  150. chython/periodictable/groupX.py +166 -0
  151. chython/periodictable/groupXI.py +164 -0
  152. chython/periodictable/groupXII.py +158 -0
  153. chython/periodictable/groupXIII.py +239 -0
  154. chython/periodictable/groupXIV.py +262 -0
  155. chython/periodictable/groupXV.py +276 -0
  156. chython/periodictable/groupXVI.py +457 -0
  157. chython/periodictable/groupXVII.py +324 -0
  158. chython/periodictable/groupXVIII.py +260 -0
  159. chython/periodictable/test/__init__.py +0 -0
  160. chython/periodictable/test/test_element.py +100 -0
  161. chython/reactor/__init__.py +24 -0
  162. chython/reactor/base.py +222 -0
  163. chython/reactor/reactor.py +177 -0
  164. chython/reactor/test/__init__.py +18 -0
  165. chython/reactor/test/test_reactor.py +40 -0
  166. chython/reactor/test/test_transformer.py +78 -0
  167. chython/reactor/transformer.py +72 -0
  168. chython/utils/__init__.py +50 -0
  169. chython/utils/free_wilson.py +114 -0
  170. chython/utils/grid.py +130 -0
  171. chython/utils/retro.py +139 -0
  172. chython-2.19.dist-info/METADATA +105 -0
  173. chython-2.19.dist-info/RECORD +175 -0
  174. chython-2.19.dist-info/WHEEL +4 -0
  175. chython-2.19.dist-info/licenses/LICENSE +165 -0
@@ -0,0 +1,350 @@
1
+ # -*- coding: utf-8 -*-
2
+ #
3
+ # Copyright 2020-2026 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 .depict import _render_config
22
+
23
+
24
+ def plane_normal(nmx, nmy, nmz, nox, noy, noz):
25
+ # return normal to plane of two vectors nm and no
26
+ # m <--- n
27
+ # \
28
+ # v
29
+ # o
30
+ return nmy * noz - nmz * noy, nox * nmz - nmx * noz, nmx * noy - nmy * nox
31
+
32
+
33
+ def unit_vector(nmx, nmy, nmz):
34
+ nmd = sqrt(nmx ** 2 + nmy ** 2 + nmz ** 2)
35
+ return nmx / nmd, nmy / nmd, nmz / nmd
36
+
37
+
38
+ def get_angle(nx, ny, nz, mx, my, mz):
39
+ ch = (nx * mx + ny * my + nz * mz) ** 2
40
+ zn = (nx ** 2 + ny ** 2 + nz ** 2) * (mx ** 2 + my ** 2 + mz ** 2)
41
+ if ch < .0001:
42
+ return 1.
43
+ elif zn < .0001:
44
+ return .0
45
+ else:
46
+ return sqrt(1 - ch / zn)
47
+
48
+
49
+ def vector_normal(nmx, nmy, nmz):
50
+ # return normal to vector nm
51
+ if not -.0001 < nmx < .0001:
52
+ return (- nmy - nmz) / nmx, 1, 1
53
+ elif not -.0001 < nmy < .0001:
54
+ return 1, (- nmx - nmz) / nmy, 1
55
+ else:
56
+ return 1, 1, (- nmx - nmy) / nmz
57
+
58
+
59
+ class JupyterWidget:
60
+ def __init__(self, xml, width, height):
61
+ self.xml = xml
62
+ self.width = width
63
+ self.height = height
64
+
65
+ def _repr_html_(self):
66
+ return ("<script type='text/javascript' src='https://www.x3dom.org/download/x3dom.js'></script>"
67
+ "<link rel='stylesheet' type='text/css' href='https://www.x3dom.org/download/x3dom.css'>"
68
+ f'<div style="width: {self.width}; height: {self.height}">{self.xml}</div>')
69
+
70
+ def __html__(self):
71
+ return self._repr_html_()
72
+
73
+
74
+ def _render_aromatic_bond(n_x, n_y, n_z, m_x, m_y, m_z, c_x, c_y, c_z):
75
+ aromatic_space = _render_config['aromatic_space']
76
+
77
+ # n aligned xyz
78
+ nc_x, nc_y, nc_z = c_x - n_x, c_y - n_y, c_z - n_z
79
+ mc_x, mc_y, mc_z = c_x - m_x, c_y - m_y, c_z - m_z
80
+
81
+ nc_ln = sqrt(nc_x ** 2 + nc_y ** 2 + nc_z ** 2)
82
+ mc_ln = sqrt(mc_x ** 2 + mc_y ** 2 + mc_z ** 2)
83
+ sin1 = get_angle(m_x - n_x, m_y - n_y, m_z - n_z, nc_x, nc_y, nc_z)
84
+ sin2 = get_angle(n_x - m_x, n_y - m_y, n_z - m_z, mc_x, mc_y, mc_z)
85
+
86
+ if sin1 < .0001 or sin2 < .0001 or nc_ln < .0001 or mc_ln < .0001:
87
+ return
88
+ else:
89
+ coef1 = aromatic_space / (nc_ln * sin1)
90
+ coef2 = aromatic_space / (mc_ln * sin2)
91
+ return nc_x * coef1, nc_y * coef1, nc_z * coef1, mc_x * coef2, mc_y * coef2, mc_z * coef2
92
+
93
+
94
+ def _render_dashes(nx, ny, nz, nmx, nmy, nmz, nm_ln, r_angle=None):
95
+ bond_radius = _render_config['bond_radius']
96
+ bond_color = _render_config['bond_color']
97
+
98
+ if r_angle is None:
99
+ dash1, dash2 = _render_config['aromatic_dashes']
100
+ r_angle = acos(nmy / nm_ln)
101
+ else:
102
+ dash1, dash2 = _render_config['dashes']
103
+
104
+ xml = []
105
+ dashes_sum = dash1 + dash2
106
+ if dashes_sum < .0001:
107
+ raise ValueError('Dashes should be nonzero')
108
+
109
+ d = dashes_sum / nm_ln
110
+ dx, dy, dz = nmx * d, nmy * d, nmz * d
111
+ b = int((nm_ln - dash1) // dashes_sum)
112
+ t = (nm_ln - (b * dashes_sum)) / nm_ln
113
+ nx, ny, nz = nx + nmx * t / 2, ny + nmy * t / 2, nz + nmz * t / 2
114
+ for _ in range(b):
115
+ xml.append(f" <transform translation='{nx:.2f} {ny:.2f} {nz:.2f}' rotation='{nmz:.2f} 0 "
116
+ f"{-nmx:.2f} {r_angle:.2f}'>\n <shape>\n <appearance>\n"
117
+ f" <material diffusecolor='{bond_color}'>\n </material>\n"
118
+ f" </appearance>\n <cylinder radius='{bond_radius}' height='{dash1:.2f}'>\n"
119
+ " </cylinder>\n </shape>\n </transform>\n")
120
+ nx += dx
121
+ ny += dy
122
+ nz += dz
123
+ xml.append(f" <transform translation='{nx:.2f} {ny:.2f} {nz:.2f}' rotation='{nmz:.2f} 0 "
124
+ f"{-nmx:.2f} {r_angle:.2f}'>\n <shape>\n <appearance>\n"
125
+ f" <material diffusecolor='{bond_color}'>\n </material>\n"
126
+ f" </appearance>\n <cylinder radius='{bond_radius}' height='{dash1:.2f}'>\n"
127
+ " </cylinder>\n </shape>\n </transform>\n")
128
+ return xml
129
+
130
+
131
+ class X3domMolecule:
132
+ __slots__ = ()
133
+
134
+ def depict3d(self, index: int = 0) -> str:
135
+ """Get X3DOM XML string.
136
+
137
+ :param index: index of conformer
138
+ """
139
+ if not hasattr(self, '_conformers'):
140
+ raise ValueError('No conformers stored within structure')
141
+ try:
142
+ xyz = self._conformers[index]
143
+ except IndexError:
144
+ raise IndexError('Invalid conformer index')
145
+
146
+ mx = sum(x for x, _, _ in xyz.values()) / len(xyz)
147
+ my = sum(y for _, y, _ in xyz.values()) / len(xyz)
148
+ mz = sum(z for _, _, z in xyz.values()) / len(xyz)
149
+ xyz = {n: (x - mx, y - my, z - mz) for n, (x, y, z) in xyz.items()}
150
+ atoms = self.__render_atoms(xyz)
151
+ bonds = self.__render_bonds(xyz)
152
+ return f'<x3d width=100% height=100%>\n <scene>\n{atoms}{bonds} </scene>\n</x3d>'
153
+
154
+ def view3d(self, index: int = 0, width='600px', height='400px'):
155
+ """
156
+ Jupyter widget for 3D visualization.
157
+
158
+ :param index: index of conformer
159
+ :param width: widget width
160
+ :param height: widget height
161
+ """
162
+ return JupyterWidget(self.depict3d(index), width, height)
163
+
164
+ def __render_atoms(self, xyz):
165
+ font = _render_config['font_size']
166
+ carbon = _render_config['carbon']
167
+ radius = _render_config['atom_radius']
168
+ colors = _render_config['atoms_colors']
169
+ mapping_color = _render_config['mapping_color']
170
+
171
+ if radius < 0:
172
+ multiplier = -radius
173
+ radius = 0
174
+ elif not radius:
175
+ multiplier = .2
176
+
177
+ atoms = []
178
+ if carbon:
179
+ for n, a in self.atoms():
180
+ r = radius or a.atomic_radius * multiplier
181
+ fr = r * 0.71
182
+ atoms.append(f" <transform translation='{' '.join(format(x, '.2f') for x in xyz[n])}'>\n"
183
+ " <billboard axisOfRotation='0 0 0'>\n <group>\n"
184
+ f" <transform translation='{fr:.2f} {fr:.2f} 0'>\n"
185
+ ' <shape>\n <appearance>\n'
186
+ f" <material diffuseColor='{mapping_color}'/>\n"
187
+ f" </appearance>\n <text string='{a.atomic_symbol}'>\n"
188
+ f" <fontstyle family='sans' size='{font:.2f}' justify='first'/>\n"
189
+ " </text>\n </shape>\n </transform>\n"
190
+ " <shape>\n <appearance>\n"
191
+ f" <material diffuseColor='{colors[a.atomic_number - 1]}'/>\n"
192
+ f" </appearance>\n <sphere radius='{r:.2f}'/>\n"
193
+ " </shape>\n </group>\n </billboard>\n </transform>\n")
194
+ else:
195
+ for n, a in self.atoms():
196
+ r = radius or a.atomic_radius * multiplier
197
+ atoms.append(f" <transform translation='{' '.join(format(x, '.2f') for x in xyz[n])}'>\n"
198
+ " <shape>\n <appearance>\n"
199
+ f" <material diffuseColor='{colors[a.atomic_number - 1]}'/>\n"
200
+ f" </appearance>\n <sphere radius='{r:.2f}'/>\n"
201
+ " </shape>\n </transform>\n")
202
+ return ''.join(atoms)
203
+
204
+ def __render_bonds(self, xyz):
205
+ bonds = self._bonds
206
+
207
+ bond_color = _render_config['bond_color']
208
+ bond_radius = _render_config['bond_radius']
209
+ double_space = _render_config['double_space']
210
+ triple_space = _render_config['triple_space']
211
+ r1 = triple_space * sqrt(3) / 3
212
+ r2 = triple_space * sqrt(3) / 6
213
+
214
+ xml = []
215
+ lengths = {}
216
+ doubles = {}
217
+ half_triple = triple_space / 2
218
+ for n, m, bond in self.bonds():
219
+ nx, ny, nz = xyz[n]
220
+ mx, my, mz = xyz[m]
221
+
222
+ nmx, nmy, nmz = mx - nx, my - ny, mz - nz
223
+ length = sqrt(nmx ** 2 + nmy ** 2 + nmz ** 2)
224
+ if length < .001:
225
+ continue
226
+
227
+ rotation_angle = acos(nmy / length)
228
+ lengths[(n, m)] = lengths[(m, n)] = (length, rotation_angle)
229
+ x, y, z = nx + nmx / 2, ny + nmy / 2, nz + nmz / 2
230
+ if bond in (1, 4):
231
+ xml.append(f" <transform translation='{x:.2f} {y:.2f} {z:.2f}' rotation='{nmz:.2f} 0 "
232
+ f"{-nmx:.2f} {rotation_angle:.2f}'>\n <shape>\n <appearance>\n"
233
+ f" <material diffusecolor='{bond_color}'>\n </material>\n"
234
+ f" </appearance>\n <cylinder radius='{bond_radius}' height='{length:.2f}'>\n"
235
+ " </cylinder>\n </shape>\n </transform>\n")
236
+ elif bond == 2:
237
+ if n in doubles:
238
+ # normal for plane n m o
239
+ norm_x, norm_y, norm_z = plane_normal(nmx, nmy, nmz, *doubles[n])
240
+ elif m in doubles:
241
+ # normal for plane n m o
242
+ norm_x, norm_y, norm_z = plane_normal(nmx, nmy, nmz, *doubles[m])
243
+ else:
244
+ third = next((x for x in bonds[n] if x != m), None)
245
+ if third:
246
+ ox, oy, oz = xyz[third]
247
+ nox, noy, noz = ox - nx, oy - ny, oz - nz
248
+ else:
249
+ third = next((x for x in bonds[m] if x != n), None)
250
+ if third:
251
+ ox, oy, oz = xyz[third]
252
+ nox, noy, noz = ox - nx, oy - ny, oz - nz
253
+ else:
254
+ nox, noy, noz = vector_normal(nmx, nmy, nmz)
255
+
256
+ # normal for plane n m o
257
+ normx, normy, normz = unit_vector(*plane_normal(nmx, nmy, nmz, nox, noy, noz))
258
+
259
+ # normal for plane n m normal
260
+ norm_x, norm_y, norm_z = plane_normal(nmx, nmy, nmz, normx, normy, normz)
261
+
262
+ doubles[n] = doubles[m] = (norm_x, norm_y, norm_z)
263
+ norm_dist = sqrt(norm_x ** 2 + norm_y ** 2 + norm_z ** 2)
264
+
265
+ if norm_dist < .0001:
266
+ coef = double_space * 10000
267
+ else:
268
+ coef = double_space / norm_dist
269
+
270
+ dx, dy, dz = norm_x * coef, norm_y * coef, norm_z * coef
271
+ xml.append(
272
+ f" <transform translation='{x + dx:.2f} {y + dy:.2f} {z + dz:.2f}' rotation='{nmz:.2f} 0 "
273
+ f"{-nmx:.2f} {rotation_angle:.2f}'>\n <shape>\n <appearance>\n"
274
+ f" <material diffusecolor='{bond_color}'>\n </material>\n"
275
+ f" </appearance>\n <cylinder radius='{bond_radius}' height='{length:.2f}'>\n"
276
+ " </cylinder>\n </shape>\n </transform>\n")
277
+ xml.append(
278
+ f" <transform translation='{x - dx:.2f} {y - dy:.2f} {z - dz:.2f}' rotation='{nmz:.2f} 0 "
279
+ f"{-nmx:.2f} {rotation_angle:.2f}'>\n <shape>\n <appearance>\n"
280
+ f" <material diffusecolor='{bond_color}'>\n </material>\n"
281
+ f" </appearance>\n <cylinder radius='{bond_radius}' height='{length:.2f}'>\n"
282
+ " </cylinder>\n </shape>\n </transform>\n")
283
+ elif bond == 3:
284
+ nox, noy, noz = vector_normal(nmx, nmy, nmz)
285
+
286
+ # normal for plane n m o
287
+ normx, normy, normz = unit_vector(*plane_normal(nmx, nmy, nmz, nox, noy, noz))
288
+ vecrx, vecry, vecrz = normx * r1, normy * r1, normz * r1
289
+
290
+ # normal for plane n m normal
291
+ norm_x, norm_y, norm_z = unit_vector(*plane_normal(nmx, nmy, nmz, normx, normy, normz))
292
+ vecx, vecy, vecz = norm_x * half_triple, norm_y * half_triple, norm_z * half_triple
293
+
294
+ xml.append(f" <transform translation='{x + vecrx:.2f} {y + vecry:.2f} {z + vecrz:.2f}'"
295
+ f" rotation='{nmz:.2f} 0 {-nmx:.2f} {rotation_angle:.2f}'>\n <shape>\n"
296
+ f" <appearance>\n <material diffusecolor='{bond_color}'>\n"
297
+ f" </material>\n </appearance>\n <cylinder radius='{bond_radius}'"
298
+ f" height='{length:.2f}'>\n </cylinder>\n </shape>\n </transform>\n")
299
+
300
+ xx, yy, zz = x - normx * r2, y - normy * r2, z - normz * r2
301
+ xml.append(f" <transform translation='{xx - vecx:.2f} {yy - vecy:.2f} {zz - vecz:.2f}'"
302
+ f" rotation='{nmz:.2f} 0 {-nmx:.2f} {rotation_angle:.2f}'>\n <shape>\n"
303
+ f" <appearance>\n <material diffusecolor='{bond_color}'>\n"
304
+ f" </material>\n </appearance>\n <cylinder radius='{bond_radius}'"
305
+ f" height='{length:.2f}'>\n </cylinder>\n </shape>\n </transform>\n")
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
+ else:
312
+ xml.extend(_render_dashes(nx, ny, nz, nmx, nmy, nmz, length, r_angle=rotation_angle))
313
+
314
+ for ring in self.aromatic_rings:
315
+ cx = sum(xyz[n][0] for n in ring) / len(ring)
316
+ cy = sum(xyz[n][1] for n in ring) / len(ring)
317
+ cz = sum(xyz[n][2] for n in ring) / len(ring)
318
+
319
+ for n, m in zip(ring, ring[1:]):
320
+ nx, ny, nz = xyz[n]
321
+ mx, my, mz = xyz[m]
322
+
323
+ aromatic = _render_aromatic_bond(nx, ny, nz, mx, my, mz, cx, cy, cz)
324
+ if aromatic:
325
+ veca_x, veca_y, veca_z, vecb_x, vecb_y, vecb_z = aromatic
326
+ ax, ay, az = nx + veca_x, ny + veca_y, nz + veca_z
327
+ abx, aby, abz = mx + vecb_x - ax, my + vecb_y - ay, mz + vecb_z - az
328
+ ab_ln = sqrt(abx ** 2 + aby ** 2 + abz ** 2)
329
+ if ab_ln < .0001:
330
+ continue
331
+ else:
332
+ xml.extend(_render_dashes(ax, ay, az, abx, aby, abz, ab_ln))
333
+
334
+ i, j = ring[-1], ring[0]
335
+ nx, ny, nz = xyz[i]
336
+ mx, my, mz = xyz[j]
337
+ aromatic = _render_aromatic_bond(nx, ny, nz, mx, my, mz, cx, cy, cz)
338
+ if aromatic:
339
+ veca_x, veca_y, veca_z, vecb_x, vecb_y, vecb_z = aromatic
340
+ ax, ay, az = nx + veca_x, ny + veca_y, nz + veca_z
341
+ abx, aby, abz = mx + vecb_x - ax, my + vecb_y - ay, mz + vecb_z - az
342
+ ab_ln = sqrt(abx ** 2 + aby ** 2 + abz ** 2)
343
+ if ab_ln < .0001:
344
+ continue
345
+ else:
346
+ xml.extend(_render_dashes(ax, ay, az, abx, aby, abz, ab_ln))
347
+ return ''.join(xml)
348
+
349
+
350
+ __all__ = ['X3domMolecule']
@@ -0,0 +1,48 @@
1
+ # -*- coding: utf-8 -*-
2
+ #
3
+ # Copyright 2017-2026 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, skip_labels_calculation=False) -> Union[MoleculeContainer, ReactionContainer]:
29
+ if compressed:
30
+ data = decompress(data)
31
+ try:
32
+ return MoleculeContainer.unpack(data, compressed=False, skip_labels_calculation=skip_labels_calculation)
33
+ except ValueError:
34
+ pass
35
+ # second try
36
+ return ReactionContainer.unpack(data, compressed=False)
37
+
38
+
39
+ def from_rdkit(mol, /):
40
+ return MoleculeContainer.from_rdkit(mol)
41
+
42
+
43
+ unpack = unpach
44
+ from_rdkit_molecule = from_rdkit
45
+
46
+
47
+ __all__ = [x for x in locals() if x.endswith('Container')]
48
+ __all__.extend(['Bond', 'QueryBond', 'unpack', 'unpach', 'from_rdkit', 'from_rdkit_molecule'])
@@ -0,0 +1,271 @@
1
+ # cython: freethreading_compatible=True
2
+ #
3
+ # Copyright 2022-2026 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