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,596 @@
1
+ # -*- coding: utf-8 -*-
2
+ #
3
+ # Copyright 2018-2026 Ramil Nugmanov <nougmanoff@protonmail.com>
4
+ # Copyright 2019-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 asyncio import new_event_loop
21
+ from collections import defaultdict
22
+ from math import atan2, sin, cos, hypot
23
+ from os.path import join
24
+ from tempfile import TemporaryDirectory
25
+ from typing import Literal
26
+ from uuid import uuid4
27
+ from zlib import compress
28
+ from .._functions import cached_method
29
+
30
+ cpk = tuple('''
31
+ #909090 #D9FFFF
32
+ #CC80FF #C2FF00 #FFB5B5 #101010 #3050F8 #FF0D0D #90E050 #B3E3F5
33
+ #AB5CF2 #8AFF00 #BFA6A6 #F0C8A0 #FF8000 #C6C600 #1FF01F #80D1E3
34
+ #8F40D4 #3DFF00 #E6E6E6 #BFC2C7 #A6A6AB #8A99C7 #9C7AC7
35
+ #E06633 #F090A0 #50D050 #C88033 #7D80B0 #C28F8F #668F8F #BD80E3 #FFA100 #A62929 #5CB8D1
36
+ #702EB0 #00FF00 #94FFFF #94E0E0 #73C2C9 #54B5B5 #3B9E9E
37
+ #248F8F #0A7D8C #006985 #C0C0C0 #FFD98F #A67573 #668080 #9E63B5 #D47A00 #940094 #429EB0
38
+ #57178F #00C900 #70D4FF
39
+ #FFFFC7 #D9FFC7 #C7FFC7 #A3FFC7 #8FFFC7 #61FFC7 #45FFC7
40
+ #30FFC7 #1FFFC7 #00FF9C #00E675 #00D452 #00BF38 #00AB24
41
+ #4DC2FF #4DA6FF #2194D6 #267DAB
42
+ #266696 #175487 #D0D0E0 #FFD123 #B8B8D0 #A6544D #575961 #9E4FB5 #AB5C00 #754F45 #428296
43
+ #420066 #007D00 #70ABFA
44
+ #00BAFF #00A1FF #008FFF #0080FF #006BFF #545CF2 #785CE3
45
+ #8A4FE3 #A136D4 #B31FD4 #B31FBA #B30DA6 #BD0D87 #C70066
46
+ #CC0059 #D1004F #D90045 #E00038
47
+ #E6002E #EB0026 #EB0026 #EB0026 #EB0026 #EB0026 #EB0026 #EB0026 #EB0026 #EB0026 #EB0026
48
+ '''.split())
49
+ _render_charge = {-4: '4-', -3: '3-', -2: '2-', -1: '-', 1: '+', 2: '2+', 3: '3+', 4: '4+'}
50
+ _render_config = {'carbon': False, 'dashes': (.2, .1), 'span_dy': .15, 'mapping': True, 'font_size': .5,
51
+ 'span_size': .35, 'other_size': 0.3, 'monochrome': False, 'bond_color': 'black', 'bond_width': .04,
52
+ 'other_color': 'black', 'bond_radius': .02, 'atom_radius': -.2, 'mapping_size': .25,
53
+ 'atoms_colors': cpk, 'triple_space': .13, 'double_space': .06, 'mapping_color': '#0305A7',
54
+ 'aromatic_space': .14, 'aromatic_dashes': (.15, .05), 'dx_m': .05, 'dy_m': .2, 'dx_s': .05, 'dy_s': .1,
55
+ 'other_font_style': 'monospace', 'dx_ci': .05, 'dy_ci': 0.2, 'symbols_font_style': 'sans-serif',
56
+ 'mapping_font_style': 'monospace', 'wedge_space': .08, 'arrow_color': 'black'}
57
+
58
+ loop = browser = None
59
+
60
+
61
+ async def svg_render(s, t, width, height, scale):
62
+ page = await browser.newPage()
63
+ await page.setViewport({'deviceScaleFactor': scale, 'width': width, 'height': height})
64
+ await page.goto(f'file://{s}')
65
+ element = await page.querySelector('svg')
66
+ await element.screenshot({'path': t, 'omitBackground': True})
67
+ await page.close()
68
+
69
+
70
+ def svg2png(svg: str, width: int = 1000, height: int = 1000, scale: float = 1.):
71
+ global loop, browser
72
+
73
+ if loop is None: # lazy browser launcher
74
+ from pyppeteer import launch
75
+
76
+ loop = new_event_loop()
77
+ browser = loop.run_until_complete(launch())
78
+ elif browser is None:
79
+ raise ImportError('pyppeteer initialization failed')
80
+
81
+ with TemporaryDirectory() as tmpdir:
82
+ with open(s := join(tmpdir, 'input.svg'), 'w') as f:
83
+ f.write(svg)
84
+
85
+ loop.run_until_complete(svg_render(s, (t := join(tmpdir, 'output.png')), width, height, scale))
86
+
87
+ with open(t, 'rb') as f:
88
+ return f.read()
89
+
90
+
91
+ def _rotate_vector(x1, y1, x2, y2):
92
+ """
93
+ rotate x,y vector over x2-x1, y2-y1 angle
94
+ """
95
+ angle = atan2(y2, x2)
96
+ cos_rad = cos(angle)
97
+ sin_rad = sin(angle)
98
+ return cos_rad * x1 - sin_rad * y1, sin_rad * x1 + cos_rad * y1
99
+
100
+
101
+ def _graph_svg(atoms, bonds, define, masks, uid, viewbox_x, viewbox_y, width, height):
102
+ svg = [f' <g id="{uid}-molecule">\n <defs>']
103
+ svg.extend(define)
104
+ if bonds:
105
+ if masks:
106
+ svg.append(f' <mask id="{uid}-mask">\n'
107
+ f' <rect x="{viewbox_x:.2f}" y="{viewbox_y:.2f}" '
108
+ f'width="{width:.2f}" height="{height:.2f}" fill="white"/>')
109
+ svg.extend(masks)
110
+ svg.append(' </mask>\n </defs>\n'
111
+ f' <g fill="none" stroke="{_render_config["bond_color"]}" '
112
+ f'stroke-width="{_render_config["bond_width"]:.2f}" mask="url(#{uid}-mask)">')
113
+ if len(bonds) == 1: # SVG BUG adhoc
114
+ svg.append(f' <line x1="{viewbox_x:.2f}" y1="{viewbox_y:.2f}" '
115
+ f'x2="{viewbox_x + width:.2f}" y2="{viewbox_y:.2f}" stroke="none"/>')
116
+ else:
117
+ svg.append(f' </defs>\n <g fill="none" stroke="{_render_config["bond_color"]}" '
118
+ f'stroke-width="{_render_config["bond_width"]:.2f}">')
119
+ svg.extend(bonds)
120
+ svg.append(' </g>')
121
+ else:
122
+ svg.append(' </defs>')
123
+
124
+ svg.extend(atoms)
125
+ svg.append(' </g>')
126
+ return svg
127
+
128
+
129
+ def _render_aromatic_bond(n_x, n_y, m_x, m_y, c_x, c_y):
130
+ aromatic_space = _render_config['aromatic_space']
131
+ dash3, dash4 = _render_config['aromatic_dashes']
132
+ # n aligned xy
133
+ mn_x, mn_y, cn_x, cn_y = m_x - n_x, m_y - n_y, c_x - n_x, c_y - n_y
134
+
135
+ # nm reoriented xy
136
+ mr_x, mr_y = hypot(mn_x, mn_y), 0
137
+ cr_x, cr_y = _rotate_vector(cn_x, cn_y, mn_x, -mn_y)
138
+
139
+ if cr_y and aromatic_space / cr_y < .65:
140
+ if cr_y > 0:
141
+ r_y = aromatic_space
142
+ else:
143
+ r_y = -aromatic_space
144
+ cr_y = -cr_y
145
+
146
+ ar_x = aromatic_space * cr_x / cr_y
147
+ br_x = mr_x - aromatic_space * (mr_x - cr_x) / cr_y
148
+
149
+ # backward reorienting
150
+ an_x, an_y = _rotate_vector(ar_x, r_y, mn_x, mn_y)
151
+ bn_x, bn_y = _rotate_vector(br_x, r_y, mn_x, mn_y)
152
+ a_x, a_y = n_x + an_x, n_y + an_y
153
+ b_x, b_y = n_x + bn_x, n_y + bn_y
154
+
155
+ return f' <line x1="{a_x:.2f}" y1="{-a_y:.2f}" x2="{b_x:.2f}" y2="{-b_y:.2f}" ' \
156
+ f'stroke-dasharray="{dash3:.2f} {dash4:.2f}"/>'
157
+
158
+
159
+ def depict_settings(*, carbon: bool = False, aam: bool = True, monochrome: bool = False,
160
+ bond_color: str = 'black', aam_color: str = '#0305A7', atoms_colors: tuple = cpk,
161
+ bond_width: float = .04, wedge_space: float = .08, dashes: tuple[float, float] = (.2, .1),
162
+ aromatic_dashes: tuple[float, float] = (.15, .05), dx_ci: float = .05, dy_ci: float = .2,
163
+ dx_m: float = .05, dy_m: float = .2, dx_s: float = .05, dy_s: float = .1, span_dy: float = .15,
164
+ double_space: float = .06, triple_space: float = .13, aromatic_space: float = .14,
165
+ atom_radius: float = .2, bond_radius=.02, font_size: float = .5, other_size: float = .3,
166
+ span_size: float = .35, aam_size: float = .25, symbols_font_style: str = 'sans-serif',
167
+ other_font_style: str = 'monospace', other_color: str = 'black', arrow_color: str = 'black',
168
+ mapping_font_style: str = 'monospace'):
169
+ """
170
+ Settings for depict of chemical structures
171
+
172
+ :param carbon: if True, depict atom C
173
+ :param font_size: font size
174
+ :param aam_size: atom-to-atom mapping font size
175
+ :param span_size: font size for hydrogen count
176
+ :param other_size: isotope, radical, charges, neighbors and hybridization symbols size
177
+ :param bond_width: bond width
178
+ :param bond_color: color of bonds
179
+ :param aam_color: atom-to-atom mapping color
180
+ :param atoms_colors: atom colors where key is atomic number - 1, value is atom color (str)
181
+ :param other_color: color for charges, radicals, isotopes
182
+ :param arrow_color: color of reaction arrow and plus signs
183
+ :param symbols_font_style: font style for atom symbols
184
+ :param other_font_style: font style for charges, radicals, isotopes, hybridization and neighbors
185
+ :param aam: if True, depict mapping
186
+ :param monochrome: if True, colors of items in molecule not used
187
+ :param dashes: first value is long of visible line, second is long of invisible line
188
+ :param aromatic_space: space between simple and aromatic bonds
189
+ :param triple_space: space between simple and triple bonds
190
+ :param double_space: space between simple and double bonds
191
+ :param aromatic_dashes: first value is long of visible line, second is long of invisible line
192
+ :param atom_radius: radius of atoms spheres in depict3d. if negative is multiplier to covalent radii
193
+ :param bond_radius: radius of bonds spheres in depict3d
194
+ :param dx_ci: x-axis offset relative to the center of the atom symbol for radical, charges, isotope
195
+ :param dy_ci: y-axis offset relative to the center of the atom symbol for radical, charges, isotope
196
+ :param dx_m: x-axis offset relative to the center of the atom symbol for atom-to-atom mapping
197
+ :param dy_m: y-axis offset relative to the center of the atom symbol for atom-to-atom mapping
198
+ :param dx_s: x-axis offset relative to the center of the atom symbol for extended stereo label
199
+ :param dy_s: y-axis offset relative to the center of the atom symbol for extended stereo label
200
+ :param span_dy: y-axis offset relative to the center of the atom symbol for hydrogen count
201
+ :param mapping_font_style: font style for mapping
202
+ :param wedge_space: wedge bond width
203
+ """
204
+ _render_config['carbon'] = carbon
205
+ _render_config['dashes'] = dashes
206
+ _render_config['span_dy'] = span_dy
207
+ _render_config['mapping'] = aam
208
+ _render_config['font_size'] = font_size
209
+ _render_config['span_size'] = span_size
210
+ _render_config['other_size'] = other_size
211
+ _render_config['monochrome'] = monochrome
212
+ _render_config['bond_color'] = bond_color
213
+ _render_config['bond_width'] = bond_width
214
+ _render_config['other_color'] = other_color
215
+ _render_config['arrow_color'] = arrow_color
216
+ _render_config['bond_radius'] = bond_radius
217
+ _render_config['atom_radius'] = -atom_radius
218
+ _render_config['mapping_size'] = aam_size
219
+ _render_config['atoms_colors'] = atoms_colors
220
+ _render_config['triple_space'] = triple_space
221
+ _render_config['double_space'] = double_space
222
+ _render_config['mapping_color'] = aam_color
223
+ _render_config['aromatic_space'] = aromatic_space
224
+ _render_config['aromatic_dashes'] = aromatic_dashes
225
+ _render_config['dx_m'], _render_config['dy_m'] = dx_m, dy_m
226
+ _render_config['other_font_style'] = other_font_style
227
+ _render_config['dx_ci'], _render_config['dy_ci'] = dx_ci, dy_ci
228
+ _render_config['dx_s'], _render_config['dy_s'] = dx_s, dy_s
229
+ _render_config['symbols_font_style'] = symbols_font_style
230
+ _render_config['mapping_font_style'] = mapping_font_style
231
+ _render_config['wedge_space'] = wedge_space
232
+
233
+
234
+ class DepictMolecule:
235
+ __slots__ = ()
236
+
237
+ def depict(self, *, width=None, height=None, clean2d: bool = True,
238
+ format: Literal['svg', 'png', 'svgz'] = 'svg', png_width=1000, png_heigh=1000, png_scale=1.,
239
+ _embedding=False) -> str | bytes:
240
+ """
241
+ Depict molecule in SVG or PNG format.
242
+
243
+ :param width: set svg width param. by default auto-calculated.
244
+ :param height: set svg height param. by default auto-calculated.
245
+ :param clean2d: calculate coordinates if necessary.
246
+ :param format: output format - svg string, png bytes or gz compressed svg
247
+ :param png_width, png_heigh: viewport size for PNG rendering
248
+ :param png_scale: image scaling in PNG rendering
249
+ """
250
+ uid = str(uuid4())
251
+ min_x = min(a.x for _, a in self.atoms())
252
+ max_x = max(a.x for _, a in self.atoms())
253
+ min_y = min(a.y for _, a in self.atoms())
254
+ max_y = max(a.y for _, a in self.atoms())
255
+ if clean2d and len(self) > 1 and max_y - min_y < .01 and max_x - min_x < 0.01:
256
+ self.clean2d()
257
+ min_x = min(a.x for _, a in self.atoms())
258
+ max_x = max(a.x for _, a in self.atoms())
259
+ min_y = min(a.y for _, a in self.atoms())
260
+ max_y = max(a.y for _, a in self.atoms())
261
+
262
+ bonds = self.__render_bonds()
263
+ atoms, define, masks = self.__render_atoms(uid)
264
+ if _embedding:
265
+ return atoms, bonds, define, masks, uid, min_x, min_y, max_x, max_y
266
+
267
+ font_size = _render_config['font_size']
268
+ font125 = 1.25 * font_size
269
+ _width = max_x - min_x + 4.0 * font_size
270
+ _height = max_y - min_y + 2.5 * font_size
271
+ viewbox_x = min_x - font125
272
+ viewbox_y = -max_y - font125
273
+
274
+ if width is None:
275
+ width = f'{_width:.2f}cm'
276
+ if height is None:
277
+ height = f'{_height:.2f}cm'
278
+
279
+ svg = [f'<svg width="{width}" height="{height}" '
280
+ f'viewBox="{viewbox_x:.2f} {viewbox_y:.2f} {_width:.2f} {_height:.2f}" '
281
+ 'xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1">']
282
+ svg.extend(_graph_svg(atoms, bonds, define, masks, uid, viewbox_x, viewbox_y, _width, _height))
283
+ svg.append('</svg>')
284
+ svg = '\n'.join(svg)
285
+ if format == 'svg':
286
+ return svg
287
+ elif format == 'png':
288
+ return svg2png(svg, png_width, png_heigh, png_scale)
289
+ elif format == 'svgz':
290
+ return compress(svg.encode(), 9)
291
+ raise ValueError(f'format must be svg, png or svgz, not {format}')
292
+
293
+ @cached_method
294
+ def _repr_svg_(self):
295
+ return self.depict()
296
+
297
+ def __render_bonds(self):
298
+ atoms = self._atoms
299
+ svg = []
300
+ double_space = _render_config['double_space']
301
+ triple_space = _render_config['triple_space']
302
+ wedge_space = _render_config['wedge_space']
303
+ dash1, dash2 = _render_config['dashes']
304
+ color = f' fill="{_render_config["bond_color"]}"'
305
+
306
+ wedge = defaultdict(set)
307
+ for n, m, s in self._wedge_map:
308
+ wedge[n].add(m)
309
+ wedge[m].add(n)
310
+
311
+ nx, ny = atoms[n].xy
312
+ mx, my = atoms[m].xy
313
+ ny, my = -ny, -my
314
+ dx, dy = _rotate_vector(0, wedge_space, mx - nx, my - ny)
315
+
316
+ svg.append(f' <path d="M{nx:.2f} {ny:.2f} L{mx + dx:.2f} {my + dy:.2f} '
317
+ f'L{mx - dx:.2f} {my - dy:.2f} Z"{s == 1 and color or ""}/>')
318
+
319
+ for n, m, bond in self.bonds():
320
+ if m in wedge[n]:
321
+ continue
322
+ nx, ny = atoms[n].xy
323
+ mx, my = atoms[m].xy
324
+ ny, my = -ny, -my
325
+ if bond in (1, 4):
326
+ svg.append(f' <line x1="{nx:.2f}" y1="{ny:.2f}" x2="{mx:.2f}" y2="{my:.2f}"/>')
327
+ elif bond == 2:
328
+ dx, dy = _rotate_vector(0, double_space, mx - nx, ny - my)
329
+ svg.append(f' <line x1="{nx + dx:.2f}" y1="{ny - dy:.2f}" x2="{mx + dx:.2f}" y2="{my - dy:.2f}"/>')
330
+ svg.append(f' <line x1="{nx - dx:.2f}" y1="{ny + dy:.2f}" x2="{mx - dx:.2f}" y2="{my + dy:.2f}"/>')
331
+ elif bond == 3:
332
+ dx, dy = _rotate_vector(0, triple_space, mx - nx, ny - my)
333
+ svg.append(f' <line x1="{nx + dx:.2f}" y1="{ny - dy:.2f}" x2="{mx + dx:.2f}" y2="{my - dy:.2f}"/>')
334
+ svg.append(f' <line x1="{nx:.2f}" y1="{ny:.2f}" x2="{mx:.2f}" y2="{my:.2f}"/>')
335
+ svg.append(f' <line x1="{nx - dx:.2f}" y1="{ny + dy:.2f}" x2="{mx - dx:.2f}" y2="{my + dy:.2f}"/>')
336
+ else:
337
+ svg.append(f' <line x1="{nx:.2f}" y1="{ny:.2f}" x2="{mx:.2f}" y2="{my:.2f}" '
338
+ f'stroke-dasharray="{dash1:.2f} {dash2:.2f}"/>')
339
+
340
+ for ring in self.aromatic_rings:
341
+ cx = sum(atoms[n].x for n in ring) / len(ring)
342
+ cy = sum(atoms[n].y for n in ring) / len(ring)
343
+
344
+ for n, m in zip(ring, ring[1:]):
345
+ nx, ny = atoms[n].xy
346
+ mx, my = atoms[m].xy
347
+ aromatic = _render_aromatic_bond(nx, ny, mx, my, cx, cy)
348
+ if aromatic:
349
+ svg.append(aromatic)
350
+
351
+ nx, ny = atoms[ring[-1]].xy
352
+ mx, my = atoms[ring[0]].xy
353
+ aromatic = _render_aromatic_bond(nx, ny, mx, my, cx, cy)
354
+ if aromatic:
355
+ svg.append(aromatic)
356
+ return svg
357
+
358
+ def __render_atoms(self, uid):
359
+ bonds = self._bonds
360
+
361
+ carbon = _render_config['carbon']
362
+ mapping = _render_config['mapping']
363
+ span_size = _render_config['span_size']
364
+ font_size = _render_config['font_size']
365
+ monochrome = _render_config['monochrome']
366
+ other_size = _render_config['other_size']
367
+ atoms_colors = _render_config['atoms_colors']
368
+ mapping_size = _render_config['mapping_size']
369
+ dx_m, dy_m = _render_config['dx_m'], _render_config['dy_m']
370
+ dx_ci, dy_ci = _render_config['dx_ci'], _render_config['dy_ci']
371
+ dx_s, dy_s = _render_config['dx_s'], _render_config['dy_s']
372
+ symbols_font_style = _render_config['symbols_font_style']
373
+ span_dy = _render_config['span_dy']
374
+ other_font_style = _render_config['other_font_style']
375
+ mapping_font_style = _render_config['mapping_font_style']
376
+
377
+ if monochrome:
378
+ map_fill = other_fill = 'black'
379
+ else:
380
+ map_fill = _render_config['mapping_color']
381
+ other_fill = _render_config['other_color']
382
+
383
+ font2 = .2 * font_size
384
+ font3 = .3 * font_size
385
+ font4 = .4 * font_size
386
+ font5 = .5 * font_size
387
+ font6 = .6 * font_size
388
+ font7 = .7 * font_size
389
+ font15 = .15 * font_size
390
+ font25 = .25 * font_size
391
+ stroke_width_s = font_size * .1
392
+ stroke_width_o = other_size * .1
393
+ stroke_width_m = mapping_size * .1
394
+
395
+ svg = []
396
+ maps = []
397
+ symbols = []
398
+ fill_zone = []
399
+ others = []
400
+ define = []
401
+ mask = []
402
+
403
+ for n, atom in self.atoms():
404
+ x, y = atom.x, -atom.y
405
+ symbol = atom.atomic_symbol
406
+ if (symbol != 'C' or atom.charge or atom.is_radical or atom.isotope or carbon
407
+ or not bonds[n] or sum(b == 2 for b in bonds[n].values()) == 2):
408
+ if atom.charge:
409
+ others.append(f' <text x="{x:.2f}" y="{y:.2f}" dx="{dx_ci:.2f}" dy="-{dy_ci:.2f}">'
410
+ f'{_render_charge[atom.charge]}{"↑" if atom.is_radical else ""}</text>')
411
+ elif atom.is_radical:
412
+ others.append(f' <text x="{x:.2f}" y="{y:.2f}" dx="{dx_ci:.2f}" dy="-{dy_ci:.2f}">↑</text>')
413
+ if atom.isotope:
414
+ others.append(f' <text x="{x:.2f}" y="{y:.2f}" dx="-{dx_ci:.2f}" dy="-{dy_ci:.2f}" '
415
+ f'text-anchor="end">{atom.isotope}</text>')
416
+ if atom.stereo is not None and atom.extended_stereo:
417
+ label = f'&amp;{atom.extended_stereo}' if atom.extended_stereo > 0 else f'o{-atom.extended_stereo}'
418
+ others.append(f' <text x="{x:.2f}" y="{y:.2f}" dx="{dx_s:.2f}" dy="-{dy_s + font3:.2f}">'
419
+ f'{label}</text>')
420
+
421
+ if len(symbol) > 1:
422
+ dx = font7
423
+ dx_mm = dx_m + font5
424
+ if symbol[-1] in ('l', 'i', 'r', 't'):
425
+ rx = font6
426
+ ax = font25
427
+ else:
428
+ rx = font7
429
+ ax = font15
430
+ fill_zone.append(f' <ellipse cx="{x - ax:.2f}" cy="{y:.2f}" rx="{rx}" ry="{font4}"/>')
431
+ else:
432
+ if symbol == 'I':
433
+ dx = font15
434
+ dx_mm = dx_m
435
+ else:
436
+ dx = font4
437
+ dx_mm = dx_m + font2
438
+ fill_zone.append(f' <circle cx="{x:.2f}" cy="{y:.2f}" r="{font4:.2f}"/>')
439
+
440
+ h = atom.implicit_hydrogens
441
+ if h == 1:
442
+ h = 'H'
443
+ elif h:
444
+ h = f'H<tspan dy="{span_dy:.2f}" font-size="{span_size:.2f}">{h}</tspan>'
445
+ else:
446
+ h = ''
447
+ symbols.append(f' <text id="{uid}-{n}" x="{x:.2f}" y="{y:.2f}" dx="-{dx:.2f}" dy="{font4:.2f}">'
448
+ f'{symbol}{h}</text>')
449
+
450
+ svg.append(f' <use xlink:href="#{uid}-{n}" '
451
+ f'fill="{"black" if monochrome else atoms_colors[atom.atomic_number - 1]}"/>')
452
+
453
+ if mapping:
454
+ maps.append(f' <text x="{x:.2f}" y="{y:.2f}" dx="-{dx_mm:.2f}" '
455
+ f'dy="{dy_m + font3:.2f}">{n}</text>')
456
+ else:
457
+ if mapping:
458
+ maps.append(f' <text x="{x:.2f}" y="{y:.2f}" dx="-{dx_m:.2f}" dy="{dy_m:.2f}">{n}</text>')
459
+ if atom.stereo is not None and atom.extended_stereo:
460
+ label = f'&amp;{atom.extended_stereo}' if atom.extended_stereo > 0 else f'o{-atom.extended_stereo}'
461
+ others.append(f' <text x="{x:.2f}" y="{y:.2f}" dx="{dx_s:.2f}" dy="-{dy_s:.2f}">'
462
+ f'{label}</text>')
463
+
464
+ if svg: # group atoms symbols
465
+ if fill_zone:
466
+ mask.append(' <g>')
467
+ mask.extend(fill_zone)
468
+ mask.append(' </g>')
469
+
470
+ svg.insert(0, f' <g font-size="{font_size:.2f}" font-family="{symbols_font_style}">')
471
+ svg.append(' </g>')
472
+ define.append(f' <g id="{uid}-symbols" font-size="{font_size:.2f}" '
473
+ f'font-family="{symbols_font_style}">')
474
+ define.extend(symbols)
475
+ define.append(' </g>')
476
+ mask.append(' <g stroke="black">\n '
477
+ f'<use xlink:href="#{uid}-symbols" stroke-width="{stroke_width_s:.2f}"/>')
478
+
479
+ if others:
480
+ define.append(f' <g id="{uid}-attrs" font-size="{other_size:.2f}" '
481
+ f'font-family="{other_font_style}">')
482
+ define.extend(others)
483
+ define.append(' </g>')
484
+ svg.append(f' <use xlink:href="#{uid}-attrs" fill="{other_fill}"/>')
485
+ mask.append(f' <use xlink:href="#{uid}-attrs" stroke-width="{stroke_width_o:.2f}"/>')
486
+ if maps:
487
+ if not svg: # no atoms but maps
488
+ mask.append(' <g stroke="black">')
489
+ define.append(f' <g id="{uid}-mapping" font-size="{mapping_size:.2f}" '
490
+ f'font-family="{mapping_font_style}" text-anchor="end">')
491
+ define.extend(maps)
492
+ define.append(' </g>')
493
+ svg.append(f' <use xlink:href="#{uid}-mapping" fill="{map_fill}"/>')
494
+ mask.append(f' <use xlink:href="#{uid}-mapping" stroke-width="{stroke_width_m:.2f}"/>\n'
495
+ ' </g>')
496
+ elif svg: # no maps but atoms
497
+ mask.append(' </g>')
498
+ return svg, define, mask
499
+
500
+
501
+ class DepictReaction:
502
+ __slots__ = ()
503
+
504
+ def depict(self, *, width=None, height=None, clean2d: bool = True,
505
+ format: Literal['svg', 'png', 'svgz'] = 'svg',
506
+ png_width=1000, png_heigh=1000, png_scale=1.) -> str | bytes:
507
+ """
508
+ Depict reaction in SVG format.
509
+
510
+ :param width: set svg width param. by default auto-calculated.
511
+ :param height: set svg height param. by default auto-calculated.
512
+ :param clean2d: calculate coordinates if necessary.
513
+ :param format: output format - svg string, png bytes or gz compressed svg
514
+ :param png_width, png_heigh: viewport size for PNG rendering
515
+ :param png_scale: image scaling in PNG rendering
516
+ """
517
+ arrow_color = _render_config['arrow_color']
518
+ if not self._arrow:
519
+ if clean2d:
520
+ for m in self.molecules():
521
+ if len(m) > 1:
522
+ min_x = min(a.x for _, a in m.atoms())
523
+ max_x = max(a.x for _, a in m.atoms())
524
+ min_y = min(a.y for _, a in m.atoms())
525
+ max_y = max(a.y for _, a in m.atoms())
526
+ if max_y - min_y < .01 and max_x - min_x < 0.01:
527
+ m.clean2d()
528
+ self.fix_positions()
529
+
530
+ r_atoms = []
531
+ r_bonds = []
532
+ r_defines = []
533
+ r_masks = []
534
+ r_uids = []
535
+ r_max_x = r_max_y = r_min_y = 0
536
+ for m in self.molecules():
537
+ atoms, bonds, define, masks, uid, min_x, min_y, max_x, max_y = m.depict(clean2d=False, _embedding=True)
538
+ r_atoms.append(atoms)
539
+ r_bonds.append(bonds)
540
+ r_defines.append(define)
541
+ r_masks.append(masks)
542
+ r_uids.append(uid)
543
+ if max_x > r_max_x:
544
+ r_max_x = max_x
545
+ if max_y > r_max_y:
546
+ r_max_y = max_y
547
+ if min_y < r_min_y:
548
+ r_min_y = min_y
549
+
550
+ font_size = _render_config['font_size']
551
+ font125 = 1.25 * font_size
552
+ _width = r_max_x + 4.0 * font_size
553
+ _height = r_max_y - r_min_y + 2.5 * font_size
554
+ viewbox_x = -font125
555
+ viewbox_y = -r_max_y - font125
556
+
557
+ if width is None:
558
+ width = f'{_width:.2f}cm'
559
+ if height is None:
560
+ height = f'{_height:.2f}cm'
561
+
562
+ svg = [f'<svg width="{width}" height="{height}" '
563
+ f'viewBox="{viewbox_x:.2f} {viewbox_y:.2f} {_width:.2f} {_height:.2f}" '
564
+ 'xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1">\n'
565
+ ' <defs>\n <marker id="arrow" markerWidth="10" markerHeight="10" '
566
+ f'refX="0" refY="3" orient="auto">\n <path d="M0,0 L0,6 L9,3" fill="{arrow_color}"/>\n'
567
+ ' </marker>\n </defs>\n'
568
+ f' <line x1="{self._arrow[0]:.2f}" y1="0" x2="{self._arrow[1]:.2f}" y2="0" '
569
+ f'fill="none" stroke="{arrow_color}" stroke-width=".04" marker-end="url(#arrow)"/>']
570
+
571
+ sings_plus = self._signs
572
+ if sings_plus:
573
+ svg.append(f' <g fill="none" stroke="{arrow_color}" stroke-width=".04">')
574
+ for x in sings_plus:
575
+ svg.append(f' <line x1="{x + .35:.2f}" y1="0" x2="{x + .65:.2f}" y2="0"/>')
576
+ svg.append(f' <line x1="{x + .5:.2f}" y1="0.15" x2="{x + .5:.2f}" y2="-0.15"/>')
577
+ svg.append(' </g>')
578
+
579
+ for atoms, bonds, define, masks, uid in zip(r_atoms, r_bonds, r_defines, r_masks, r_uids):
580
+ svg.extend(_graph_svg(atoms, bonds, define, masks, uid, viewbox_x, viewbox_y, _width, _height))
581
+ svg.append('</svg>')
582
+ svg = '\n'.join(svg)
583
+ if format == 'svg':
584
+ return svg
585
+ elif format == 'png':
586
+ return svg2png(svg, png_width, png_heigh, png_scale)
587
+ elif format == 'svgz':
588
+ return compress(svg.encode(), 9)
589
+ raise ValueError(f'format must be svg, png or svgz, not {format}')
590
+
591
+ @cached_method
592
+ def _repr_svg_(self):
593
+ return self.depict()
594
+
595
+
596
+ __all__ = ['DepictMolecule', 'DepictReaction', 'depict_settings']
@@ -0,0 +1,43 @@
1
+ # -*- coding: utf-8 -*-
2
+ #
3
+ # Copyright 2021-2026 Ramil Nugmanov <nougmanoff@protonmail.com>
4
+ # Copyright 2021 Aleksandr Sizov <murkyrussian@gmail.com>
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 .linear import *
21
+ from .morgan import *
22
+
23
+
24
+ class Fingerprints(LinearFingerprint, MorganFingerprint):
25
+ __slots__ = ()
26
+
27
+ @property
28
+ def _atom_identifiers(self):
29
+ return {idx: hash((atom.isotope or 0, atom.atomic_number, atom.charge, atom.is_radical))
30
+ for idx, atom in self.atoms()}
31
+
32
+
33
+ class FingerprintsCGR(LinearFingerprint, MorganFingerprint):
34
+ __slots__ = ()
35
+
36
+ @property
37
+ def _atom_identifiers(self):
38
+ return {idx: hash((atom.isotope or 0, atom.atomic_number, atom.charge, atom.p_charge,
39
+ atom.is_radical, atom.p_is_radical))
40
+ for idx, atom in self._atoms.items()}
41
+
42
+
43
+ __all__ = ['Fingerprints', 'FingerprintsCGR']