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,384 @@
1
+ # -*- coding: utf-8 -*-
2
+ #
3
+ # Copyright 2018-2026 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 ._reagents import *
23
+ from ...exceptions import MappingError
24
+
25
+
26
+ class StandardizeReaction:
27
+ __slots__ = ()
28
+
29
+ def canonicalize(self, *, fix_mapping: bool = True, logging=False, fix_tautomers=True) -> bool | list:
30
+ """
31
+ Convert molecules to canonical forms of functional groups and aromatic rings without explicit hydrogens.
32
+ Return True if in any molecule found not canonical group.
33
+
34
+ :param fix_mapping: Search AAM errors of functional groups.
35
+ :param logging: return processing log.
36
+ :param fix_tautomers: convert tautomers to canonical forms.
37
+ """
38
+ total = []
39
+ for n, m in enumerate(self.molecules()):
40
+ total.extend((n, x) for x in m.canonicalize(logging=True, fix_tautomers=fix_tautomers))
41
+
42
+ if fix_mapping:
43
+ total.extend(self.fix_mapping(logging=True))
44
+
45
+ if total:
46
+ self.flush_cache(keep_molecule_cache=True)
47
+ if logging:
48
+ return total
49
+ return bool(total)
50
+
51
+ def thiele(self, *, fix_tautomers=True) -> bool:
52
+ """
53
+ Convert structures to aromatic form.
54
+ Return True if in any molecule found kekule ring
55
+
56
+ :param fix_tautomers: convert tautomers to canonical forms.
57
+ """
58
+ total = False
59
+ for m in self.molecules():
60
+ if m.thiele(fix_tautomers=fix_tautomers):
61
+ total = True
62
+ if total:
63
+ self.flush_cache(keep_molecule_cache=True)
64
+ return total
65
+
66
+ def kekule(self, *, buffer_size=7, ignore_pyrrole_hydrogen=False) -> bool:
67
+ """
68
+ Convert structures to a kekule form.
69
+ Return True if in any molecule found aromatic ring
70
+
71
+ :param buffer_size: number of attempts of pyridine form searching.
72
+ :param ignore_pyrrole_hydrogen: ignore hydrogen on pyrrole to fix invalid rings like Cn1cc[nH]c1.
73
+ """
74
+ total = False
75
+ for m in self.molecules():
76
+ if m.kekule(buffer_size=buffer_size, ignore_pyrrole_hydrogen=ignore_pyrrole_hydrogen):
77
+ total = True
78
+ if total:
79
+ self.flush_cache(keep_molecule_cache=True)
80
+ return total
81
+
82
+ def clean_isotopes(self) -> bool:
83
+ """
84
+ Clean isotope marks for all molecules in reaction.
85
+ Returns True if in any molecule found isotope.
86
+ """
87
+ flag = False
88
+ for m in self.molecules():
89
+ if m.clean_isotopes():
90
+ flag = True
91
+ if flag:
92
+ self.flush_cache(keep_molecule_cache=True)
93
+ return flag
94
+
95
+ def clean_stereo(self):
96
+ """
97
+ Remove stereo data
98
+ """
99
+ for m in self.molecules():
100
+ m.clean_stereo()
101
+ self.flush_cache(keep_molecule_cache=True)
102
+
103
+ def implicify_hydrogens(self) -> int:
104
+ """
105
+ Remove explicit hydrogens if possible.
106
+
107
+ :return: number of removed hydrogens.
108
+ """
109
+ total = 0
110
+ for m in self.molecules():
111
+ total += m.implicify_hydrogens()
112
+ if total:
113
+ self.flush_cache(keep_molecule_cache=True)
114
+ return total
115
+
116
+ def explicify_hydrogens(self) -> int:
117
+ """
118
+ Add explicit hydrogens to atoms
119
+
120
+ :return: number of added atoms
121
+ """
122
+ total = 0
123
+ start_map = 0
124
+ for m in self.molecules():
125
+ map_ = max(m, default=0)
126
+ if map_ > start_map:
127
+ start_map = map_
128
+
129
+ mapping = defaultdict(list)
130
+ for m in self.reactants:
131
+ maps = m.explicify_hydrogens(_return_map=True, start_map=start_map + 1)
132
+ if maps:
133
+ for n, h in maps:
134
+ mapping[n].append(h)
135
+ start_map = maps[-1][1]
136
+ total += len(maps)
137
+
138
+ for m in self.reagents:
139
+ maps = m.explicify_hydrogens(_return_map=True, start_map=start_map + 1)
140
+ if maps:
141
+ start_map = maps[-1][1]
142
+ total += len(maps)
143
+
144
+ for m in self.products:
145
+ maps = m.explicify_hydrogens(_return_map=True, start_map=start_map + 1)
146
+ if maps:
147
+ total += len(maps)
148
+ remap = {}
149
+ free = []
150
+ for n, h in maps:
151
+ if n in mapping and mapping[n]:
152
+ remap[h] = mapping[n].pop()
153
+ free.append(h)
154
+ elif free:
155
+ remap[h] = start_map = free.pop(0)
156
+ else:
157
+ start_map = h
158
+ m.remap(remap)
159
+
160
+ if total:
161
+ self.flush_cache(keep_molecule_cache=True)
162
+ return total
163
+
164
+ def remove_reagents(self, *, keep_reagents: bool = False, mapping: bool = True) -> bool:
165
+ """
166
+ Place molecules, except reactants, to reagents list. Reagents - molecules which atoms not presented in products.
167
+ Mapping based approach remove molecules without reaction center.
168
+ Rule based approach remove equal molecules in reactants and products, and predefined reactants.
169
+
170
+ :param mapping: use atom-to-atom mapping to detect reagents, otherwise use predefined list of common reagents.
171
+ :param keep_reagents: delete reagents if False
172
+
173
+ Return True if any reagent found.
174
+ """
175
+ if mapping:
176
+ return self.__remove_reagents_mapping(keep_reagents)
177
+ return self.__remove_reagents_rules(keep_reagents)
178
+
179
+ def __remove_reagents_rules(self, keep_reagents):
180
+ if not self.reactants or not self.products: # there is no reaction
181
+ return False
182
+
183
+ reactants_st1 = []
184
+ products_st1 = []
185
+ reagents_st1 = set(self.reagents)
186
+
187
+ # find the same molecules in reactants and products
188
+ for m in self.reactants:
189
+ if m in self.products:
190
+ reagents_st1.add(m)
191
+ else:
192
+ reactants_st1.append(m)
193
+ for m in self.products:
194
+ if m in self.reactants:
195
+ reagents_st1.add(m)
196
+ else:
197
+ products_st1.append(m)
198
+ if not reactants_st1 or not products_st1:
199
+ return False # keep bad reaction as is
200
+
201
+ reactants_st2 = []
202
+ products_st2 = []
203
+ reagents_st2 = reagents_st1.copy()
204
+
205
+ # filter out predefined reagents
206
+ for m in reactants_st1:
207
+ if m in reagents_set:
208
+ reagents_st2.add(m)
209
+ else:
210
+ reactants_st2.append(m)
211
+ for m in products_st1:
212
+ if m in reagents_set:
213
+ reagents_st2.add(m)
214
+ else:
215
+ products_st2.append(m)
216
+ if not reactants_st2 or not products_st2: # reaction contains only simple molecules. roll-back to step 1
217
+ reactants_st2 = reactants_st1
218
+ products_st2 = products_st1
219
+ reagents_st2 = reagents_st1
220
+
221
+ # remove reagents from reactants
222
+ tmp = []
223
+ for m in self.reagents:
224
+ tmp.append(m)
225
+ if m in reagents_st2:
226
+ reagents_st2.discard(m)
227
+ tmp.extend(reagents_st2)
228
+ reagents = tuple(tmp) if keep_reagents else ()
229
+
230
+ self._reactants = tuple(reactants_st2)
231
+ self._products = tuple(products_st2)
232
+ self._reagents = reagents
233
+ self.fix_positions()
234
+ return True
235
+
236
+ def __remove_reagents_mapping(self, keep_reagents):
237
+ cgr = ~self
238
+ if cgr.center_atoms:
239
+ active = set(cgr.center_atoms)
240
+ reactants = []
241
+ products = []
242
+ reagents = set(self.reagents)
243
+ for i in self.reactants:
244
+ if not active.isdisjoint(i):
245
+ reactants.append(i)
246
+ else:
247
+ reagents.add(i)
248
+ for i in self.products:
249
+ if not active.isdisjoint(i):
250
+ products.append(i)
251
+ else:
252
+ reagents.add(i)
253
+
254
+ # remove reagents from reactants
255
+ tmp = []
256
+ for m in self.reagents:
257
+ tmp.append(m)
258
+ if m in reagents:
259
+ reagents.discard(m)
260
+ tmp.extend(reagents)
261
+ reagents = tuple(tmp) if keep_reagents else ()
262
+
263
+ if len(reactants) != len(self.reactants) or len(products) != len(self.products) or len(reagents) != len(self.reagents):
264
+ self._reactants = tuple(reactants)
265
+ self._products = tuple(products)
266
+ self._reagents = reagents
267
+ self.fix_positions()
268
+ return True
269
+ return False
270
+ raise MappingError("Reaction center is absent according to mapping")
271
+
272
+ def contract_ions(self) -> bool:
273
+ """
274
+ Contract ions into salts (Molecules with disconnected components).
275
+ Note: works only for unambiguous cases. e.g. equal anions/cations and different or equal cations/anions.
276
+
277
+ Return True if any ions contracted.
278
+ """
279
+ neutral, cations, anions, total = _sift_ions(self.reagents)
280
+ salts = _contract_ions(anions, cations, total)
281
+ if salts:
282
+ neutral.extend(salts)
283
+ self._reagents = tuple(neutral)
284
+ changed = True
285
+ else:
286
+ changed = False
287
+
288
+ neutral, cations, anions, total = _sift_ions(self.reactants)
289
+ salts = _contract_ions(anions, cations, total)
290
+ if salts:
291
+ anions_order = {frozenset(m): n for n, m in enumerate(anions)}
292
+ cations_order = {frozenset(m): n for n, m in enumerate(cations)}
293
+ neutral.extend(salts)
294
+ self._reactants = tuple(neutral)
295
+ changed = True
296
+ else:
297
+ anions_order = cations_order = {}
298
+
299
+ neutral, cations, anions, total = _sift_ions(self.products)
300
+ if cations and anions:
301
+ anions.sort(key=lambda x: anions_order.get(frozenset(x), -1))
302
+ cations.sort(key=lambda x: cations_order.get(frozenset(x), -1))
303
+ salts = _contract_ions(anions, cations, total)
304
+ if salts:
305
+ neutral.extend(salts)
306
+ self._products = tuple(neutral)
307
+ changed = True
308
+
309
+ if changed:
310
+ self.fix_positions()
311
+ return True
312
+ return False
313
+
314
+
315
+ def _sift_ions(mols):
316
+ anions = []
317
+ cations = []
318
+ neutral = []
319
+ total = 0
320
+ for m in mols:
321
+ c = int(m)
322
+ total += c
323
+ if c > 0:
324
+ cations.append(m)
325
+ elif c < 0:
326
+ anions.append(m)
327
+ else:
328
+ neutral.append(m)
329
+ return neutral, cations, anions, total
330
+
331
+
332
+ def _contract_ions(anions, cations, total):
333
+ if not anions or not cations: # nothing to contract
334
+ return
335
+ # check ambiguous cases
336
+ if total > 0:
337
+ if len(cations) > 1: # deficit of anions
338
+ # we have an excess of cations. we can't assign anions univocally
339
+ return # unite is ambiguous
340
+ salt = cations[0]
341
+ shift_x = salt._fix_plane_mean(0) + 1
342
+ for x in anions:
343
+ shift_x = x._fix_plane_mean(shift_x) + 1
344
+ salt = salt | x
345
+ return [salt]
346
+ elif total < 0:
347
+ if len(anions) > 1: # deficit of cations
348
+ # we have an excess of anions. we can't assign cations univocally
349
+ return # unite is ambiguous
350
+ salt = anions[0]
351
+ shift_x = salt._fix_plane_mean(0) + 1
352
+ for x in cations:
353
+ shift_x = x._fix_plane_mean(shift_x) + 1
354
+ salt = salt | x
355
+ return [salt]
356
+ elif len(set(anions)) > 1 and len(set(cations)) > 1: # different anions and cations
357
+ return
358
+
359
+ salts = []
360
+ anions = anions.copy()
361
+ cations = cations.copy()
362
+ while anions:
363
+ ct = cations.pop()
364
+ an = anions.pop()
365
+ shift_x = ct._fix_plane_mean(0) + 1
366
+ shift_x = an._fix_plane_mean(shift_x) + 1
367
+ salt = ct | an
368
+ while True:
369
+ c = int(salt)
370
+ if c > 0:
371
+ an = anions.pop()
372
+ shift_x = an._fix_plane_mean(shift_x) + 1
373
+ salt = salt | an
374
+ elif c < 0:
375
+ ct = cations.pop()
376
+ shift_x = ct._fix_plane_mean(shift_x) + 1
377
+ salt = salt | ct
378
+ else:
379
+ break
380
+ salts.append(salt)
381
+ return salts
382
+
383
+
384
+ __all__ = ['StandardizeReaction']
@@ -0,0 +1,191 @@
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 ...exceptions import ValenceError
20
+
21
+
22
+ # atomic number constants
23
+ B = 5
24
+ C = 6
25
+ N = 7
26
+ O = 8
27
+ Si = 14
28
+ P = 15
29
+ S = 16
30
+ As = 33
31
+ Se = 34
32
+ Te = 52
33
+
34
+
35
+ class Resonance:
36
+ __slots__ = ()
37
+
38
+ def fix_resonance(self, *, logging=False) -> bool | list:
39
+ """
40
+ Transform biradical or dipole resonance structures into neutral form. Return True if structure form changed.
41
+
42
+ :param logging: return list of changed atoms.
43
+ """
44
+ atoms = self._atoms
45
+ bonds = self._bonds
46
+ entries, exits, rads, constrains, nitrogen_cat, nitrogen_ani, sulfur_cat = self.__entries()
47
+ hs = set()
48
+ while len(rads) > 1:
49
+ n = rads.pop()
50
+ for path in self.__find_delocalize_path(n, rads, constrains, True):
51
+ atoms[n]._is_radical = False
52
+ hs.add(n)
53
+ for n, m, b in path:
54
+ hs.add(m)
55
+ bonds[n][m]._order = b
56
+ atoms[m]._is_radical = False # noqa
57
+ rads.discard(m)
58
+ break # path found
59
+ # path not found. atom n keep as is
60
+ while entries and exits:
61
+ n = entries.pop()
62
+ for path in self.__find_delocalize_path(n, exits, constrains, False):
63
+ l, m, b = path[-1]
64
+ if n in nitrogen_cat and m in nitrogen_ani:
65
+ continue
66
+
67
+ if m in sulfur_cat: # prevent X-[S+]=X >> X=S=X
68
+ if b != 1:
69
+ continue
70
+ atoms[m]._charge -= 1
71
+ else: # check cations end valence.
72
+ atoms[m]._charge -= 1 # reduce atom change and check valence
73
+ try:
74
+ atoms[m].valence_rules(sum(int(y) for x, y in bonds[m].items() if x != l) + b)
75
+ except ValenceError:
76
+ atoms[m]._charge += 1 # roll back
77
+ continue
78
+
79
+ # succeed!
80
+ atoms[n]._charge += 1
81
+ hs.add(n)
82
+ for n, m, b in path:
83
+ hs.add(m)
84
+ bonds[n][m]._order = b
85
+ exits.discard(m)
86
+ break # path from negative atom to positive atom found.
87
+ # path not found. keep negative atom n as is
88
+ if hs:
89
+ for n in hs:
90
+ self.calc_implicit(n)
91
+ self.flush_cache(keep_sssr=True, keep_components=True, keep_special_connectivity=True)
92
+ self.fix_stereo()
93
+ if logging:
94
+ return list(hs)
95
+ return True
96
+ elif logging:
97
+ return []
98
+ return False
99
+
100
+ def __find_delocalize_path(self, start, finish, constrains, odd_only):
101
+ bonds = self._bonds
102
+ stack = [(start, n, 0, b.order + 1) for n, b in bonds[start].items() if n in constrains and b.order < 3]
103
+ path = []
104
+ seen = {start}
105
+ while stack:
106
+ last, current, depth, order = stack.pop()
107
+ if len(path) > depth:
108
+ seen.difference_update(x for _, x, _ in path[depth:])
109
+ path = path[:depth]
110
+
111
+ path.append((last, current, order))
112
+
113
+ if current in finish:
114
+ if odd_only: # radicals!
115
+ if len(path) % 2:
116
+ yield path
117
+ else: # invalid path
118
+ continue
119
+ elif depth: # one bonded ignored. we search double bond transfer! A=A-A >> A-A=A.
120
+ yield path
121
+
122
+ depth += 1
123
+ seen.add(current)
124
+ diff = -1 if depth % 2 else 1
125
+ stack.extend((current, n, depth, bo) for n, b in bonds[current].items()
126
+ if n not in seen and n in constrains and 1 <= (bo := b.order + diff) <= 3)
127
+
128
+ def __entries(self):
129
+ atoms = self._atoms
130
+ bonds = self._bonds
131
+ errors = {n for n, a in self.atoms() if a.implicit_hydrogens is None}
132
+
133
+ transfer = set()
134
+ entries = set()
135
+ exits = set()
136
+ rads = set()
137
+ nitrogen_cat = set()
138
+ nitrogen_ani = set()
139
+ sulfur_cat = set()
140
+ for n, a in self.atoms():
141
+ if a not in (B, C, N, O, Si, P, S, As, Se, Te):
142
+ # filter non-organic set, halogens and aromatics
143
+ continue
144
+ elif a.is_radical:
145
+ rads.add(n)
146
+ elif a.charge == -1:
147
+ if a == B and a.total_hydrogens: # skip [BHx-]
148
+ continue
149
+ elif (lb := len(bonds[n])) == 4 and a == B: # skip [B-]X4
150
+ continue
151
+ elif lb == 6 and a == P: # skip [P-]X6
152
+ continue
153
+ if n in errors: # only valid anions accepted
154
+ continue
155
+ entries.add(n)
156
+ elif a.charge == 1:
157
+ lb = len(bonds[n])
158
+ if a == N:
159
+ if lb == 4: # skip ammonia
160
+ continue
161
+ elif lb == 2 and a.hybridization == 3: # skip Azide
162
+ (n1, b1), (n2, b2) = bonds[n].items()
163
+ an1 = atoms[n1]
164
+ an2 = atoms[n2]
165
+ if b1 == b2 == 2 and (an1.charge == -1 and an1 == N or an2.charge == -1 and an2 == N):
166
+ continue
167
+ elif lb == 3 and a.hybridization == 2: # X=[N+](-X)-X - prevent N-N migration
168
+ nitrogen_ani.add(n)
169
+ elif a == P and lb == 4: # skip [P+]R4
170
+ continue
171
+ elif a == S:
172
+ if lb == 2 and a.hybridization == 2: # ad-hoc for X-[S+]=X
173
+ sulfur_cat.add(n)
174
+ elif lb == 3 and a.hybridization == 1: # ad-hoc for X-[S+](-X)-X
175
+ continue
176
+ exits.add(n)
177
+ transfer.add(n)
178
+
179
+ if exits or entries: # try to move cation to nitrogen. saturation fixup.
180
+ for n, a in self.atoms():
181
+ if a == N and not a.charge:
182
+ if a.hybridization == 1 and a.neighbors <= 3: # any amine - potential e-donor
183
+ entries.add(n)
184
+ nitrogen_cat.add(n)
185
+ elif a.hybridization == 3 and a.neighbors == 1: # N#X-[X-] >> [N-]=X=X
186
+ exits.add(n)
187
+ nitrogen_ani.add(n)
188
+ return entries, exits, rads, transfer, nitrogen_cat, nitrogen_ani, sulfur_cat
189
+
190
+
191
+ __all__ = ['Resonance']
@@ -0,0 +1,128 @@
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 ._salts import acids, rules
20
+
21
+
22
+ # atomic number constants
23
+ H = 1
24
+ N = 7
25
+ s_metals = {3, 4, 11, 12, 19, 20, 37, 38, 55, 56, 87, 88}
26
+ s_metals_ammonia = {3, 4, 7, 11, 12, 19, 20, 37, 38, 55, 56, 87, 88}
27
+
28
+
29
+ class Salts:
30
+ __slots__ = ()
31
+
32
+ def remove_metals(self, *, skip_elements: list[int] = None, logging=False) -> bool | list:
33
+ """
34
+ Remove disconnected S-metals and ammonia.
35
+
36
+ :param logging: return deleted atoms list.
37
+ :param skip_elements: skip elements from removing.
38
+ """
39
+ atoms = self._atoms
40
+ bonds = self._bonds
41
+ filters = s_metals_ammonia.difference(skip_elements) if skip_elements else s_metals_ammonia
42
+
43
+ metals = []
44
+ for n, a in atoms.items():
45
+ if not bonds[n] and a.atomic_number in filters:
46
+ metals.append(n)
47
+
48
+ if 0 < len(metals) < len(self):
49
+ for n in metals:
50
+ del atoms[n]
51
+ del bonds[n]
52
+
53
+ self.flush_cache(keep_sssr=True)
54
+ if logging:
55
+ return metals
56
+ return True
57
+ elif logging:
58
+ return []
59
+ return False
60
+
61
+ def remove_acids(self, *, logging=False) -> bool | list:
62
+ """
63
+ Remove common acids from organic bases salts.
64
+ Works only for neutral pairs like HA+B. Use `neutralize` before.
65
+
66
+ :param logging: return deleted atoms list.
67
+ """
68
+ if self.connected_components_count > 1:
69
+ log = []
70
+ for c in self.connected_components:
71
+ if self.substructure(c, recalculate_hydrogens=False) in acids:
72
+ log.extend(c)
73
+ if 0 < len(log) < len(self): # prevent singularity
74
+ atoms = self._atoms
75
+ bonds = self._bonds
76
+
77
+ for n in log:
78
+ del atoms[n]
79
+ del bonds[n]
80
+
81
+ self.flush_cache()
82
+ if logging:
83
+ return log
84
+ return True
85
+ if logging:
86
+ return []
87
+ return False
88
+
89
+ def split_metal_salts(self, *, skip_elements: list[int] = None, logging=False) -> bool | list:
90
+ """
91
+ Split connected S-metal salts to cation/anion pairs.
92
+
93
+ :param logging: return deleted bonds list.
94
+ :param skip_elements: skip elements from splitting.
95
+ """
96
+ atoms = self._atoms
97
+ bonds = self._bonds
98
+ filters = s_metals.difference(skip_elements) if skip_elements else s_metals
99
+
100
+ metals = [n for n, a in atoms.items() if a.atomic_number in filters]
101
+ if metals:
102
+ acceptors = set()
103
+ log = []
104
+ for q in rules:
105
+ for mapping in q.get_mapping(self, automorphism_filter=False):
106
+ acceptors.add(mapping[1])
107
+
108
+ for n in metals:
109
+ for m in acceptors & bonds[n].keys():
110
+ if atoms[n].charge == 4: # prevent overcharging
111
+ break
112
+ del bonds[n][m]
113
+ del bonds[m][n]
114
+ atoms[n]._charge += 1
115
+ atoms[m]._charge -= 1
116
+ log.append((n, m))
117
+ if log:
118
+ self.flush_cache()
119
+ self.fix_stereo()
120
+ if logging:
121
+ return log
122
+ return True
123
+ if logging:
124
+ return []
125
+ return False
126
+
127
+
128
+ __all__ = ['Salts']