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,597 @@
1
+ # -*- coding: utf-8 -*-
2
+ #
3
+ # Copyright 2017-2026 Ramil Nugmanov <nougmanoff@protonmail.com>
4
+ # Copyright 2019 Timur Gimadiev <timur.gimadiev@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 abc import ABC, abstractmethod
21
+ from collections import defaultdict
22
+ from functools import cached_property
23
+ from heapq import heappop, heappush
24
+ from itertools import product
25
+ from random import random
26
+ from collections.abc import Callable
27
+ from .._functions import cached_method
28
+
29
+ charge_str = {-4: '-4', -3: '-3', -2: '-2', -1: '-', 0: '0', 1: '+', 2: '+2', 3: '+3', 4: '+4'}
30
+ order_str = {1: '-', 2: '=', 3: '#', 4: ':', 8: '~', None: '.'}
31
+ organic_set = {'C', 'N', 'O', 'P', 'S', 'F', 'Cl', 'Br', 'I', 'B'}
32
+ dyn_order_str = {(None, 1): '[.>-]', (None, 2): '[.>=]', (None, 3): '[.>#]', (None, 4): '[.>:]', (None, 8): '[.>~]',
33
+ (1, None): '[->.]', (1, 1): '', (1, 2): '[->=]', (1, 3): '[->#]', (1, 4): '[->:]', (1, 8): '[->~]',
34
+ (2, None): '[=>.]', (2, 1): '[=>-]', (2, 2): '=', (2, 3): '[=>#]', (2, 4): '[=>:]', (2, 8): '[=>~]',
35
+ (3, None): '[#>.]', (3, 1): '[#>-]', (3, 2): '[#>=]', (3, 3): '#', (3, 4): '[#>:]', (3, 8): '[#>~]',
36
+ (4, None): '[:>.]', (4, 1): '[:>-]', (4, 2): '[:>=]', (4, 3): '[:>#]', (4, 4): ':', (4, 8): '[:>~]',
37
+ (8, None): '[~>.]', (8, 1): '[~>-]', (8, 2): '[~>=]', (8, 3): '[~>#]', (8, 4): '[~>:]', (8, 8): '~'}
38
+
39
+ dyn_charge_str = {(i, j): f'{charge_str[i]}>{charge_str[j]}' if i != j else charge_str[i]
40
+ for i, j in product(range(-4, 5), repeat=2)}
41
+ dyn_charge_str[(0, 0)] = ''
42
+
43
+ dyn_radical_str = {(True, True): '*', (True, False): '*>^', (False, True): '^>*'}
44
+
45
+ # atomic number constants
46
+ B = 5
47
+ C = 6
48
+ N = 7
49
+ P = 15
50
+ S = 16
51
+
52
+
53
+ class Smiles(ABC):
54
+ __slots__ = ()
55
+
56
+ @property
57
+ def smiles(self) -> str:
58
+ """
59
+ Generate SMILES string of the molecule.
60
+ """
61
+ return str(self)
62
+
63
+ @cached_method
64
+ def __str__(self):
65
+ smiles, order = self._smiles(self._smiles_order(), _return_order=True)
66
+ if (cx := self._format_cxsmiles(order)) is not None:
67
+ smiles.append(' ')
68
+ smiles.append(cx)
69
+
70
+ self.__dict__['smiles_atoms_order'] = tuple(order) # cache smiles_atoms_order
71
+ return ''.join(smiles)
72
+
73
+ def __format__(self, format_spec, *, _return_order=False):
74
+ """
75
+ Signature generation options.
76
+
77
+ :param format_spec: String with keys:
78
+ a - Generate asymmetric closures.
79
+ !s - Disable stereo marks.
80
+ A - Use aromatic bonds instead aromatic atoms.
81
+ m - Set atom mapping.
82
+ r - Generate random-ordered smiles.
83
+ h - Show implicit hydrogens.
84
+ !b - Disable bonds tokens.
85
+ !x - Disable CXSMILES extension.
86
+ !z - Disable charge representation.
87
+
88
+ Combining possible. Order independent. Another keys ignored.
89
+ """
90
+ if format_spec:
91
+ kwargs = {}
92
+ if 'a' in format_spec:
93
+ kwargs['asymmetric_closures'] = True
94
+ if '!s' in format_spec:
95
+ kwargs['stereo'] = False
96
+ if 'A' in format_spec:
97
+ kwargs['aromatic'] = False
98
+ if 'm' in format_spec:
99
+ kwargs['mapping'] = True
100
+ if 'h' in format_spec:
101
+ kwargs['hydrogens'] = True
102
+ if '!b' in format_spec:
103
+ kwargs['bonds'] = False
104
+ if '!z' in format_spec:
105
+ kwargs['charges'] = False
106
+
107
+ if 'r' in format_spec:
108
+ kwargs['random'] = True
109
+
110
+ def w(_):
111
+ return random()
112
+ else:
113
+ w = self._smiles_order('!s' not in format_spec)
114
+
115
+ smiles, order = self._smiles(w, _return_order=True, **kwargs)
116
+ if _return_order:
117
+ return ''.join(smiles), order
118
+ elif '!x' in format_spec or (cx := self._format_cxsmiles(order)) is None:
119
+ return ''.join(smiles)
120
+ else:
121
+ smiles.append(' ')
122
+ smiles.append(cx)
123
+ return ''.join(smiles)
124
+
125
+ elif _return_order:
126
+ smiles, order = self._smiles(self._smiles_order(), _return_order=True)
127
+ smiles = ''.join(smiles)
128
+ if (cx := self._format_cxsmiles(order)) is not None: # cache molecule smiles
129
+ self.__dict__['__cached_method___str__'] = f'{smiles} {cx}'
130
+ else:
131
+ self.__dict__['__cached_method___str__'] = smiles
132
+ self.__dict__['smiles_atoms_order'] = tuple(order) # cache smiles_atoms_order
133
+ return smiles, order
134
+ return str(self)
135
+
136
+ def __eq__(self, other):
137
+ return isinstance(other, Smiles) and str(self) == str(other)
138
+
139
+ @cached_method
140
+ def __hash__(self):
141
+ return hash(str(self))
142
+
143
+ @cached_property
144
+ def smiles_atoms_order(self) -> tuple[int, ...]:
145
+ """
146
+ Atoms order in canonic SMILES.
147
+ """
148
+ smiles, order = self._smiles(self._smiles_order(), _return_order=True)
149
+ if (cx := self._format_cxsmiles(order)) is not None:
150
+ smiles.append(' ')
151
+ smiles.append(cx)
152
+ self.__dict__['__cached_method___str__'] = ''.join(smiles) # cache smiles
153
+ return tuple(order)
154
+
155
+ def _smiles(self, weights, *, asymmetric_closures=False,
156
+ open_parenthesis='(', close_parenthesis=')', delimiter='.', _return_order=False, **kwargs):
157
+ if not self._atoms:
158
+ return []
159
+ bonds = self._bonds
160
+ atoms_set = set(self._atoms)
161
+ seen = {}
162
+ cycle = 0
163
+ casted_cycles = {}
164
+ string = []
165
+ order = []
166
+ visited_bond = set()
167
+ heap = list(range(1, 100))
168
+
169
+ if kwargs.get('random', False):
170
+ mod_weights_start = mod_weights = weights
171
+ else:
172
+ groups = defaultdict(int)
173
+ for n in atoms_set:
174
+ groups[weights(n)] -= 1
175
+
176
+ def mod_weights_start(x):
177
+ return (groups[weights(x)], # common groups
178
+ weights(x)) # smallest weight
179
+
180
+ def mod_weights(x):
181
+ return (groups[weights(x)], # common groups
182
+ weights(x), # smallest weight
183
+ seen[x]) # BFS nearest to starting
184
+
185
+ while True:
186
+ start = min(atoms_set, key=mod_weights_start)
187
+ if not kwargs.get('random', False):
188
+ seen[start] = 0
189
+ queue = [(start, 1)]
190
+ while queue:
191
+ n, d = queue.pop(0)
192
+ for m in bonds[n].keys() - seen.keys():
193
+ queue.append((m, d + 1))
194
+ seen[m] = d
195
+
196
+ # modified NX dfs with cycle detection
197
+ stack = [(start, len(atoms_set), iter(sorted(bonds[start], key=mod_weights)))]
198
+ visited = {start: []} # predecessors for stereo. atom: (visited[atom], *edges[atom])
199
+ disconnected = set()
200
+ edges = defaultdict(list)
201
+ tokens = defaultdict(list)
202
+ while stack:
203
+ parent, depth_now, children = stack[-1]
204
+ try:
205
+ child = next(children)
206
+ except StopIteration:
207
+ stack.pop()
208
+ else:
209
+ if child not in visited:
210
+ edges[parent].append(child)
211
+ visited[child] = [parent]
212
+ if depth_now > 1:
213
+ front = bonds[child].keys() - {parent}
214
+ if front:
215
+ stack.append((child, depth_now - 1, iter(sorted(front, key=mod_weights))))
216
+ elif (child, parent) not in disconnected:
217
+ disconnected.add((parent, child))
218
+ disconnected.add((child, parent))
219
+ cycle += 1
220
+ tokens[parent].append((child, cycle))
221
+ tokens[child].append((parent, cycle))
222
+
223
+ # flatten directed graph: edges
224
+ stack = [[start, 0, [start]]]
225
+ while True:
226
+ tail, closure, smiles = stack[-1]
227
+ if tail in edges:
228
+ children = edges[tail]
229
+ if len(children) > 1: # has side chain
230
+ child = children[-1]
231
+ stack_len = len(stack)
232
+ stack.append([child, 0, [(tail, child), child]]) # end of current chain
233
+ for child in children[-2::-1]: # start side chains
234
+ stack.append([child, stack_len, ['(', (tail, child), child]])
235
+ else: # chain grow
236
+ child = children[-1]
237
+ stack[-1][0] = child
238
+ smiles.append((tail, child))
239
+ smiles.append(child)
240
+ elif closure: # end of side chain
241
+ stack.pop()
242
+ if smiles[-2] == '(':
243
+ smiles.pop(-2)
244
+ else:
245
+ smiles.append(')')
246
+ stack[closure - 1][2].extend(smiles)
247
+ elif len(stack) > 2:
248
+ stack.pop()
249
+ stack[-1][0] = tail
250
+ stack[-1][2].extend(smiles)
251
+ elif len(stack) == 2:
252
+ stack[0][2].extend(smiles)
253
+ smiles = stack[0][2]
254
+ break
255
+ else:
256
+ break
257
+
258
+ # get order of each atom in ring closures
259
+ rings_order = {token: n for n, token in enumerate(smiles) if token in tokens}
260
+ for token in rings_order: # prepare closure numbers
261
+ released = []
262
+ # order closure atoms as in smiles string
263
+ for _, c in sorted(tokens[token], key=lambda x: rings_order[x[0]]):
264
+ if c in casted_cycles: # release ring closure number
265
+ released.append(casted_cycles[c])
266
+ else:
267
+ casted_cycles[c] = heappop(heap)
268
+ for c in released: # delayed release to avoid duplicates. e.g. C1..C11..C1 instead of C1..C12..C2
269
+ heappush(heap, c)
270
+
271
+ # prepare new neighbors order for stereo sign calculation
272
+ for token in smiles:
273
+ if token in tokens:
274
+ tokens[token].sort(key=lambda x: casted_cycles[x[1]]) # order closures
275
+ visited[token].extend(n for n, _ in tokens[token])
276
+ if token in edges:
277
+ visited[token].extend(edges[token])
278
+
279
+ for token in smiles:
280
+ if isinstance(token, int): # atoms
281
+ string.append(self._format_atom(token, visited, **kwargs))
282
+ order.append(token)
283
+ if token in tokens:
284
+ for m, c in tokens[token]:
285
+ if asymmetric_closures:
286
+ if (token, m) not in visited_bond:
287
+ string.append(self._format_bond(token, m, visited, **kwargs))
288
+ visited_bond.add((m, token))
289
+ else:
290
+ string.append(self._format_bond(token, m, visited, **kwargs))
291
+ string.append(self._format_closure(casted_cycles[c]))
292
+ elif token == '(':
293
+ string.append(open_parenthesis)
294
+ elif token == ')':
295
+ string.append(close_parenthesis)
296
+ else: # bonds
297
+ string.append(self._format_bond(*token, visited, **kwargs))
298
+
299
+ atoms_set.difference_update(visited)
300
+ if atoms_set:
301
+ string.append(delimiter)
302
+ else:
303
+ break
304
+ if _return_order:
305
+ return string, order
306
+ return string
307
+
308
+ @staticmethod
309
+ def _format_closure(c):
310
+ return str(c) if c < 10 else f'%{c}'
311
+
312
+ @abstractmethod
313
+ def _format_atom(self, n, adjacency, **kwargs):
314
+ ...
315
+
316
+ @abstractmethod
317
+ def _format_bond(self, n, m, adjacency, **kwargs):
318
+ ...
319
+
320
+ @abstractmethod
321
+ def _smiles_order(self, stereo=True) -> Callable:
322
+ ...
323
+
324
+ def _format_cxsmiles(self, order) -> str | None:
325
+ ...
326
+
327
+
328
+ class MoleculeSmiles(Smiles):
329
+ __slots__ = ()
330
+
331
+ def sticky_smiles(self, left: int = None, right: int = None, *,
332
+ remove_left: bool = False, remove_right: bool = False, tries: int = 10,
333
+ keep_bond_left: bool = False, keep_bond_right: bool = False, hydrogens: bool = False) -> str:
334
+ """
335
+ Generate smiles with fixed left and/or right terminal atoms.
336
+ The right atom must be terminal if set. Use a temporary attached atom with remove_right=True as a workaround.
337
+ Removing in-ring terminal atoms can lead to invalid smiles.
338
+
339
+ :param remove_left: drop terminal atom and corresponding bond by default
340
+ :param remove_right: drop terminal atom and corresponding bond by default
341
+ :param tries: number of attempts to generate smiles
342
+ :param keep_bond_left: keep bond of removed left atom. Sets explicit single bond
343
+ :param keep_bond_right: keep bond of removed right atom
344
+ :param hydrogens: show implicit hydrogens in smiles
345
+ """
346
+ bonds = self._bonds
347
+ if right:
348
+ assert tries > 0, 'tries count should be positive'
349
+ assert len(bonds[right]) == 1, 'right atom should be terminal'
350
+ assert left != right, 'left and right atoms the same'
351
+ assert self.connected_components_count == 1, 'only single component structures supported'
352
+ if remove_left: assert left, 'specify left atom to remove'
353
+
354
+ seen = {right: 0}
355
+ queue = [(right, -10)]
356
+ while queue:
357
+ n, d = queue.pop(0)
358
+ for m in bonds[n].keys() - seen.keys():
359
+ queue.append((m, d - 10))
360
+ seen[m] = d
361
+ if left:
362
+ bonds[left] # noqa. check left atom availability
363
+ seen[left] = -1_000_000_000 # prioritize left atom
364
+
365
+ for _ in range(tries):
366
+ smiles, order = self._smiles(lambda x: seen[x] + random(), _return_order=True,
367
+ random=True, hydrogens=hydrogens)
368
+ if order[-1] == right:
369
+ break
370
+ else:
371
+ raise Exception('generation of smiles failed')
372
+ if remove_left:
373
+ if keep_bond_left:
374
+ smiles = smiles[1:]
375
+ if not smiles[0]:
376
+ smiles[0] = '-'
377
+ else:
378
+ smiles = smiles[2:]
379
+ if remove_right:
380
+ if keep_bond_right:
381
+ smiles = smiles[:-1]
382
+ if not smiles[-1]:
383
+ smiles[-1] = '-'
384
+ else:
385
+ smiles = smiles[:-2]
386
+ elif left:
387
+ bonds[left] # noqa. check left atom availability
388
+ smiles = self._smiles(lambda x: x != left, random=True, hydrogens=hydrogens)
389
+ if remove_left:
390
+ if keep_bond_left:
391
+ smiles = smiles[1:]
392
+ if not smiles[0]:
393
+ smiles[0] = '-' # assuming nobody will try to remove atom from an aromatic ring
394
+ else:
395
+ smiles = smiles[2:]
396
+ else:
397
+ raise ValueError('either left or right atom should be specified')
398
+ return ''.join(smiles)
399
+
400
+ def _smiles_order(self, stereo=True):
401
+ if stereo:
402
+ return self._chiral_morgan.__getitem__
403
+ else:
404
+ return self.atoms_order.__getitem__
405
+
406
+ def _format_cxsmiles(self, order):
407
+ cx = []
408
+ rd = []
409
+ es = defaultdict(list)
410
+
411
+ atoms = self._atoms
412
+ for i, n in enumerate(order):
413
+ a = atoms[n]
414
+ if a.is_radical:
415
+ rd.append(str(i))
416
+ if (s := a.extended_stereo) is not None:
417
+ if s < 0:
418
+ es[f'o{-s}:'].append(str(i))
419
+ elif s > 0:
420
+ es[f'&{s}:'].append(str(i))
421
+
422
+ if rd:
423
+ cx.append('^1:' + ','.join(rd))
424
+ if es:
425
+ for k in sorted(es):
426
+ cx.append(k + ','.join(es[k]))
427
+ if cx:
428
+ return '|' + ','.join(cx) + '|'
429
+ return None
430
+
431
+ def _format_atom(self, n, adjacency, **kwargs):
432
+ atom = self._atoms[n]
433
+
434
+ smi = ['', # [
435
+ str(atom.isotope) if atom.isotope else '', # isotope
436
+ None,
437
+ '', # stereo
438
+ '', # hydrogen
439
+ '', # charge
440
+ f':{n}' if kwargs.get('mapping', False) else '', # mapping
441
+ ''] # ]
442
+
443
+ if atom.stereo is not None and kwargs.get('stereo', True):
444
+ # allene
445
+ if n in self._stereo_allenes_terminals:
446
+ t1, t2 = self._stereo_allenes_terminals[n]
447
+ env = self.stereogenic_allenes[n]
448
+ n1 = next(x for x in adjacency[t1] if x in env)
449
+ n2 = next(x for x in adjacency[t2] if x in env)
450
+ smi[3] = '@' if self._translate_allene_sign(n, n1, n2) else '@@'
451
+ # tetrahedron
452
+ elif atom.implicit_hydrogens and next(x for x in adjacency) == n:
453
+ # first atom in smiles has reversed chiral mark
454
+ smi[3] = '@@' if self._translate_tetrahedron_sign(n, adjacency[n]) else '@'
455
+ else:
456
+ smi[3] = '@' if self._translate_tetrahedron_sign(n, adjacency[n]) else '@@'
457
+
458
+ if atom.charge and kwargs.get('charges', True):
459
+ smi[5] = charge_str[atom.charge]
460
+
461
+ if any(smi) or atom.atomic_symbol not in organic_set or atom.is_radical or kwargs.get('hydrogens', False):
462
+ smi[0] = '['
463
+ smi[-1] = ']'
464
+ if atom.implicit_hydrogens == 1:
465
+ smi[4] = 'H'
466
+ elif atom.implicit_hydrogens:
467
+ smi[4] = f'H{atom.implicit_hydrogens}'
468
+ elif atom.hybridization == 4 and atom.implicit_hydrogens and atom in (B, N, P): # pyrrole
469
+ smi[0] = '['
470
+ smi[-1] = ']'
471
+ if atom.implicit_hydrogens == 1:
472
+ smi[4] = 'H'
473
+ else:
474
+ smi[4] = f'H{atom.implicit_hydrogens}'
475
+ elif not atom.implicit_hydrogens and atom in (B, C, P, S) and not self.not_special_connectivity[n]:
476
+ # elemental B, C, P, S
477
+ smi[0] = '['
478
+ smi[-1] = ']'
479
+ elif atom.implicit_hydrogens and atom == P and atom.hybridization != 1:
480
+ smi[0] = '['
481
+ smi[-1] = ']'
482
+ if atom.implicit_hydrogens == 1:
483
+ smi[4] = 'H'
484
+ else:
485
+ smi[4] = f'H{atom.implicit_hydrogens}'
486
+
487
+ if kwargs.get('aromatic', True) and atom.hybridization == 4:
488
+ smi[2] = atom.atomic_symbol.lower()
489
+ else:
490
+ smi[2] = atom.atomic_symbol
491
+ return ''.join(smi)
492
+
493
+ def _format_bond(self, n, m, adjacency, **kwargs):
494
+ if not kwargs.get('bonds', True):
495
+ return ''
496
+ bond = self._bonds[n][m]
497
+ if bond == 4:
498
+ if kwargs.get('aromatic', True):
499
+ return ''
500
+ return ':'
501
+ elif bond == 1: # cis-trans /\
502
+ if kwargs.get('aromatic', True) and self._atoms[n].hybridization == self._atoms[m].hybridization == 4:
503
+ return '-'
504
+ if kwargs.get('stereo', True):
505
+ if 'cache' in adjacency:
506
+ ct_map = adjacency['cache']
507
+ else:
508
+ ct_map = adjacency['cache'] = self.__ct_map(adjacency)
509
+
510
+ if (x := ct_map.get((n, m))) is not None:
511
+ return '/' if x else '\\'
512
+ return ''
513
+ elif bond == 2:
514
+ return '='
515
+ elif bond == 3:
516
+ return '#'
517
+ else: # bond == 8
518
+ return '~'
519
+
520
+ def __ct_map(self, adjacency):
521
+ stereo_bonds = {n for n, mb in self._bonds.items() if any(b.stereo is not None for m, b in mb.items())}
522
+ if not stereo_bonds:
523
+ return {}
524
+ ct_map = {}
525
+ sct = self.stereogenic_cis_trans
526
+ ctc = self._stereo_cis_trans_centers
527
+ ctt = self._stereo_cis_trans_terminals
528
+ ctcp = self._stereo_cis_trans_counterpart
529
+
530
+ seen = set()
531
+ for k, vs in adjacency.items():
532
+ seen.add(k)
533
+ if (cs := ctc.get(k)) and stereo_bonds.issuperset(cs):
534
+ env = sct[ctt[k]]
535
+ for v in vs:
536
+ if v in env:
537
+ if (k, v) in ct_map:
538
+ continue
539
+ elif x := ct_map.get(k): # second substituent of C=
540
+ s = ct_map[(k, x)]
541
+ ct_map[(k, v)] = not s # X/C(/R)=, C(\X)(/R)=, C(=C(\X)/R)=C=
542
+ ct_map[(v, k)] = s
543
+ if y := ctc.get(v): # =C(\X)/R=, C(\X)(/R=)=
544
+ ct_map[v] = k
545
+ seen.add(y)
546
+ elif cs in seen:
547
+ o = ctcp[k]
548
+ on = ct_map[o]
549
+ s = ct_map[(o, on)]
550
+ if not self._translate_cis_trans_sign(k, o, v, on):
551
+ s = not s
552
+ ct_map[(k, v)] = s
553
+ ct_map[k] = v
554
+ ct_map[(v, k)] = not s # C/R=, R\1...C/1
555
+ if y := ctc.get(v):
556
+ ct_map[v] = k
557
+ seen.add(y)
558
+ else: # left entry to double bond
559
+ if y := ctc.get(v): # 1,3-diene case
560
+ ct_map[v] = k
561
+ seen.add(y)
562
+ ct_map[(v, k)] = True # R/C=, C\1=...R/1, C(/R=)=, C(=C(/R=))=C=
563
+ ct_map[(k, v)] = False # first DOWN
564
+ ct_map[k] = v
565
+ seen.add(cs)
566
+ return ct_map
567
+
568
+
569
+ class CGRSmiles(Smiles):
570
+ __slots__ = ()
571
+
572
+ def _smiles_order(self, stereo=True):
573
+ return self.atoms_order.__getitem__
574
+
575
+ def _format_atom(self, n, adjacency, **kwargs):
576
+ atom = self._atoms[n]
577
+ if atom.isotope:
578
+ smi = [str(atom.isotope), atom.atomic_symbol]
579
+ else:
580
+ smi = [atom.atomic_symbol]
581
+
582
+ if atom.charge or atom.p_charge:
583
+ smi.append(dyn_charge_str[(atom.charge, atom.p_charge)])
584
+ if atom.is_radical or atom.p_is_radical:
585
+ smi.append(dyn_radical_str[(atom.is_radical, atom.p_is_radical)])
586
+
587
+ if len(smi) != 1 or atom.atomic_symbol not in organic_set:
588
+ smi.insert(0, '[')
589
+ smi.append(']')
590
+ return ''.join(smi)
591
+
592
+ def _format_bond(self, n, m, adjacency, **kwargs):
593
+ bond = self._bonds[n][m]
594
+ return dyn_order_str[(bond.order, bond.p_order)]
595
+
596
+
597
+ __all__ = ['MoleculeSmiles', 'CGRSmiles']
@@ -0,0 +1,30 @@
1
+ # -*- coding: utf-8 -*-
2
+ #
3
+ # Copyright 2021-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 .molecule import *
20
+ from .reaction import *
21
+ from .resonance import *
22
+ from .salts import *
23
+ from .saturation import *
24
+
25
+
26
+ class StandardizeMolecule(Standardize, Resonance, Saturation, Salts):
27
+ __slots__ = ()
28
+
29
+
30
+ __all__ = ['StandardizeMolecule', 'StandardizeReaction']