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,551 @@
1
+ # -*- coding: utf-8 -*-
2
+ #
3
+ # Copyright 2017-2025 Ramil Nugmanov <nougmanoff@protonmail.com>
4
+ # This file is part of chython.
5
+ #
6
+ # chython is free software; you can redistribute it and/or modify
7
+ # it under the terms of the GNU Lesser General Public License as published by
8
+ # the Free Software Foundation; either version 3 of the License, or
9
+ # (at your option) any later version.
10
+ #
11
+ # This program is distributed in the hope that it will be useful,
12
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
13
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
+ # GNU Lesser General Public License for more details.
15
+ #
16
+ # You should have received a copy of the GNU Lesser General Public License
17
+ # along with this program; if not, see <https://www.gnu.org/licenses/>.
18
+ #
19
+ from collections import defaultdict, deque
20
+ from functools import cached_property
21
+ from itertools import combinations
22
+ from operator import itemgetter
23
+ from typing import Any, Dict, List, Optional, Set, Tuple, TYPE_CHECKING, Union
24
+ from ..exceptions import ImplementationError
25
+
26
+
27
+ if TYPE_CHECKING:
28
+ from chython.containers import MoleculeContainer
29
+
30
+
31
+ class Rings:
32
+ __slots__ = ()
33
+
34
+ @cached_property
35
+ def sssr(self) -> List[Tuple[int, ...]]:
36
+ """
37
+ Smallest Set of Smallest Rings. Special bonds ignored.
38
+
39
+ Based on idea of PID matrices from:
40
+ Lee, C. J., Kang, Y.-M., Cho, K.-H., & No, K. T. (2009).
41
+ A robust method for searching the smallest set of smallest rings with a path-included distance matrix.
42
+ Proceedings of the National Academy of Sciences of the United States of America, 106(41), 17355–17358.
43
+ https://doi.org/10.1073/pnas.0813040106
44
+
45
+ :return rings atoms numbers
46
+ """
47
+ if self.rings_count:
48
+ return _sssr(self.not_special_connectivity, self.rings_count)
49
+ return []
50
+
51
+ @cached_property
52
+ def atoms_rings(self) -> Dict[int, List[Tuple[int, ...]]]:
53
+ """
54
+ A dictionary with atom numbers as keys and a list of tuples (representing SSSR rings) as values.
55
+ """
56
+ rings = defaultdict(list)
57
+ for r in self.sssr:
58
+ for n in r:
59
+ rings[n].append(r)
60
+ return dict(rings)
61
+
62
+ @cached_property
63
+ def atoms_rings_sizes(self) -> Dict[int, Set[int]]:
64
+ """
65
+ Sizes of SSSR rings containing atom.
66
+ """
67
+ return {n: {len(r) for r in rs} for n, rs in self.atoms_rings.items()}
68
+
69
+ @cached_property
70
+ def rings_count(self) -> int:
71
+ """
72
+ SSSR rings count. Ignored rings with special bonds.
73
+ """
74
+ bonds = self.not_special_connectivity
75
+ return sum(len(x) for x in bonds.values()) // 2 - len(bonds) + len(_connected_components(bonds))
76
+
77
+ @cached_property
78
+ def not_special_connectivity(self: 'MoleculeContainer') -> Dict[int, Set[int]]:
79
+ """
80
+ Graph connectivity without special bonds.
81
+ """
82
+ bonds = {}
83
+ for n, ms in self._bonds.items():
84
+ bonds[n] = ngb = set()
85
+ for m, b in ms.items():
86
+ if b != 8:
87
+ ngb.add(m)
88
+ return bonds
89
+
90
+ @cached_property
91
+ def connected_components(self: 'MoleculeContainer') -> List[Set[int]]:
92
+ """
93
+ Isolated components of single graph. E.g. salts as ion pair.
94
+ """
95
+ return _connected_components(self._bonds)
96
+
97
+ @property
98
+ def connected_components_count(self) -> int:
99
+ """
100
+ Number of components in graph
101
+ """
102
+ return len(self.connected_components)
103
+
104
+ @cached_property
105
+ def skin_graph(self: 'MoleculeContainer') -> Dict[int, Set[int]]:
106
+ """
107
+ Graph without terminal atoms. Only rings and linkers
108
+ """
109
+ return _skin_graph(self._bonds)
110
+
111
+ @cached_property
112
+ def rings_graph(self: 'MoleculeContainer'):
113
+ """
114
+ Graph of rings. Linkers are not included. Special bonds are considered.
115
+ """
116
+ bonds = {n: ms.copy() for n, ms in self.skin_graph.items()}
117
+ if not bonds:
118
+ return {}
119
+
120
+ in_rings = set()
121
+ atoms = set(bonds)
122
+ while atoms:
123
+ stack = deque([(atoms.pop(), 0, 0)])
124
+ path = []
125
+ seen = set()
126
+ while stack:
127
+ c, p, d = stack.pop()
128
+ if len(path) > d:
129
+ path = path[:d]
130
+ if c in in_rings:
131
+ continue
132
+ path.append(c)
133
+ seen.add(c)
134
+
135
+ d += 1
136
+ for n in bonds[c]:
137
+ if n == p:
138
+ continue
139
+ elif n in seen:
140
+ in_rings.update(path[path.index(n):])
141
+ else:
142
+ stack.append((n, c, d))
143
+
144
+ atoms.difference_update(seen)
145
+ for n in bonds.keys() - in_rings:
146
+ for m in bonds.pop(n):
147
+ bonds[m].discard(n)
148
+ return bonds
149
+
150
+
151
+ def _sssr(bonds: Dict[int, Union[Set[int], Dict[int, Any]]], n_sssr: int) -> List[Tuple[int, ...]]:
152
+ """
153
+ Smallest Set of Smallest Rings of any adjacency matrix.
154
+ Number of rings required.
155
+ """
156
+ bonds = _skin_graph(bonds)
157
+ paths = _bfs(bonds)
158
+ pid1, pid2, dist = _make_pid(paths)
159
+ return _rings_filter(_c_set(pid1, pid2, dist), n_sssr)
160
+
161
+
162
+ def _connected_components(bonds: Dict[int, Union[Set[int], Dict[int, Any]]]) -> List[Set[int]]:
163
+ atoms = set(bonds)
164
+ components = []
165
+ while atoms:
166
+ start = atoms.pop()
167
+ seen = {start}
168
+ queue = deque([start])
169
+ while queue:
170
+ current = queue.popleft()
171
+ for i in bonds[current]:
172
+ if i not in seen:
173
+ queue.append(i)
174
+ seen.add(i)
175
+ components.append(seen)
176
+ atoms.difference_update(seen)
177
+ return components
178
+
179
+
180
+ def _skin_graph(bonds: Dict[int, Union[Set[int], Dict[int, Any]]]) -> Dict[int, Set[int]]:
181
+ """
182
+ Graph without terminal nodes. Only rings and linkers
183
+ """
184
+ bonds = {n: set(ms) for n, ms in bonds.items() if ms}
185
+ while True: # skip not-cycle chains
186
+ try:
187
+ n = next(n for n, ms in bonds.items() if len(ms) <= 1)
188
+ except StopIteration:
189
+ break
190
+ for m in bonds.pop(n):
191
+ bonds[m].discard(n)
192
+ return bonds
193
+
194
+
195
+ def _bfs(bonds):
196
+ atoms = set(bonds)
197
+ terminated = []
198
+ tail = atoms.pop()
199
+ next_stack = {x: [tail, x] for x in bonds[tail]}
200
+
201
+ while True:
202
+ next_front = set()
203
+ found_odd = set()
204
+ stack, next_stack = next_stack, {}
205
+ for tail, path in stack.items():
206
+ neighbors = bonds[tail] & atoms
207
+ next_front.add(tail)
208
+
209
+ if len(neighbors) == 1:
210
+ n = neighbors.pop()
211
+ if n in found_odd:
212
+ if len(path) != 1:
213
+ terminated.append(tuple(path)) # save second ring closure
214
+ next_stack[n] = [n] # maybe we have another path?
215
+ else:
216
+ path.append(n)
217
+ if n in stack: # odd rings
218
+ found_odd.add(tail)
219
+ terminated.append(tuple(path)) # found ring closure. save path.
220
+ elif n in next_stack: # even rings
221
+ terminated.append(tuple(path))
222
+ if len(next_stack[n]) != 1: # prevent bicycle case
223
+ terminated.append(tuple(next_stack[n]))
224
+ next_stack[n] = [n]
225
+ else:
226
+ next_stack[n] = path # grow must go on
227
+ elif neighbors:
228
+ if len(path) != 1:
229
+ terminated.append(tuple(path)) # save path.
230
+ for n in neighbors:
231
+ if n in found_odd:
232
+ if n in stack:
233
+ if n in next_stack:
234
+ del next_stack[n]
235
+ else:
236
+ next_stack[n] = [n]
237
+ else:
238
+ path = [tail, n]
239
+ if n in stack: # odd rings
240
+ found_odd.add(tail)
241
+ terminated.append(tuple(path))
242
+ elif n in next_stack: # even rings
243
+ terminated.append(tuple(path))
244
+ if len(next_stack[n]) != 1: # prevent bicycle case
245
+ terminated.append(tuple(next_stack[n]))
246
+ next_stack[n] = [n]
247
+ else:
248
+ next_stack[n] = path
249
+
250
+ atoms.difference_update(next_front)
251
+ if not atoms:
252
+ break
253
+ elif not next_stack:
254
+ tail = atoms.pop()
255
+ next_stack = {x: [tail, x] for x in bonds[tail] & atoms}
256
+ return terminated
257
+
258
+
259
+ def _make_pid(paths: List[List[int]]):
260
+ pid1 = defaultdict(lambda: defaultdict(dict))
261
+ pid2 = defaultdict(lambda: defaultdict(dict))
262
+ distances = defaultdict(lambda: defaultdict(lambda: 1e9))
263
+ chains = sorted(paths, key=len)
264
+ for c in chains:
265
+ di = len(c) - 1
266
+ n, m = c[0], c[-1]
267
+ nn, mm = c[1], c[-2]
268
+ if n in distances and m in distances[n] and distances[n][m] != di:
269
+ pid2[n][m][(nn, mm)] = c
270
+ pid2[m][n][(mm, nn)] = c[::-1]
271
+ else:
272
+ pid1[n][m][(nn, mm)] = c
273
+ pid1[m][n][(mm, nn)] = c[::-1]
274
+ distances[n][m] = distances[m][n] = di
275
+
276
+ for k in pid1:
277
+ new_distances = defaultdict(dict)
278
+ dk = distances[k]
279
+ ndk = new_distances[k]
280
+ for i in pid1:
281
+ if i == k:
282
+ continue
283
+ di = distances[i]
284
+ ndi = new_distances[i]
285
+ ndk[i] = ndi[k] = di[k]
286
+ for j in pid1:
287
+ if j == k or j == i:
288
+ continue
289
+ ij = di[j]
290
+ ikj = di[k] + dk[j]
291
+ if ij - ikj == 1: # A new shortest path == previous shortest path - 1
292
+ pid2[i][j] = pid1[i][j]
293
+ pid1[i][j] = {(ni, mj): ip[:-1] + jp for ((ni, _), ip), ((_, mj), jp) in
294
+ zip(pid1[i][k].items(), pid1[k][j].items())}
295
+ ndi[j] = ikj
296
+ elif ij > ikj: # A new shortest path
297
+ pid2[i][j] = {}
298
+ pid1[i][j] = {(ni, mj): ip[:-1] + jp for ((ni, _), ip), ((_, mj), jp) in
299
+ zip(pid1[i][k].items(), pid1[k][j].items())}
300
+ ndi[j] = ikj
301
+ elif ij == ikj: # Another shortest path
302
+ pid1[i][j].update({(ni, mj): ip[:-1] + jp for ((ni, _), ip), ((_, mj), jp) in
303
+ zip(pid1[i][k].items(), pid1[k][j].items())})
304
+ ndi[j] = ij
305
+ elif ikj - ij == 1: # Shortest+1 path
306
+ pid2[i][j].update({(ni, mj): ip[:-1] + jp for ((ni, _), ip), ((_, mj), jp) in
307
+ zip(pid1[i][k].items(), pid1[k][j].items())})
308
+ ndi[j] = ij
309
+ else:
310
+ ndi[j] = ij
311
+ distances = new_distances
312
+ return pid1, pid2, distances
313
+
314
+
315
+ def _c_set(pid1, pid2, pid1l):
316
+ c_set = []
317
+ seen = set()
318
+ for i, p1i in pid1.items():
319
+ seen.add(i)
320
+ di = pid1l[i]
321
+ p2i = pid2[i]
322
+
323
+ for j, p1ij in p1i.items():
324
+ if j in seen:
325
+ continue
326
+ p1ij = list(p1ij.values())
327
+ p2ij = list(p2i[j].values())
328
+ dij = di[j] * 2
329
+
330
+ if len(p1ij) == 1: # one shortest
331
+ if not p2ij: # need shortest + 1 path
332
+ continue
333
+ c_set.append((dij + 1, p1ij, p2ij))
334
+ elif not p2ij: # one or more odd rings
335
+ c_set.append((dij, p1ij, None))
336
+ else: # odd and even rings found (e.g. bicycle)
337
+ c_set.append((dij, p1ij, None))
338
+ c_set.append((dij + 1, p1ij, p2ij))
339
+
340
+ for c_num, p1ij, p2ij in sorted(c_set, key=itemgetter(0)):
341
+ if c_num % 2: # odd rings
342
+ for c1 in p1ij:
343
+ for c2 in p2ij:
344
+ c = c1 + c2[-2:0:-1]
345
+ if len(c) == len(set(c)):
346
+ yield _canonic_ring(c)
347
+ else:
348
+ for c1, c2 in zip(p1ij, p1ij[1:]):
349
+ c = c1 + c2[-2:0:-1]
350
+ if len(c) == len(set(c)):
351
+ yield _canonic_ring(c)
352
+
353
+
354
+ def _canonic_ring(ring: Tuple[int, ...]) -> Tuple[int, ...]:
355
+ n = min(ring)
356
+ ndx = ring.index(n)
357
+ if ndx == 0:
358
+ if ring[-1] < ring[1]:
359
+ return n, *ring[:0:-1]
360
+ return ring
361
+ elif ndx == len(ring) - 1:
362
+ if ring[0] > ring[-2]:
363
+ return ring[::-1]
364
+ return n, *ring[:-1]
365
+ if ring[ndx + 1] > ring[ndx - 1]:
366
+ return *ring[ndx::-1], *ring[:ndx:-1]
367
+ return *ring[ndx:], *ring[:ndx]
368
+
369
+
370
+ def _ring_scissors(ring: Tuple[int, ...], n: int, m: int) -> Tuple[int, ...]:
371
+ ndx = ring.index(n)
372
+ mdx = ring.index(m)
373
+ if ndx == 0:
374
+ if mdx == 1:
375
+ return n, *ring[:0:-1]
376
+ return ring
377
+ elif ndx == len(ring) - 1:
378
+ if mdx == 0:
379
+ return ring[::-1]
380
+ return n, *ring[:-1]
381
+ if ndx < mdx:
382
+ return *ring[ndx::-1], *ring[:ndx:-1]
383
+ return *ring[ndx:], *ring[:ndx]
384
+
385
+
386
+ def _ring_adjacency(ring: Tuple[int, ...]) -> Dict[int, List[int]]:
387
+ adj = {ring[0]: [ring[-1]]} # ring adjacency matrix
388
+ for n, m in zip(ring, ring[1:]):
389
+ adj[n].append(m)
390
+ adj[m] = [n]
391
+ adj[m].append(ring[0])
392
+ return adj
393
+
394
+
395
+ def _is_condensed_ring(c, sssr, seen_rings):
396
+ # create graph of connected neighbour rings
397
+ ck = seen_rings[c]
398
+ neighbors = {x: set() for x in sssr if len(seen_rings[x].keys() & ck.keys()) > 1}
399
+ if len(neighbors) > 1:
400
+ for (i, iv), (j, jv) in combinations(neighbors.items(), 2):
401
+ if len(seen_rings[i].keys() & seen_rings[j].keys()) > 1:
402
+ iv.add(j)
403
+ jv.add(i)
404
+ # check if hold rings is combination of existing. (123654) is combo of (1254) and (2365)
405
+ #
406
+ # 1--2--3
407
+ # | | |
408
+ # 4--5--6
409
+ #
410
+ # modified NX.dfs_labeled_edges
411
+ # https://networkx.github.io/documentation/stable/reference/algorithms/generated/networkx.algorithms.\
412
+ # traversal.depth_first_search.dfs_labeled_edges.html
413
+ depth_limit = len(neighbors) - 1
414
+ for start, nbrs in neighbors.items():
415
+ if not nbrs:
416
+ continue
417
+ stack = [(start, seen_rings[start], depth_limit, iter(nbrs), {start})]
418
+ while stack:
419
+ parent, p_adj, depth_now, children, seen = stack[-1]
420
+ try:
421
+ child = next(children)
422
+ except StopIteration:
423
+ stack.pop()
424
+ else:
425
+ if child not in seen:
426
+ common = p_adj.keys() & seen_rings[child].keys()
427
+ if len(common) > 2: # only terminal common atoms required
428
+ term = {n for n in common if len(common.intersection(p_adj[n])) == 1}
429
+ if len(term) != 2: # skip multiple contacts
430
+ continue
431
+ common.difference_update(term)
432
+ n, m = term
433
+ mc = _canonic_ring(
434
+ (*_ring_scissors(tuple(x for x in parent if x not in common), n, m),
435
+ *_ring_scissors(tuple(x for x in child if x not in common), m, n)[1:-1]))
436
+ elif len(common) == 2:
437
+ n, m = common
438
+ mc = _canonic_ring((*_ring_scissors(parent, n, m), *_ring_scissors(child, m, n)[1:-1]))
439
+ else: # point connections
440
+ continue
441
+ if c == mc: # macrocycle found
442
+ return True
443
+ elif depth_now and 2 < len(mc) <= len(c) + 1:
444
+ stack.append((mc, _ring_adjacency(mc), depth_now - 1, iter(neighbors[child]),
445
+ {child} | seen))
446
+ return False
447
+
448
+
449
+ def _get_unique_chord(ring: Tuple[int, ...], common: Set[int]) -> Optional[Tuple[int, ...]]:
450
+ lc = len(common)
451
+ if len(ring) == lc:
452
+ if common == set(ring):
453
+ return ()
454
+ else:
455
+ if common == set(ring[:lc]):
456
+ return *ring[lc - 1:], ring[0]
457
+ for _ in range(len(ring) - 1):
458
+ ring = (*ring[1:], ring[0])
459
+ if common == set(ring[:lc]):
460
+ return *ring[lc - 1:], ring[0]
461
+
462
+
463
+ def _connected_rings(rings, seen_rings):
464
+ rings = rings.copy()
465
+ out = []
466
+ for i in range(len(rings)):
467
+ c = rings[i]
468
+ ck = seen_rings[c]
469
+ for j in range(i + 1, len(rings)):
470
+ r = rings[j]
471
+ rk = seen_rings[r]
472
+ common = rk.keys() & ck.keys()
473
+ if len(common) == 2: # one common bond
474
+ n, m = common
475
+ if m in ck[n] and m in rk[n]: # only common bond!
476
+ c = _canonic_ring((*_ring_scissors(c, n, m), *_ring_scissors(r, m, n)[1:-1]))
477
+ ck = _ring_adjacency(c)
478
+ rings[j] = c
479
+ seen_rings[c] = ck
480
+ break
481
+ elif len(common) > 2:
482
+ cc = _get_unique_chord(c, common)
483
+ if cc is None: # skip multitouched rings
484
+ continue
485
+ r = _get_unique_chord(r, common)
486
+ if r is None:
487
+ continue
488
+ if cc:
489
+ if r:
490
+ if r[0] == cc[0]:
491
+ r = r[::-1]
492
+ c = _canonic_ring((*cc, *r[1:-1]))
493
+ ck = _ring_adjacency(c)
494
+ rings[j] = c
495
+ seen_rings[c] = ck
496
+ break
497
+ else:
498
+ c = _canonic_ring(cc)
499
+ ck = _ring_adjacency(c)
500
+ rings[j] = c
501
+ seen_rings[c] = ck
502
+ break
503
+ elif r:
504
+ c = _canonic_ring(r)
505
+ ck = _ring_adjacency(c)
506
+ rings[j] = c
507
+ seen_rings[c] = ck
508
+ break
509
+ else: # isolated ring[s] found
510
+ out.append(c)
511
+ return out
512
+
513
+
514
+ def _rings_filter(rings, n_sssr):
515
+ c = next(rings)
516
+ if n_sssr == 1:
517
+ return [c]
518
+
519
+ seen_rings = {c}
520
+ sssr_atoms = set(c)
521
+ sssr = [c]
522
+ hold = []
523
+ for c in rings:
524
+ if c in seen_rings:
525
+ continue
526
+ seen_rings.add(c)
527
+ if sssr_atoms.issuperset(c): # potentially condensed ring
528
+ hold.append(c)
529
+ continue
530
+ sssr_atoms.update(c)
531
+ sssr.append(c)
532
+ if len(sssr) == n_sssr:
533
+ return sssr
534
+
535
+ # now we have set of plug rings (cuban fullerene), besiege rings and condensed trash
536
+ seen_rings = {c: _ring_adjacency(c) for c in seen_rings} # prepare adjacency
537
+ condensed_rings = _connected_rings(sssr, seen_rings) # collection of contours of condensed rings
538
+
539
+ for c in hold:
540
+ if c in condensed_rings or _is_condensed_ring(c, sssr, seen_rings):
541
+ continue
542
+ condensed_rings.insert(0, c)
543
+ condensed_rings = _connected_rings(condensed_rings, seen_rings)
544
+ sssr.append(c)
545
+ if len(sssr) == n_sssr:
546
+ return sorted(sssr, key=len)
547
+
548
+ raise ImplementationError('SSSR count not reached')
549
+
550
+
551
+ __all__ = ['Rings']