chython 2.4__cp312-cp312-win_amd64.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (171) hide show
  1. chython/__init__.py +31 -0
  2. chython/_functions.py +69 -0
  3. chython/algorithms/__init__.py +18 -0
  4. chython/algorithms/_isomorphism.cp312-win_amd64.pyd +0 -0
  5. chython/algorithms/_isomorphism.pyx +189 -0
  6. chython/algorithms/aromatics/__init__.py +27 -0
  7. chython/algorithms/aromatics/_rules.py +124 -0
  8. chython/algorithms/aromatics/kekule.py +530 -0
  9. chython/algorithms/aromatics/test/__init__.py +18 -0
  10. chython/algorithms/aromatics/test/test_kekule.py +160 -0
  11. chython/algorithms/aromatics/test/test_thiele.py +263 -0
  12. chython/algorithms/aromatics/thiele.py +226 -0
  13. chython/algorithms/calculate2d/__init__.py +23 -0
  14. chython/algorithms/calculate2d/clean2d.js +1 -0
  15. chython/algorithms/calculate2d/molecule.py +149 -0
  16. chython/algorithms/calculate2d/reaction.py +80 -0
  17. chython/algorithms/depict.py +523 -0
  18. chython/algorithms/fingerprints/__init__.py +48 -0
  19. chython/algorithms/fingerprints/linear.py +208 -0
  20. chython/algorithms/fingerprints/morgan.py +137 -0
  21. chython/algorithms/fingerprints/test/__init__.py +18 -0
  22. chython/algorithms/fingerprints/test/test_linear.py +127 -0
  23. chython/algorithms/fingerprints/test/test_morgan.py +79 -0
  24. chython/algorithms/isomorphism.py +667 -0
  25. chython/algorithms/mapping/__init__.py +28 -0
  26. chython/algorithms/mapping/_groups.py +94 -0
  27. chython/algorithms/mapping/_reactions.py +72 -0
  28. chython/algorithms/mapping/attention.py +216 -0
  29. chython/algorithms/mapping/fixmapper.py +84 -0
  30. chython/algorithms/mapping/groups.py +137 -0
  31. chython/algorithms/mcs.py +202 -0
  32. chython/algorithms/morgan.py +82 -0
  33. chython/algorithms/rings.py +551 -0
  34. chython/algorithms/smiles.py +556 -0
  35. chython/algorithms/standardize/__init__.py +30 -0
  36. chython/algorithms/standardize/_charged.py +115 -0
  37. chython/algorithms/standardize/_groups.py +926 -0
  38. chython/algorithms/standardize/_metal_organics.py +189 -0
  39. chython/algorithms/standardize/_reagents.py +44 -0
  40. chython/algorithms/standardize/_salts.py +68 -0
  41. chython/algorithms/standardize/molecule.py +479 -0
  42. chython/algorithms/standardize/reaction.py +429 -0
  43. chython/algorithms/standardize/resonance.py +196 -0
  44. chython/algorithms/standardize/salts.py +128 -0
  45. chython/algorithms/standardize/saturation.py +378 -0
  46. chython/algorithms/standardize/test/__init__.py +18 -0
  47. chython/algorithms/standardize/test/test_groups.py +118 -0
  48. chython/algorithms/stereo.py +1222 -0
  49. chython/algorithms/tautomers/__init__.py +171 -0
  50. chython/algorithms/tautomers/_acid.py +59 -0
  51. chython/algorithms/tautomers/_base.py +116 -0
  52. chython/algorithms/tautomers/_keto_enol.py +107 -0
  53. chython/algorithms/tautomers/acid_base.py +214 -0
  54. chython/algorithms/tautomers/heteroarenes.py +108 -0
  55. chython/algorithms/tautomers/keto_enol.py +148 -0
  56. chython/algorithms/tautomers/test/__init__.py +18 -0
  57. chython/algorithms/tautomers/test/test_tautomers.py +80 -0
  58. chython/algorithms/test/__init__.py +18 -0
  59. chython/algorithms/test/test_isomorphism.py +58 -0
  60. chython/algorithms/test/test_smiles.py +76 -0
  61. chython/algorithms/x3dom.py +355 -0
  62. chython/containers/__init__.py +43 -0
  63. chython/containers/_pack_v2.cp312-win_amd64.pyd +0 -0
  64. chython/containers/_pack_v2.pyx +271 -0
  65. chython/containers/_unpack_v0v2.cp312-win_amd64.pyd +0 -0
  66. chython/containers/_unpack_v0v2.pyx +300 -0
  67. chython/containers/bonds.py +255 -0
  68. chython/containers/cgr.py +115 -0
  69. chython/containers/graph.py +200 -0
  70. chython/containers/molecule.py +695 -0
  71. chython/containers/query.py +66 -0
  72. chython/containers/reaction.py +324 -0
  73. chython/exceptions.py +114 -0
  74. chython/files/MRVrw.py +523 -0
  75. chython/files/PDBrw.py +257 -0
  76. chython/files/RDFrw.py +298 -0
  77. chython/files/SDFrw.py +221 -0
  78. chython/files/__init__.py +29 -0
  79. chython/files/_convert.py +207 -0
  80. chython/files/_mapping.py +117 -0
  81. chython/files/_xyz.cp312-win_amd64.pyd +0 -0
  82. chython/files/_xyz.pyx +74 -0
  83. chython/files/daylight/__init__.py +24 -0
  84. chython/files/daylight/parser.py +157 -0
  85. chython/files/daylight/smarts.py +107 -0
  86. chython/files/daylight/smiles.py +245 -0
  87. chython/files/daylight/test/__init__.py +18 -0
  88. chython/files/daylight/test/test_daylight_smarts.py +289 -0
  89. chython/files/daylight/test/test_daylight_smiles.py +101 -0
  90. chython/files/daylight/test/test_parser.py +118 -0
  91. chython/files/daylight/test/test_tokenize.py +77 -0
  92. chython/files/daylight/tokenize.py +376 -0
  93. chython/files/libinchi/__init__.py +22 -0
  94. chython/files/libinchi/libinchi.dll +0 -0
  95. chython/files/libinchi/wrapper.py +555 -0
  96. chython/files/mdl/__init__.py +25 -0
  97. chython/files/mdl/emol.py +209 -0
  98. chython/files/mdl/erxn.py +67 -0
  99. chython/files/mdl/mol.py +144 -0
  100. chython/files/mdl/read.py +225 -0
  101. chython/files/mdl/rxn.py +67 -0
  102. chython/files/mdl/stereo.py +63 -0
  103. chython/files/mdl/write.py +165 -0
  104. chython/files/xyz.py +78 -0
  105. chython/periodictable/__init__.py +69 -0
  106. chython/periodictable/base/__init__.py +25 -0
  107. chython/periodictable/base/dynamic.py +149 -0
  108. chython/periodictable/base/element.py +452 -0
  109. chython/periodictable/base/groups.py +90 -0
  110. chython/periodictable/base/periods.py +46 -0
  111. chython/periodictable/base/query.py +473 -0
  112. chython/periodictable/base/vector.py +119 -0
  113. chython/periodictable/groupI.py +252 -0
  114. chython/periodictable/groupII.py +223 -0
  115. chython/periodictable/groupIII.py +1154 -0
  116. chython/periodictable/groupIV.py +210 -0
  117. chython/periodictable/groupIX.py +196 -0
  118. chython/periodictable/groupV.py +192 -0
  119. chython/periodictable/groupVI.py +183 -0
  120. chython/periodictable/groupVII.py +162 -0
  121. chython/periodictable/groupVIII.py +161 -0
  122. chython/periodictable/groupX.py +166 -0
  123. chython/periodictable/groupXI.py +164 -0
  124. chython/periodictable/groupXII.py +158 -0
  125. chython/periodictable/groupXIII.py +239 -0
  126. chython/periodictable/groupXIV.py +262 -0
  127. chython/periodictable/groupXV.py +276 -0
  128. chython/periodictable/groupXVI.py +457 -0
  129. chython/periodictable/groupXVII.py +324 -0
  130. chython/periodictable/groupXVIII.py +260 -0
  131. chython/reactor/__init__.py +24 -0
  132. chython/reactor/base.py +212 -0
  133. chython/reactor/deprotection.py +565 -0
  134. chython/reactor/groups.py +128 -0
  135. chython/reactor/reactions/__init__.py +157 -0
  136. chython/reactor/reactions/_amidation.py +58 -0
  137. chython/reactor/reactions/_amine_isocyanate.py +52 -0
  138. chython/reactor/reactions/_buchwald_hartwig.py +50 -0
  139. chython/reactor/reactions/_esterification.py +49 -0
  140. chython/reactor/reactions/_macmillan.py +43 -0
  141. chython/reactor/reactions/_reductive_amination.py +50 -0
  142. chython/reactor/reactions/_sonogashira.py +49 -0
  143. chython/reactor/reactions/_sulfonamidation.py +52 -0
  144. chython/reactor/reactions/_suzuki_miyaura.py +88 -0
  145. chython/reactor/reactions/ufe.py +99 -0
  146. chython/reactor/reactor.py +170 -0
  147. chython/reactor/retro/__init__.py +77 -0
  148. chython/reactor/retro/_amidation.py +39 -0
  149. chython/reactor/retro/_aryl_amination.py +32 -0
  150. chython/reactor/retro/_mitsunobu.py +49 -0
  151. chython/reactor/retro/_sonogashira.py +49 -0
  152. chython/reactor/retro/_suzuki_miyaura.py +54 -0
  153. chython/reactor/scaffold.py +122 -0
  154. chython/reactor/test/__init__.py +18 -0
  155. chython/reactor/test/test_deprotection.py +57 -0
  156. chython/reactor/test/test_reactor.py +40 -0
  157. chython/reactor/test/test_scaffold.py +57 -0
  158. chython/reactor/test/test_transformer.py +40 -0
  159. chython/reactor/transformer.py +62 -0
  160. chython/utils/__init__.py +57 -0
  161. chython/utils/free_wilson.py +114 -0
  162. chython/utils/grid.py +130 -0
  163. chython/utils/rdkit.py +165 -0
  164. chython/utils/retro.py +139 -0
  165. chython/utils/svg.py +57 -0
  166. chython/utils/test/__init__.py +18 -0
  167. chython/utils/test/test_rdkit.py +71 -0
  168. chython-2.4.dist-info/LICENSE +165 -0
  169. chython-2.4.dist-info/METADATA +101 -0
  170. chython-2.4.dist-info/RECORD +171 -0
  171. chython-2.4.dist-info/WHEEL +4 -0
@@ -0,0 +1,530 @@
1
+ # -*- coding: utf-8 -*-
2
+ #
3
+ # Copyright 2021-2024 Ramil Nugmanov <nougmanoff@protonmail.com>
4
+ # This file is part of chython.
5
+ #
6
+ # chython is free software; you can redistribute it and/or modify
7
+ # it under the terms of the GNU Lesser General Public License as published by
8
+ # the Free Software Foundation; either version 3 of the License, or
9
+ # (at your option) any later version.
10
+ #
11
+ # This program is distributed in the hope that it will be useful,
12
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
13
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
+ # GNU Lesser General Public License for more details.
15
+ #
16
+ # You should have received a copy of the GNU Lesser General Public License
17
+ # along with this program; if not, see <https://www.gnu.org/licenses/>.
18
+ #
19
+ from collections import defaultdict, deque
20
+ from typing import List, Optional, Tuple, TYPE_CHECKING, Union
21
+ from ._rules import rules
22
+ from ..._functions import lazy_product
23
+ from ...exceptions import InvalidAromaticRing
24
+
25
+
26
+ if TYPE_CHECKING:
27
+ from chython import MoleculeContainer
28
+
29
+
30
+ # atomic number constants
31
+ B = 5
32
+ C = 6
33
+ N = 7
34
+ O = 8
35
+ P = 15
36
+ S = 16
37
+ As = 33
38
+ Se = 34
39
+ Te = 52
40
+
41
+
42
+ class Kekule:
43
+ __slots__ = ()
44
+
45
+ def kekule(self: Union['Kekule', 'MoleculeContainer'], *, buffer_size=7) -> bool:
46
+ """
47
+ Convert structure to kekule form. Return True if found any aromatic ring. Set implicit hydrogen count and
48
+ hybridization marks on atoms.
49
+
50
+ Only one of possible double/single bonds positions will be set.
51
+ For enumerate bonds positions use `enumerate_kekule`.
52
+
53
+ :param buffer_size: number of attempts of pyridine form searching.
54
+ """
55
+ fixed = self.__fix_rings() # fix bad aromatic rings
56
+ kekule = next(self.__kekule_full(buffer_size), None)
57
+ if kekule:
58
+ bonds = self._bonds
59
+ atoms = set()
60
+ for n, m, b in kekule:
61
+ bonds[n][m]._order = b
62
+ atoms.add(n)
63
+ atoms.add(m)
64
+ for n in atoms:
65
+ self.calc_implicit(n)
66
+ self.flush_cache(keep_sssr=True, keep_components=True)
67
+ self.calc_labels()
68
+ return True
69
+ return fixed
70
+
71
+ def enumerate_kekule(self: Union['Kekule', 'MoleculeContainer']):
72
+ """
73
+ Enumerate all possible kekule forms of molecule.
74
+ """
75
+ self.__fix_rings() # fix bad aromatic rings
76
+ for form in self.__kekule_full(0):
77
+ copy = self.copy(keep_sssr=True, keep_components=True)
78
+ bonds = copy._bonds
79
+ atoms = set()
80
+ for n, m, b in form:
81
+ bonds[n][m]._order = b
82
+ atoms.add(n)
83
+ atoms.add(m)
84
+ for n in atoms:
85
+ copy.calc_implicit(n)
86
+ copy.calc_labels()
87
+ yield copy
88
+
89
+ def __fix_rings(self: 'MoleculeContainer'):
90
+ atoms = self._atoms
91
+ bonds = self._bonds
92
+ seen = set()
93
+ keep = True
94
+ for q, af, bf, mm in rules:
95
+ for mapping in q.get_mapping(self, automorphism_filter=False):
96
+ match = set(mapping.values())
97
+ if not mm and not match.isdisjoint(seen): # prevent double patching of atoms
98
+ continue
99
+ seen.update(match)
100
+
101
+ for n, c in af.items():
102
+ n = mapping[n]
103
+ atoms[n]._charge = c
104
+ for n, m, b in bf:
105
+ n = mapping[n]
106
+ m = mapping[m]
107
+ bonds[n][m]._order = b
108
+ if b == 8:
109
+ # flush sssr and components cache
110
+ keep = False
111
+ if seen:
112
+ self.flush_cache(keep_sssr=keep, keep_components=keep)
113
+ self.calc_labels()
114
+ return True
115
+ return False
116
+
117
+ def __prepare_rings(self: 'MoleculeContainer'):
118
+ atoms = self._atoms
119
+ bonds = self._bonds
120
+
121
+ rings = defaultdict(list) # aromatic skeleton
122
+ pyrroles = set()
123
+
124
+ double_bonded = defaultdict(list)
125
+ triple_bonded = set()
126
+ for n, m_bond in bonds.items():
127
+ for m, bond in m_bond.items():
128
+ if bond == 4:
129
+ rings[n].append(m)
130
+ elif bond == 2:
131
+ double_bonded[n].append(m)
132
+ elif bond == 3:
133
+ triple_bonded.add(n)
134
+
135
+ if not rings:
136
+ return rings, pyrroles, set()
137
+ elif not triple_bonded.isdisjoint(rings):
138
+ raise InvalidAromaticRing('triple bonds connected to rings')
139
+
140
+ copy_rings = {n: ms.copy() for n, ms in rings.items()}
141
+ for r in self.sssr:
142
+ if set(r).issubset(rings):
143
+ n, *_, m = r
144
+ if n not in rings[m]: # fix invalid structures: c1ccc-cc1
145
+ # remove inner ring double bonds: c1ccc=cc1
146
+ if n in double_bonded and m in double_bonded and m in double_bonded[n]:
147
+ double_bonded[n].remove(m)
148
+ double_bonded[m].remove(n)
149
+ rings[m].append(n)
150
+ rings[n].append(m)
151
+ elif m in copy_rings[n]:
152
+ copy_rings[n].remove(m)
153
+ copy_rings[m].remove(n)
154
+ for n, m in zip(r, r[1:]):
155
+ if n not in rings[m]:
156
+ if n in double_bonded and m in double_bonded and m in double_bonded[n]:
157
+ double_bonded[n].remove(m)
158
+ double_bonded[m].remove(n)
159
+ rings[m].append(n)
160
+ rings[n].append(m)
161
+ elif m in copy_rings[n]:
162
+ copy_rings[n].remove(m)
163
+ copy_rings[m].remove(n)
164
+
165
+ # fix invalid smiles: c1ccccc1c2ccccc2 instead of c1ccccc1-c2ccccc2
166
+ seen = set()
167
+ for n, ms in copy_rings.items():
168
+ if ms:
169
+ seen.add(n)
170
+ for m in ms:
171
+ if m not in seen:
172
+ rings[n].remove(m)
173
+ rings[m].remove(n)
174
+ bonds[n][m]._order = 1
175
+
176
+ if any(len(ms) not in (2, 3) for ms in rings.values()):
177
+ raise InvalidAromaticRing('not in ring aromatic bond or hypercondensed rings: '
178
+ f'{{{", ".join(str(n) for n, ms in rings.items() if len(ms) not in (2, 3))}}}')
179
+
180
+ # get double bonded ring atoms
181
+ double_bonded = {n for n, ms in double_bonded.items() if ms and n in rings}
182
+ if any(len(rings[n]) != 2 for n in double_bonded): # double bonded never condensed
183
+ raise InvalidAromaticRing('quinone valence error')
184
+ for n in double_bonded:
185
+ if (atom := atoms[n]) == N:
186
+ if atom.charge != 1:
187
+ raise InvalidAromaticRing('quinone should be charged N atom')
188
+ elif atom not in (C, P, S, As, Se, Te) or atom.charge:
189
+ raise InvalidAromaticRing('quinone should be neutral S, Se, Te, C, P, As atom')
190
+
191
+ for n in rings:
192
+ if (atom := atoms[n]) == C: # carbon
193
+ if atom.charge == 0:
194
+ if atom.neighbors not in (2, 3):
195
+ raise InvalidAromaticRing
196
+ elif atom.charge in (-1, 1):
197
+ if atom.is_radical:
198
+ if atom.neighbors == 2:
199
+ double_bonded.add(n)
200
+ else:
201
+ raise InvalidAromaticRing
202
+ elif atom.neighbors == 3:
203
+ double_bonded.add(n)
204
+ elif atom.neighbors == 2: # benzene (an|cat)ion or pyrrole
205
+ pyrroles.add(n)
206
+ else:
207
+ raise InvalidAromaticRing
208
+ else:
209
+ raise InvalidAromaticRing
210
+ elif atom in (N, P, As):
211
+ if atom.charge == 0: # pyrrole or pyridine. include radical pyrrole
212
+ if atom.is_radical:
213
+ if atom.neighbors != 2: # only pyrrole radical
214
+ raise InvalidAromaticRing
215
+ double_bonded.add(n)
216
+ elif atom.neighbors == 3:
217
+ if atom == N: # pyrrole only possible
218
+ double_bonded.add(n)
219
+ else: # P(III) or P(V)H
220
+ pyrroles.add(n)
221
+ elif atom.neighbors == 2:
222
+ if atom.implicit_hydrogens is None: # pyrrole or pyridine
223
+ pyrroles.add(n)
224
+ elif atom.implicit_hydrogens == 1: # only pyrrole
225
+ double_bonded.add(n)
226
+ elif atom.implicit_hydrogens: # too many hydrogens for aromatic rings
227
+ raise InvalidAromaticRing
228
+ elif atom.neighbors != 4 or atom not in (P, As): # P(V) in ring [P;a](-R1)-R2
229
+ raise InvalidAromaticRing
230
+ elif atom.charge == -1: # pyrrole only
231
+ if atom.neighbors != 2 or atom.is_radical:
232
+ raise InvalidAromaticRing
233
+ double_bonded.add(n)
234
+ elif atom.charge != 1:
235
+ raise InvalidAromaticRing
236
+ elif atom.is_radical:
237
+ if atom.neighbors != 2: # not cation-radical pyridine
238
+ raise InvalidAromaticRing
239
+ elif atom.neighbors == 2: # pyrrole cation or protonated pyridine
240
+ pyrroles.add(n)
241
+ elif atom.neighbors != 3: # not pyridine oxyde
242
+ raise InvalidAromaticRing
243
+ elif atom == O: # furan
244
+ if atom.neighbors == 2:
245
+ if atom.charge == 0:
246
+ if atom.is_radical:
247
+ raise InvalidAromaticRing('radical oxygen')
248
+ double_bonded.add(n)
249
+ elif atom.charge == 1:
250
+ if atom.is_radical: # furan cation-radical
251
+ double_bonded.add(n)
252
+ # pyrylium
253
+ else:
254
+ raise InvalidAromaticRing('invalid oxygen charge')
255
+ else:
256
+ raise InvalidAromaticRing('Triple-bonded oxygen')
257
+ elif atom in (S, Se, Te): # thiophene
258
+ if n not in double_bonded: # not sulphoxyde nor sulphone
259
+ if atom.neighbors == 2:
260
+ if atom.is_radical:
261
+ if atom.charge == 1:
262
+ double_bonded.add(n)
263
+ else:
264
+ raise InvalidAromaticRing('S, Se, Te cation-radical expected')
265
+ if atom.charge == 0:
266
+ double_bonded.add(n)
267
+ elif atom.charge != 1:
268
+ raise InvalidAromaticRing('S, Se, Te cation in benzene like ring expected')
269
+ elif atom.neighbors == 3:
270
+ if atom.is_radical:
271
+ if atom.charge:
272
+ raise InvalidAromaticRing('S, Se, Te ion-radical ring')
273
+ double_bonded.add(n)
274
+ elif atom.charge == 1:
275
+ double_bonded.add(n)
276
+ elif atom.charge:
277
+ raise InvalidAromaticRing('S, Se, Te invalid charge ring')
278
+ else:
279
+ raise InvalidAromaticRing('S, Se, Te hypervalent ring')
280
+ elif atom == B:
281
+ if atom.charge == 0:
282
+ if atom.neighbors == 2:
283
+ if atom.is_radical: # C=1O[B]OC=1
284
+ double_bonded.add(n)
285
+ else:
286
+ if atom.implicit_hydrogens is None: # b1ccccc1, C=1OBOC=1 or B1C=CC=N1
287
+ pyrroles.add(n)
288
+ elif atom.implicit_hydrogens == 1: # C=1O[BH]OC=1 or [BH]1C=CC=N1
289
+ double_bonded.add(n)
290
+ elif atom.implicit_hydrogens:
291
+ raise InvalidAromaticRing
292
+ elif not atom.is_radical:
293
+ double_bonded.add(n)
294
+ else:
295
+ raise InvalidAromaticRing
296
+ elif atom.charge == 1:
297
+ if atom.neighbors == 2 and not atom.is_radical:
298
+ double_bonded.add(n)
299
+ else:
300
+ raise InvalidAromaticRing
301
+ elif atom.charge == -1:
302
+ if atom.neighbors == 2:
303
+ if not atom.is_radical: # C=1O[B-]OC=1 or [bH-]1ccccc1
304
+ pyrroles.add(n)
305
+ # anion-radical is benzene like
306
+ elif atom.is_radical: # C=1O[B-*](R)OC=1
307
+ double_bonded.add(n)
308
+ else:
309
+ pyrroles.add(n)
310
+ else:
311
+ raise InvalidAromaticRing
312
+ else:
313
+ raise InvalidAromaticRing(f'only B, C, N, P, O, S, Se, Te possible, not: {atoms[n].atomic_symbol}')
314
+ return rings, pyrroles, double_bonded
315
+
316
+ def __kekule_full(self, buffer_size):
317
+ rings, pyrroles, double_bonded = self.__prepare_rings()
318
+ atoms = set(rings)
319
+ components = []
320
+ while atoms:
321
+ start = atoms.pop()
322
+ component = {start: rings[start]}
323
+ queue = deque([start])
324
+ while queue:
325
+ current = queue.popleft()
326
+ for n in rings[current]:
327
+ if n not in component:
328
+ queue.append(n)
329
+ component[n] = rings[n]
330
+
331
+ components.append(component)
332
+ atoms.difference_update(component)
333
+
334
+ for keks in lazy_product(*(_kekule_component(c, double_bonded & c.keys(), pyrroles & c.keys(), buffer_size)
335
+ for c in components)):
336
+ yield [x for x in keks for x in x]
337
+
338
+
339
+ def _kekule_component(rings, double_bonded, pyrroles, buffer_size):
340
+ # (current atom, previous atom, bond between cp atoms, path deep for cutting [None if cut impossible])
341
+ stack: List[List[Tuple[int, int, int, Optional[int]]]]
342
+ if double_bonded: # start from double bonded if exists
343
+ start = next(iter(double_bonded))
344
+ stack = [[(next(iter(rings[start])), start, 1, 0)]]
345
+ else: # select not pyrrole not condensed atom
346
+ try:
347
+ start = next(n for n, ms in rings.items() if len(ms) == 2 and n not in pyrroles)
348
+ except StopIteration: # all pyrroles. select not condensed atom.
349
+ try:
350
+ start = next(n for n, ms in rings.items() if len(ms) == 2)
351
+ except StopIteration: # fullerene?
352
+ start = next(iter(rings))
353
+ double_bonded.add(start)
354
+ stack = [[(next_atom, start, 2, 0)] for next_atom in rings[start]]
355
+ else:
356
+ stack = [[(next_atom, start, 1, 0)] for next_atom in rings[start]]
357
+ else:
358
+ stack = [[(next_atom, start, 1, 0)] for next_atom in rings[start]]
359
+
360
+ size = sum(len(x) for x in rings.values()) // 2
361
+ path = []
362
+ hashed_path = set()
363
+ nether_yielded = True
364
+ buffer = []
365
+
366
+ while stack:
367
+ atom, prev_atom, bond, _ = stack[-1].pop()
368
+ path.append((atom, prev_atom, bond))
369
+ hashed_path.add(atom)
370
+
371
+ if len(path) == size:
372
+ if nether_yielded:
373
+ nether_yielded = False
374
+ if pyrroles and buffer_size: # prioritize pyridine over pyrrole
375
+ g = defaultdict(int)
376
+ for n, m, b in path:
377
+ g[n] += b
378
+ g[m] += b
379
+ # should be pairs of pyrrole atoms
380
+ if sum(b == 2 and n in pyrroles for n, b in g.items()) >= 2:
381
+ if len(buffer) == buffer_size: # optimization. try only few times to prevent freezes.
382
+ buffer_size = 0 # disable bufferization
383
+ yield from buffer
384
+ yield path
385
+ buffer = []
386
+ else:
387
+ buffer.append(path)
388
+ else:
389
+ yield path
390
+ buffer_size = 0 # disable bufferization
391
+ if buffer: # empty buffer
392
+ yield from buffer
393
+ buffer = []
394
+ else:
395
+ yield path
396
+
397
+ del stack[-1]
398
+ if stack:
399
+ path = path[:stack[-1][-1][-1]]
400
+ hashed_path = {x for x, *_ in path}
401
+ elif atom != start:
402
+ for_stack = []
403
+ closures = []
404
+ loop = 0
405
+ for next_atom in rings[atom]:
406
+ if next_atom == prev_atom: # only forward. behind us is the homeland
407
+ continue
408
+ elif next_atom == start:
409
+ loop = next_atom
410
+ elif next_atom in hashed_path: # closure found
411
+ closures.append(next_atom)
412
+ else:
413
+ for_stack.append(next_atom)
414
+
415
+ if loop: # we found starting point.
416
+ if bond == 2: # finish should be single bonded
417
+ if double_bonded: # ok
418
+ stack[-1].insert(0, (loop, atom, 1, None))
419
+ else:
420
+ del stack[-1]
421
+ if stack:
422
+ path = path[:stack[-1][-1][-1]]
423
+ hashed_path = {x for x, *_ in path}
424
+ continue
425
+ elif double_bonded: # we in quinone ring. finish should be single bonded
426
+ # side-path for storing double bond or atom is quinone or pyrrole
427
+ if for_stack or atom in double_bonded or atom in pyrroles:
428
+ stack[-1].insert(0, (loop, atom, 1, None))
429
+ else:
430
+ del stack[-1]
431
+ if stack:
432
+ path = path[:stack[-1][-1][-1]]
433
+ hashed_path = {x for x, *_ in path}
434
+ continue
435
+ else: # finish should be double bonded
436
+ stack[-1].insert(0, (loop, atom, 2, None))
437
+ bond = 2 # grow should be single bonded
438
+
439
+ if bond == 2 or atom in double_bonded: # double in - single out. quinone has two single bonds
440
+ for next_atom in closures:
441
+ path.append((next_atom, atom, 1)) # closures always single-bonded
442
+ stack[-1].remove((atom, next_atom, 1, None)) # remove fork from stack
443
+ for next_atom in for_stack:
444
+ stack[-1].append((next_atom, atom, 1, None))
445
+ elif len(for_stack) == 1: # easy path grow. next bond double or include single for pyrroles
446
+ next_atom = for_stack[0]
447
+ if next_atom in double_bonded: # need double bond, but next atom quinone
448
+ if atom in pyrroles:
449
+ stack[-1].append((next_atom, atom, 1, None))
450
+ else:
451
+ del stack[-1]
452
+ if stack:
453
+ path = path[:stack[-1][-1][-1]]
454
+ hashed_path = {x for x, *_ in path}
455
+ elif atom in pyrroles: # try pyrrole and pyridine
456
+ opposite = stack[-1].copy()
457
+ opposite.append((next_atom, atom, 2, None))
458
+ stack[-1].append((next_atom, atom, 1, len(path)))
459
+ stack.append(opposite)
460
+ else:
461
+ stack[-1].append((next_atom, atom, 2, None))
462
+ if closures:
463
+ next_atom = closures[0]
464
+ path.append((next_atom, atom, 1)) # closures always single-bonded
465
+ stack[-1].remove((atom, next_atom, 1, None)) # remove fork from stack
466
+ elif for_stack: # fork
467
+ next_atom1, next_atom2 = for_stack
468
+ if next_atom1 in double_bonded: # quinone next from fork
469
+ if next_atom2 in double_bonded:
470
+ if atom in pyrroles: # shit like O=C1C=CC2=CC=CC3=C2P1C(=O)C=C3
471
+ stack[-1].append((next_atom1, atom, 1, None))
472
+ stack[-1].append((next_atom2, atom, 1, None))
473
+ else: # bad path
474
+ del stack[-1]
475
+ if stack:
476
+ path = path[:stack[-1][-1][-1]]
477
+ hashed_path = {x for x, *_ in path}
478
+ elif atom in pyrroles: # O=C1C=CC2=CC=CC3=C2P1C=C3 or O=C1C=CC2=CC=CC3=C2P1=CC=C3
479
+ opposite = stack[-1].copy()
480
+ opposite.append((next_atom1, atom, 1, None))
481
+ opposite.append((next_atom2, atom, 2, None))
482
+ stack[-1].append((next_atom1, atom, 1, None))
483
+ stack[-1].append((next_atom2, atom, 1, len(path)))
484
+ stack.append(opposite) # pyridine first
485
+ else: # normal condensed ring
486
+ stack[-1].append((next_atom1, atom, 1, None))
487
+ stack[-1].append((next_atom2, atom, 2, None))
488
+ elif next_atom2 in double_bonded: # quinone next from fork
489
+ if atom in pyrroles:
490
+ opposite = stack[-1].copy()
491
+ opposite.append((next_atom2, atom, 1, None))
492
+ opposite.append((next_atom1, atom, 2, None))
493
+ stack[-1].append((next_atom1, atom, 1, None))
494
+ stack[-1].append((next_atom2, atom, 1, len(path)))
495
+ stack.append(opposite)
496
+ else:
497
+ stack[-1].append((next_atom2, atom, 1, None))
498
+ stack[-1].append((next_atom1, atom, 2, None))
499
+ elif atom in pyrroles: # C1=CC2=CC=CC3=C2P1C=C3 or C1=CP2=CC=CC3=C2C1=CC=C3
500
+ opposite1 = stack[-1].copy()
501
+ opposite1.append((next_atom2, atom, 1, None))
502
+ opposite1.append((next_atom1, atom, 2, len(path)))
503
+ opposite2 = stack[-1].copy()
504
+ opposite2.append((next_atom1, atom, 1, None))
505
+ opposite2.append((next_atom2, atom, 2, None))
506
+
507
+ stack[-1].append((next_atom1, atom, 1, None))
508
+ stack[-1].append((next_atom2, atom, 1, len(path)))
509
+ stack.append(opposite1)
510
+ stack.append(opposite2)
511
+ else: # new path
512
+ opposite = stack[-1].copy()
513
+ stack[-1].append((next_atom1, atom, 1, None))
514
+ stack[-1].append((next_atom2, atom, 2, len(path))) # double bond on top of stack
515
+ opposite.append((next_atom2, atom, 1, None))
516
+ opposite.append((next_atom1, atom, 2, None))
517
+ stack.append(opposite)
518
+ elif closures and atom not in pyrroles: # need double bond, but closure should be single bonded
519
+ del stack[-1]
520
+ if stack:
521
+ path = path[:stack[-1][-1][-1]]
522
+ hashed_path = {x for x, *_ in path}
523
+
524
+ if nether_yielded:
525
+ raise InvalidAromaticRing(f'kekule form not found for: {list(rings)}')
526
+ elif buffer: # optimal solution not found. return available.
527
+ yield from buffer
528
+
529
+
530
+ __all__ = ['Kekule']
@@ -0,0 +1,18 @@
1
+ # -*- coding: utf-8 -*-
2
+ #
3
+ # Copyright 2025 Ramil Nugmanov <nougmanoff@protonmail.com>
4
+ # This file is part of chython.
5
+ #
6
+ # chython is free software; you can redistribute it and/or modify
7
+ # it under the terms of the GNU Lesser General Public License as published by
8
+ # the Free Software Foundation; either version 3 of the License, or
9
+ # (at your option) any later version.
10
+ #
11
+ # This program is distributed in the hope that it will be useful,
12
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
13
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
+ # GNU Lesser General Public License for more details.
15
+ #
16
+ # You should have received a copy of the GNU Lesser General Public License
17
+ # along with this program; if not, see <https://www.gnu.org/licenses/>.
18
+ #