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,429 @@
1
+ # -*- coding: utf-8 -*-
2
+ #
3
+ # Copyright 2018-2024 Ramil Nugmanov <nougmanoff@protonmail.com>
4
+ # Copyright 2021 Timur Gimadiev <timur.gimadiev@gmail.com>
5
+ # Copyright 2024 Philippe Gantzer <p.gantzer@icredd.hokudai.ac.jp>
6
+ # This file is part of chython.
7
+ #
8
+ # chython is free software; you can redistribute it and/or modify
9
+ # it under the terms of the GNU Lesser General Public License as published by
10
+ # the Free Software Foundation; either version 3 of the License, or
11
+ # (at your option) any later version.
12
+ #
13
+ # This program is distributed in the hope that it will be useful,
14
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
15
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16
+ # GNU Lesser General Public License for more details.
17
+ #
18
+ # You should have received a copy of the GNU Lesser General Public License
19
+ # along with this program; if not, see <https://www.gnu.org/licenses/>.
20
+ #
21
+ from collections import defaultdict
22
+ from typing import List, Tuple, TYPE_CHECKING, Union
23
+ from ._reagents import *
24
+ from ...exceptions import MappingError
25
+
26
+
27
+ if TYPE_CHECKING:
28
+ from chython import ReactionContainer
29
+
30
+
31
+ class StandardizeReaction:
32
+ __slots__ = ()
33
+
34
+ def canonicalize(self: 'ReactionContainer', *, fix_mapping: bool = True, logging=False, fix_tautomers=True) -> \
35
+ Union[bool, List[Tuple[int, Tuple[int, ...], int, str]]]:
36
+ """
37
+ Convert molecules to canonical forms of functional groups and aromatic rings without explicit hydrogens.
38
+ Return True if in any molecule found not canonical group.
39
+
40
+ :param fix_mapping: Search AAM errors of functional groups.
41
+ :param logging: return log from molecules with index of molecule.
42
+ Otherwise, return True if these groups found in any molecule.
43
+ :param fix_tautomers: convert tautomers to canonical forms.
44
+ """
45
+ total = []
46
+ for n, m in enumerate(self.molecules()):
47
+ total.extend((n, *x) for x in m.canonicalize(logging=True, fix_tautomers=fix_tautomers))
48
+
49
+ if fix_mapping:
50
+ total.extend((-1, x, -1, m) for m, x in self.fix_groups_mapping(logging=True))
51
+
52
+ if total:
53
+ self.flush_cache(keep_molecule_cache=True)
54
+ if logging:
55
+ return total
56
+ return bool(total)
57
+
58
+ def standardize(self: 'ReactionContainer', *, fix_mapping: bool = True, logging=False, fix_tautomers=True) -> \
59
+ Union[bool, List[Tuple[int, Tuple[int, ...], int, str]]]:
60
+ """
61
+ Fix functional groups representation.
62
+ Return True if in any molecule fixed group.
63
+
64
+ Deprecated method. Use `canonicalize` directly.
65
+
66
+ :param fix_mapping: Search AAM errors of functional groups.
67
+ :param logging: return log from molecules with index of molecule.
68
+ Otherwise, return True if these groups found in any molecule.
69
+ :param fix_tautomers: convert tautomers to canonical forms.
70
+ """
71
+ total = []
72
+ for n, m in enumerate(self.molecules()):
73
+ total.extend((n, *x) for x in m.standardize(logging=True, fix_tautomers=fix_tautomers))
74
+
75
+ if fix_mapping:
76
+ total.extend((-1, x, -1, m) for m, x in self.fix_groups_mapping(logging=True))
77
+
78
+ if total:
79
+ self.flush_cache(keep_molecule_cache=True)
80
+ if logging:
81
+ return total
82
+ return bool(total)
83
+
84
+ def thiele(self: 'ReactionContainer', *, fix_tautomers=True) -> bool:
85
+ """
86
+ Convert structures to aromatic form.
87
+ Return True if in any molecule found kekule ring
88
+
89
+ :param fix_tautomers: convert tautomers to canonical forms.
90
+ """
91
+ total = False
92
+ for m in self.molecules():
93
+ if m.thiele(fix_tautomers=fix_tautomers):
94
+ total = True
95
+ if total:
96
+ self.flush_cache(keep_molecule_cache=True)
97
+ return total
98
+
99
+ def kekule(self: 'ReactionContainer', *, buffer_size=7) -> bool:
100
+ """
101
+ Convert structures to kekule form.
102
+ Return True if in any molecule found aromatic ring
103
+
104
+ :param buffer_size: number of attempts of pyridine form searching.
105
+ """
106
+ total = False
107
+ for m in self.molecules():
108
+ if m.kekule(buffer_size=buffer_size):
109
+ total = True
110
+ if total:
111
+ self.flush_cache(keep_molecule_cache=True)
112
+ return total
113
+
114
+ def clean_isotopes(self: 'ReactionContainer') -> bool:
115
+ """
116
+ Clean isotope marks for all molecules in reaction.
117
+ Returns True if in any molecule found isotope.
118
+ """
119
+ flag = False
120
+ for m in self.molecules():
121
+ if m.clean_isotopes():
122
+ flag = True
123
+ if flag:
124
+ self.flush_cache(keep_molecule_cache=True)
125
+ return flag
126
+
127
+ def clean_stereo(self: 'ReactionContainer'):
128
+ """
129
+ Remove stereo data
130
+ """
131
+ for m in self.molecules():
132
+ m.clean_stereo()
133
+ self.flush_cache(keep_molecule_cache=True)
134
+
135
+ def check_valence(self: 'ReactionContainer') -> List[Tuple[int, Tuple[int, ...]]]:
136
+ """
137
+ Check valences of all atoms of all molecules.
138
+
139
+ Works only on molecules with aromatic rings in Kekule form.
140
+ :return: list of invalid molecules with invalid atoms lists
141
+ """
142
+ out = []
143
+ for n, m in enumerate(self.molecules()):
144
+ if c := m.check_valence():
145
+ out.append((n, tuple(c)))
146
+ return out
147
+
148
+ def implicify_hydrogens(self: 'ReactionContainer') -> int:
149
+ """
150
+ Remove explicit hydrogens if possible.
151
+
152
+ :return: number of removed hydrogens.
153
+ """
154
+ total = 0
155
+ for m in self.molecules():
156
+ total += m.implicify_hydrogens()
157
+ if total:
158
+ self.flush_cache(keep_molecule_cache=True)
159
+ return total
160
+
161
+ def explicify_hydrogens(self: 'ReactionContainer') -> int:
162
+ """
163
+ Add explicit hydrogens to atoms
164
+
165
+ :return: number of added atoms
166
+ """
167
+ total = 0
168
+ start_map = 0
169
+ for m in self.molecules():
170
+ map_ = max(m, default=0)
171
+ if map_ > start_map:
172
+ start_map = map_
173
+
174
+ mapping = defaultdict(list)
175
+ for m in self.reactants:
176
+ maps = m.explicify_hydrogens(_return_map=True, start_map=start_map + 1)
177
+ if maps:
178
+ for n, h in maps:
179
+ mapping[n].append(h)
180
+ start_map = maps[-1][1]
181
+ total += len(maps)
182
+
183
+ for m in self.reagents:
184
+ maps = m.explicify_hydrogens(_return_map=True, start_map=start_map + 1)
185
+ if maps:
186
+ start_map = maps[-1][1]
187
+ total += len(maps)
188
+
189
+ for m in self.products:
190
+ maps = m.explicify_hydrogens(_return_map=True, start_map=start_map + 1)
191
+ if maps:
192
+ total += len(maps)
193
+ remap = {}
194
+ free = []
195
+ for n, h in maps:
196
+ if n in mapping and mapping[n]:
197
+ remap[h] = mapping[n].pop()
198
+ free.append(h)
199
+ elif free:
200
+ remap[h] = start_map = free.pop(0)
201
+ else:
202
+ start_map = h
203
+ m.remap(remap)
204
+
205
+ if total:
206
+ self.flush_cache(keep_molecule_cache=True)
207
+ return total
208
+
209
+ def remove_reagents(self, *, keep_reagents: bool = False, mapping: bool = True) -> bool:
210
+ """
211
+ Place molecules, except reactants, to reagents list. Reagents - molecules which atoms not presented in products.
212
+ Mapping based approach remove molecules without reaction center.
213
+ Rule based approach remove equal molecules in reactants and products, and predefined reactants.
214
+
215
+ :param mapping: use atom-to-atom mapping to detect reagents, otherwise use predefined list of common reagents.
216
+ :param keep_reagents: delete reagents if False
217
+
218
+ Return True if any reagent found.
219
+ """
220
+ if mapping:
221
+ return self.__remove_reagents_mapping(keep_reagents)
222
+ return self.__remove_reagents_rules(keep_reagents)
223
+
224
+ def __remove_reagents_rules(self: 'ReactionContainer', keep_reagents):
225
+ if not self.reactants or not self.products: # there is no reaction
226
+ return False
227
+
228
+ reactants_st1 = []
229
+ products_st1 = []
230
+ reagents_st1 = set(self.reagents)
231
+
232
+ # find the same molecules in reactants and products
233
+ for m in self.reactants:
234
+ if m in self.products:
235
+ reagents_st1.add(m)
236
+ else:
237
+ reactants_st1.append(m)
238
+ for m in self.products:
239
+ if m in self.reactants:
240
+ reagents_st1.add(m)
241
+ else:
242
+ products_st1.append(m)
243
+ if not reactants_st1 or not products_st1:
244
+ return False # keep bad reaction as is
245
+
246
+ reactants_st2 = []
247
+ products_st2 = []
248
+ reagents_st2 = reagents_st1.copy()
249
+
250
+ # filter out predefined reagents
251
+ for m in reactants_st1:
252
+ if m in reagents_set:
253
+ reagents_st2.add(m)
254
+ else:
255
+ reactants_st2.append(m)
256
+ for m in products_st1:
257
+ if m in reagents_set:
258
+ reagents_st2.add(m)
259
+ else:
260
+ products_st2.append(m)
261
+ if not reactants_st2 or not products_st2: # reaction contains only simple molecules. roll-back to step 1
262
+ reactants_st2 = reactants_st1
263
+ products_st2 = products_st1
264
+ reagents_st2 = reagents_st1
265
+
266
+ # remove reagents from reactants
267
+ tmp = []
268
+ for m in self.reagents:
269
+ tmp.append(m)
270
+ if m in reagents_st2:
271
+ reagents_st2.discard(m)
272
+ tmp.extend(reagents_st2)
273
+ reagents = tuple(tmp) if keep_reagents else ()
274
+
275
+ self._reactants = tuple(reactants_st2)
276
+ self._products = tuple(products_st2)
277
+ self._reagents = reagents
278
+ self.fix_positions()
279
+ return True
280
+
281
+ def __remove_reagents_mapping(self: 'ReactionContainer', keep_reagents):
282
+ cgr = ~self
283
+ if cgr.center_atoms:
284
+ active = set(cgr.center_atoms)
285
+ reactants = []
286
+ products = []
287
+ reagents = set(self.reagents)
288
+ for i in self.reactants:
289
+ if not active.isdisjoint(i):
290
+ reactants.append(i)
291
+ else:
292
+ reagents.add(i)
293
+ for i in self.products:
294
+ if not active.isdisjoint(i):
295
+ products.append(i)
296
+ else:
297
+ reagents.add(i)
298
+
299
+ # remove reagents from reactants
300
+ tmp = []
301
+ for m in self.reagents:
302
+ tmp.append(m)
303
+ if m in reagents:
304
+ reagents.discard(m)
305
+ tmp.extend(reagents)
306
+ reagents = tuple(tmp) if keep_reagents else ()
307
+
308
+ if len(reactants) != len(self.reactants) or len(products) != len(self.products) or len(reagents) != len(self.reagents):
309
+ self._reactants = tuple(reactants)
310
+ self._products = tuple(products)
311
+ self._reagents = reagents
312
+ self.fix_positions()
313
+ return True
314
+ return False
315
+ raise MappingError("Reaction center is absent according to mapping")
316
+
317
+ def contract_ions(self: 'ReactionContainer') -> bool:
318
+ """
319
+ Contract ions into salts (Molecules with disconnected components).
320
+ Note: works only for unambiguous cases. e.g. equal anions/cations and different or equal cations/anions.
321
+
322
+ Return True if any ions contracted.
323
+ """
324
+ neutral, cations, anions, total = _sift_ions(self.reagents)
325
+ salts = _contract_ions(anions, cations, total)
326
+ if salts:
327
+ neutral.extend(salts)
328
+ self._reagents = tuple(neutral)
329
+ changed = True
330
+ else:
331
+ changed = False
332
+
333
+ neutral, cations, anions, total = _sift_ions(self.reactants)
334
+ salts = _contract_ions(anions, cations, total)
335
+ if salts:
336
+ anions_order = {frozenset(m): n for n, m in enumerate(anions)}
337
+ cations_order = {frozenset(m): n for n, m in enumerate(cations)}
338
+ neutral.extend(salts)
339
+ self._reactants = tuple(neutral)
340
+ changed = True
341
+ else:
342
+ anions_order = cations_order = {}
343
+
344
+ neutral, cations, anions, total = _sift_ions(self.products)
345
+ if cations and anions:
346
+ anions.sort(key=lambda x: anions_order.get(frozenset(x), -1))
347
+ cations.sort(key=lambda x: cations_order.get(frozenset(x), -1))
348
+ salts = _contract_ions(anions, cations, total)
349
+ if salts:
350
+ neutral.extend(salts)
351
+ self._products = tuple(neutral)
352
+ changed = True
353
+
354
+ if changed:
355
+ self.fix_positions()
356
+ return True
357
+ return False
358
+
359
+
360
+ def _sift_ions(mols):
361
+ anions = []
362
+ cations = []
363
+ neutral = []
364
+ total = 0
365
+ for m in mols:
366
+ c = int(m)
367
+ total += c
368
+ if c > 0:
369
+ cations.append(m)
370
+ elif c < 0:
371
+ anions.append(m)
372
+ else:
373
+ neutral.append(m)
374
+ return neutral, cations, anions, total
375
+
376
+
377
+ def _contract_ions(anions, cations, total):
378
+ if not anions or not cations: # nothing to contract
379
+ return
380
+ # check ambiguous cases
381
+ if total > 0:
382
+ if len(cations) > 1: # deficit of anions
383
+ # we have an excess of cations. we can't assign anions univocally
384
+ return # unite is ambiguous
385
+ salt = cations[0]
386
+ shift_x = salt._fix_plane_mean(0) + 1
387
+ for x in anions:
388
+ shift_x = x._fix_plane_mean(shift_x) + 1
389
+ salt = salt | x
390
+ return [salt]
391
+ elif total < 0:
392
+ if len(anions) > 1: # deficit of cations
393
+ # we have an excess of anions. we can't assign cations univocally
394
+ return # unite is ambiguous
395
+ salt = anions[0]
396
+ shift_x = salt._fix_plane_mean(0) + 1
397
+ for x in cations:
398
+ shift_x = x._fix_plane_mean(shift_x) + 1
399
+ salt = salt | x
400
+ return [salt]
401
+ elif len(set(anions)) > 1 and len(set(cations)) > 1: # different anions and cations
402
+ return
403
+
404
+ salts = []
405
+ anions = anions.copy()
406
+ cations = cations.copy()
407
+ while anions:
408
+ ct = cations.pop()
409
+ an = anions.pop()
410
+ shift_x = ct._fix_plane_mean(0) + 1
411
+ shift_x = an._fix_plane_mean(shift_x) + 1
412
+ salt = ct | an
413
+ while True:
414
+ c = int(salt)
415
+ if c > 0:
416
+ an = anions.pop()
417
+ shift_x = an._fix_plane_mean(shift_x) + 1
418
+ salt = salt | an
419
+ elif c < 0:
420
+ ct = cations.pop()
421
+ shift_x = ct._fix_plane_mean(shift_x) + 1
422
+ salt = salt | ct
423
+ else:
424
+ break
425
+ salts.append(salt)
426
+ return salts
427
+
428
+
429
+ __all__ = ['StandardizeReaction']
@@ -0,0 +1,196 @@
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 typing import List, TYPE_CHECKING, Union
20
+ from ...exceptions import ValenceError
21
+
22
+
23
+ if TYPE_CHECKING:
24
+ from chython import MoleculeContainer
25
+
26
+
27
+ # atomic number constants
28
+ B = 5
29
+ C = 6
30
+ N = 7
31
+ O = 8
32
+ Si = 14
33
+ P = 15
34
+ S = 16
35
+ As = 33
36
+ Se = 34
37
+ Te = 52
38
+
39
+
40
+ class Resonance:
41
+ __slots__ = ()
42
+
43
+ def fix_resonance(self: Union['MoleculeContainer', 'Resonance'], *, logging=False,
44
+ _fix_stereo=True) -> Union[bool, List[int]]:
45
+ """
46
+ Transform biradical or dipole resonance structures into neutral form. Return True if structure form changed.
47
+
48
+ :param logging: return list of changed atoms.
49
+ """
50
+ atoms = self._atoms
51
+ bonds = self._bonds
52
+ entries, exits, rads, constrains, nitrogen_cat, nitrogen_ani, sulfur_cat = self.__entries()
53
+ hs = set()
54
+ while len(rads) > 1:
55
+ n = rads.pop()
56
+ for path in self.__find_delocalize_path(n, rads, constrains, True):
57
+ atoms[n]._is_radical = False
58
+ hs.add(n)
59
+ for n, m, b in path:
60
+ hs.add(m)
61
+ bonds[n][m]._order = b
62
+ atoms[m]._is_radical = False # noqa
63
+ rads.discard(m)
64
+ break # path found
65
+ # path not found. atom n keep as is
66
+ while entries and exits:
67
+ n = entries.pop()
68
+ for path in self.__find_delocalize_path(n, exits, constrains, False):
69
+ l, m, b = path[-1]
70
+ if n in nitrogen_cat and m in nitrogen_ani:
71
+ continue
72
+
73
+ if m in sulfur_cat: # prevent X-[S+]=X >> X=S=X
74
+ if b != 1:
75
+ continue
76
+ atoms[m]._charge -= 1
77
+ else: # check cations end valence.
78
+ atoms[m]._charge -= 1 # reduce atom change and check valence
79
+ try:
80
+ atoms[m].valence_rules(sum(int(y) for x, y in bonds[m].items() if x != l) + b)
81
+ except ValenceError:
82
+ atoms[m]._charge += 1 # roll back
83
+ continue
84
+
85
+ # succeed!
86
+ atoms[n]._charge += 1
87
+ hs.add(n)
88
+ for n, m, b in path:
89
+ hs.add(m)
90
+ bonds[n][m]._order = b
91
+ exits.discard(m)
92
+ break # path from negative atom to positive atom found.
93
+ # path not found. keep negative atom n as is
94
+ if hs:
95
+ for n in hs:
96
+ self.calc_implicit(n)
97
+ self.flush_cache(keep_sssr=True, keep_components=True)
98
+ if _fix_stereo:
99
+ self.fix_stereo()
100
+ if logging:
101
+ return list(hs)
102
+ return True
103
+ elif logging:
104
+ return []
105
+ return False
106
+
107
+ def __find_delocalize_path(self: 'MoleculeContainer', start, finish, constrains, odd_only):
108
+ bonds = self._bonds
109
+ stack = [(start, n, 0, b.order + 1) for n, b in bonds[start].items() if n in constrains and b.order < 3]
110
+ path = []
111
+ seen = {start}
112
+ while stack:
113
+ last, current, depth, order = stack.pop()
114
+ if len(path) > depth:
115
+ seen.difference_update(x for _, x, _ in path[depth:])
116
+ path = path[:depth]
117
+
118
+ path.append((last, current, order))
119
+
120
+ if current in finish:
121
+ if odd_only: # radicals!
122
+ if len(path) % 2:
123
+ yield path
124
+ else: # invalid path
125
+ continue
126
+ elif depth: # one bonded ignored. we search double bond transfer! A=A-A >> A-A=A.
127
+ yield path
128
+
129
+ depth += 1
130
+ seen.add(current)
131
+ diff = -1 if depth % 2 else 1
132
+ stack.extend((current, n, depth, bo) for n, b in bonds[current].items()
133
+ if n not in seen and n in constrains and 1 <= (bo := b.order + diff) <= 3)
134
+
135
+ def __entries(self: 'MoleculeContainer'):
136
+ atoms = self._atoms
137
+ bonds = self._bonds
138
+ errors = {n for n, a in self.atoms() if a.implicit_hydrogens is None}
139
+
140
+ transfer = set()
141
+ entries = set()
142
+ exits = set()
143
+ rads = set()
144
+ nitrogen_cat = set()
145
+ nitrogen_ani = set()
146
+ sulfur_cat = set()
147
+ for n, a in self.atoms():
148
+ if a not in (B, C, N, O, Si, P, S, As, Se, Te):
149
+ # filter non-organic set, halogens and aromatics
150
+ continue
151
+ elif a.is_radical:
152
+ rads.add(n)
153
+ elif a.charge == -1:
154
+ if (lb := len(bonds[n])) == 4 and a == B: # skip boron
155
+ continue
156
+ elif lb == 6 and a == P: # skip [P-]X6
157
+ continue
158
+ if n in errors: # only valid anions accepted
159
+ continue
160
+ entries.add(n)
161
+ elif a.charge == 1:
162
+ lb = len(bonds[n])
163
+ if a == N:
164
+ if lb == 4: # skip ammonia
165
+ continue
166
+ elif lb == 2 and a.hybridization == 3: # skip Azide
167
+ (n1, b1), (n2, b2) = bonds[n].items()
168
+ an1 = atoms[n1]
169
+ an2 = atoms[n2]
170
+ if b1 == b2 == 2 and (an1.charge == -1 and an1 == N or an2.charge == -1 and an2 == N):
171
+ continue
172
+ elif lb == 3 and a.hybridization == 2: # X=[N+](-X)-X - prevent N-N migration
173
+ nitrogen_ani.add(n)
174
+ elif a == P and lb == 4: # skip [P+]R4
175
+ continue
176
+ elif a == S:
177
+ if lb == 2 and a.hybridization == 2: # ad-hoc for X-[S+]=X
178
+ sulfur_cat.add(n)
179
+ elif lb == 3 and a.hybridization == 1: # ad-hoc for X-[S+](-X)-X
180
+ continue
181
+ exits.add(n)
182
+ transfer.add(n)
183
+
184
+ if exits or entries: # try to move cation to nitrogen. saturation fixup.
185
+ for n, a in self.atoms():
186
+ if a == N and not a.charge:
187
+ if a.hybridization == 1 and a.neighbors <= 3: # any amine - potential e-donor
188
+ entries.add(n)
189
+ nitrogen_cat.add(n)
190
+ elif a.hybridization == 3 and a.neighbors == 1: # N#X-[X-] >> [N-]=X=X
191
+ exits.add(n)
192
+ nitrogen_ani.add(n)
193
+ return entries, exits, rads, transfer, nitrogen_cat, nitrogen_ani, sulfur_cat
194
+
195
+
196
+ __all__ = ['Resonance']