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,475 @@
1
+ # -*- coding: utf-8 -*-
2
+ #
3
+ # Copyright 2018-2026 Ramil Nugmanov <nougmanoff@protonmail.com>
4
+ # Copyright 2021 Dmitrij Zanadvornykh <zandmitrij@gmail.com>
5
+ # Copyright 2018 Tagir Akhmetshin <tagirshin@gmail.com>
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 ._groups import rules as fragment_rules
23
+ from ._isomers import rules as isomer_rules
24
+ from ._metal_organics import rules as metal_rules
25
+ from ...containers.bonds import Bond
26
+ from ...exceptions import ValenceError, ImplementationError
27
+ from ...periodictable import H as _H
28
+
29
+
30
+ H = 1
31
+
32
+
33
+ class Standardize:
34
+ __slots__ = ()
35
+
36
+ def canonicalize(self, *, fix_tautomers=True, keep_kekule=False,
37
+ ignore_pyrrole_hydrogen=False, buffer_size=7,
38
+ logging=False, ignore=True) -> bool | list:
39
+ """
40
+ Convert molecule to canonical forms of functional groups and aromatic rings without explicit hydrogens.
41
+
42
+ :param logging: return log.
43
+ :param ignore: ignore standardization bugs.
44
+ :param fix_tautomers: convert tautomers to canonical forms.
45
+ :param keep_kekule: return kekule form.
46
+ :param buffer_size: number of attempts of pyridine form searching.
47
+ :param ignore_pyrrole_hydrogen: ignore hydrogen on pyrrole to fix invalid rings like Cn1cc[nH]c1.
48
+ """
49
+ k = self.kekule(buffer_size=buffer_size, ignore_pyrrole_hydrogen=ignore_pyrrole_hydrogen)
50
+ s = self.standardize(logging=True, ignore=ignore, fix_tautomers=fix_tautomers)
51
+ h, changed = self.implicify_hydrogens(logging=True)
52
+
53
+ if logging or keep_kekule: # thiele can change tautomeric form
54
+ hgs = {n: a.implicit_hydrogens for n, a in self.atoms()}
55
+ if keep_kekule: # save bond orders
56
+ bonds = [(b, b.order) for _, _, b in self.bonds()]
57
+
58
+ t = self.thiele(fix_tautomers=fix_tautomers)
59
+ a = self.standardize_isomers(logging=True)
60
+
61
+ if keep_kekule and t: # restore
62
+ # check ring charge/hydrogen moving
63
+ if a or any(hgs[n] != aa.implicit_hydrogens for n, aa in self.atoms()):
64
+ self.kekule() # we need to do full kekule again
65
+ else:
66
+ for b, o in bonds: # noqa
67
+ b._order = o
68
+ self.flush_cache()
69
+ self.calc_labels()
70
+
71
+ if logging:
72
+ if k:
73
+ s.insert(0, ((), -1, 'kekulized'))
74
+ if h:
75
+ s.append((tuple(changed), -1, 'implicified'))
76
+ if t:
77
+ s.append(((), -1, 'aromatized'))
78
+ if fix_tautomers and (x := tuple(n for n, aa in self.atoms() if hgs[n] != aa.implicit_hydrogens)):
79
+ s.append((x, -1, 'aromatic tautomer found'))
80
+ if a:
81
+ s.append((tuple(a), -1, 'isomers standardized'))
82
+ if keep_kekule and t:
83
+ if a or any(hgs[n] != aa.implicit_hydrogens for n, aa in self.atoms()):
84
+ s.append(((), -1, 'kekulized again'))
85
+ else:
86
+ s.append(((), -1, 'kekule form restored'))
87
+ return s
88
+ return bool(k or s or h or t or a)
89
+
90
+ def standardize(self, *, logging=False, ignore=True, fix_tautomers=True) -> bool | list:
91
+ """
92
+ Standardize functional groups. Return True if any non-canonical group found.
93
+
94
+ :param fix_tautomers: convert tautomers to canonical forms.
95
+ :param logging: return list of fixed atoms with matched rules.
96
+ :param ignore: ignore standardization bugs.
97
+ """
98
+ log, fixed = [], set()
99
+
100
+ l, f = self.__standardize(fragment_rules, fix_tautomers)
101
+ log.extend(l)
102
+ fixed.update(f)
103
+ l, f = self.__standardize(metal_rules, fix_tautomers) # metal-organics fix
104
+ log.extend(l)
105
+ fixed.update(f)
106
+
107
+ if b := fixed.intersection(n for n, a in self.atoms() if a.implicit_hydrogens is None):
108
+ if ignore:
109
+ log.append((tuple(b), -1, 'standardization failed'))
110
+ else:
111
+ raise ImplementationError(f'standardization leads to invalid valences: {b}')
112
+
113
+ if fixed:
114
+ self.fix_stereo()
115
+
116
+ if logging:
117
+ if fixed:
118
+ log.append((tuple(fixed), -1, 'standardized atoms'))
119
+ return log
120
+ return bool(fixed)
121
+
122
+ def standardize_isomers(self, *, logging=False) -> bool | list:
123
+ """
124
+ Set canonical positions of charges and hydrogens.
125
+
126
+ :param logging: return list of changed atoms.
127
+ """
128
+ changed: list[int] = []
129
+ bonds = self._bonds
130
+ atoms = self._atoms
131
+
132
+ seen = set()
133
+ charge_morgan_pairs = []
134
+ fcr = []
135
+ tautomer_morgan_pairs = []
136
+ nitrogens = set()
137
+ doubles = set()
138
+ protonated = set()
139
+ carbons = set()
140
+
141
+ for q, rule_type in isomer_rules:
142
+ for mapping in q.get_mapping(self, automorphism_filter=False):
143
+ match = set(mapping.values())
144
+ if len(match.intersection(seen)) > 2:
145
+ continue
146
+ seen.update(match)
147
+
148
+ if rule_type == 'charge_fixed':
149
+ atom_1, atom_2 = mapping[1], mapping[2]
150
+ atoms[atom_1]._charge = 0
151
+ atoms[atom_2]._charge = 1
152
+ changed.append(atom_1)
153
+ changed.append(atom_2)
154
+
155
+ elif rule_type == 'charge_fixed_bridge':
156
+ atom_1, atom_2, atom_3 = mapping[1], mapping[2], mapping[3]
157
+ atoms[atom_3]._charge = 0
158
+ atoms[atom_2]._charge = 1
159
+ changed.append(atom_3)
160
+ changed.append(atom_2)
161
+
162
+ elif rule_type == 'tautomer_fixed':
163
+ atom_1, atom_2 = mapping[1], mapping[2]
164
+ atoms[atom_1]._implicit_hydrogens = 1
165
+ atoms[atom_2]._implicit_hydrogens = 0
166
+ changed.append(atom_1)
167
+ changed.append(atom_2)
168
+
169
+ elif rule_type == 'charge_morgan':
170
+ atom_1, atom_2 = mapping[1], mapping[2]
171
+ atoms[atom_1]._charge = 0
172
+ charge_morgan_pairs.append((atom_1, atom_2, atom_1))
173
+
174
+ elif rule_type == 'charge_morgan_bridge':
175
+ atom_1, atom_2, atom_3 = mapping[1], mapping[2], mapping[3]
176
+ atoms[atom_3]._charge = 0
177
+ charge_morgan_pairs.append((atom_1, atom_2, atom_3))
178
+
179
+ elif rule_type == 'ferrocene':
180
+ atoms[mapping[1]]._charge = 0
181
+ fcr.append([mapping[i] for i in range(1, 6)])
182
+ changed.append(mapping[1])
183
+
184
+ elif rule_type == 'tautomer_morgan':
185
+ atom_1, atom_2 = mapping[1], mapping[2]
186
+ atoms[atom_1]._implicit_hydrogens = 0
187
+ tautomer_morgan_pairs.append((atom_1, atom_2, atom_1))
188
+
189
+ elif rule_type == 'tautomer_morgan_donor':
190
+ atom_1, atom_2, atom_3 = mapping[1], mapping[2], mapping[3]
191
+ atoms[atom_3]._implicit_hydrogens = 0
192
+ tautomer_morgan_pairs.append((atom_1, atom_2, atom_3))
193
+
194
+ elif rule_type == 'amidine':
195
+ atom_1, atom_2 = mapping[1], mapping[2]
196
+ nitrogens.add(atom_2)
197
+ if atom_1 not in nitrogens:
198
+ atom_3 = mapping[3]
199
+ carbons.add(atom_3)
200
+ nitrogens.add(atom_1)
201
+ bonds[atom_1][atom_3]._order = 1
202
+ atoms[atom_1]._implicit_hydrogens += 1
203
+ doubles.add((atom_1, atom_3))
204
+ protonated.add(atom_1)
205
+
206
+ if charge_morgan_pairs or fcr or tautomer_morgan_pairs or carbons:
207
+ self.__dict__.pop('atoms_order', None)
208
+ self.__dict__.pop('int_adjacency', None)
209
+
210
+ for atom_1, atom_2, source in charge_morgan_pairs:
211
+ if self.atoms_order[atom_1] > self.atoms_order[atom_2]:
212
+ atoms[atom_2]._charge = 1
213
+ if source != atom_2:
214
+ changed.append(source)
215
+ changed.append(atom_2)
216
+ else:
217
+ atoms[atom_1]._charge = 1
218
+ if source != atom_1:
219
+ changed.append(source)
220
+ changed.append(atom_1)
221
+
222
+ for ca in fcr:
223
+ n = min(ca, key=self.atoms_order.get)
224
+ atoms[n]._charge = -1
225
+ changed.append(n)
226
+
227
+ for atom_1, atom_2, source in tautomer_morgan_pairs:
228
+ if self.atoms_order[atom_1] >= self.atoms_order[atom_2]:
229
+ atoms[atom_1]._implicit_hydrogens = 1
230
+ if source != atom_1:
231
+ changed.append(source)
232
+ changed.append(atom_1)
233
+ else:
234
+ atoms[atom_2]._implicit_hydrogens = 1
235
+ if source != atom_2:
236
+ changed.append(source)
237
+ changed.append(atom_2)
238
+
239
+ if carbons:
240
+ nitrogens_sorted = sorted(nitrogens, key=self.atoms_order.get)
241
+ while nitrogens_sorted:
242
+ atom_1 = nitrogens_sorted.pop()
243
+ for atom_3 in sorted(bonds[atom_1], key=self.atoms_order.get):
244
+ if atom_3 in carbons:
245
+ carbons.discard(atom_3)
246
+ atoms[atom_1]._implicit_hydrogens -= 1
247
+ bonds[atom_1][atom_3]._order = 2
248
+ if (atom_1, atom_3) not in doubles:
249
+ changed.append(atom_1)
250
+ changed.append(atom_3)
251
+ break
252
+ else:
253
+ if atom_1 in protonated:
254
+ changed.append(atom_1)
255
+
256
+ del self.__dict__['atoms_order']
257
+ del self.__dict__['int_adjacency']
258
+
259
+ if changed:
260
+ self.flush_cache(keep_sssr=True, keep_components=True, keep_special_connectivity=True)
261
+ self.fix_stereo()
262
+ if logging:
263
+ return changed
264
+ return True
265
+ if logging:
266
+ return []
267
+ return False
268
+
269
+ def remove_coordinate_bonds(self, *, keep_to_terminal=True) -> int:
270
+ """Remove coordinate (or hydrogen) bonds marked with 8 (any) bond
271
+
272
+ :param keep_to_terminal: Keep any bonds to terminal hydrogens
273
+ :return: removed bonds count
274
+ """
275
+ ab = [(n, m) for n, m, b in self.bonds() if b == 8]
276
+
277
+ if keep_to_terminal:
278
+ skeleton = self.not_special_connectivity
279
+ hs = {n for n, a in self.atoms() if a == H and not skeleton[n]}
280
+ ab = [(n, m) for n, m in ab if n not in hs and m not in hs]
281
+
282
+ for n, m in ab:
283
+ self.delete_bond(n, m, _skip_calculation=True)
284
+
285
+ if ab:
286
+ self.flush_cache(keep_sssr=True, keep_special_connectivity=True)
287
+ self.calc_labels()
288
+ self.fix_stereo()
289
+ return len(ab)
290
+
291
+ def implicify_hydrogens(self, *, logging=False) -> int | tuple:
292
+ """
293
+ Remove explicit hydrogen if possible. Return number of removed hydrogens.
294
+ Works only with Kekule forms of aromatic structures.
295
+ Keeps isotopes of hydrogen.
296
+
297
+ :param logging: return list of changed atoms.
298
+ """
299
+ atoms = self._atoms
300
+ bonds = self._bonds
301
+
302
+ explicit = defaultdict(list)
303
+ for n, atom in atoms.items():
304
+ if atom == H and (atom.isotope is None or atom.isotope == 1):
305
+ if len(bonds[n]) > 1:
306
+ raise ValenceError(f'Hydrogen atom {n} has invalid valence. Try to use remove_coordinate_bonds()')
307
+ for m, b in bonds[n].items():
308
+ if b == 1:
309
+ if atoms[m] != H: # not H-H
310
+ explicit[m].append(n)
311
+ elif b != 8:
312
+ raise ValenceError(f'Hydrogen atom {n} has invalid valence {b.order}.')
313
+
314
+ to_remove = set()
315
+ fixed = {}
316
+ for n, hs in explicit.items():
317
+ atom = atoms[n]
318
+ len_h = len(hs)
319
+ for i in range(len_h, 0, -1):
320
+ hi = hs[:i]
321
+ explicit_sum = 0
322
+ explicit_dict = defaultdict(int)
323
+ for m, bond in bonds[n].items():
324
+ if m not in hi and bond != 8:
325
+ explicit_sum += bond.order
326
+ explicit_dict[(bond.order, atoms[m].atomic_number)] += 1
327
+ try:
328
+ # aromatic rings don't match any rule
329
+ rules = atom.valence_rules(explicit_sum)
330
+ except ValenceError:
331
+ break
332
+ for s, d, h in rules:
333
+ if s.issubset(explicit_dict) and all(explicit_dict[k] >= c for k, c in d.items()) and h >= i:
334
+ to_remove.update(hi)
335
+ fixed[n] = h
336
+ break
337
+ else:
338
+ continue
339
+ break
340
+
341
+ for n in to_remove:
342
+ del atoms[n]
343
+ for m in bonds.pop(n):
344
+ del bonds[m][n]
345
+
346
+ for n, h in fixed.items():
347
+ atoms[n]._implicit_hydrogens = h
348
+
349
+ if to_remove:
350
+ self.flush_cache(keep_sssr=True)
351
+ self.calc_labels()
352
+ self.fix_stereo()
353
+
354
+ if logging:
355
+ return len(to_remove), list(fixed)
356
+ return len(to_remove)
357
+
358
+ def explicify_hydrogens(self, *, start_map=None, _return_map=False) -> int | list:
359
+ """
360
+ Add explicit hydrogens to atoms.
361
+
362
+ :return: number of added atoms
363
+ """
364
+ atoms = self._atoms
365
+ to_add = []
366
+ for n, a in atoms.items():
367
+ try:
368
+ to_add.extend([n] * a.implicit_hydrogens)
369
+ except TypeError:
370
+ raise ValenceError(f'atom {n} has valence error')
371
+
372
+ if to_add:
373
+ log = []
374
+ bonds = self._bonds
375
+ m = start_map if start_map is not None else max(atoms) + 1
376
+ for n in to_add:
377
+ atoms[m] = _H(implicit_hydrogens=0)
378
+ bonds[n][m] = b = Bond(1)
379
+ bonds[m] = {n: b}
380
+ atoms[n]._implicit_hydrogens = 0
381
+ log.append((n, m))
382
+ m += 1
383
+
384
+ self.flush_cache(keep_sssr=True)
385
+ self.calc_labels()
386
+ self.fix_stereo()
387
+ if _return_map:
388
+ return log
389
+ return len(to_add)
390
+ elif _return_map:
391
+ return []
392
+ return 0
393
+
394
+ def check_valence(self) -> list[int]:
395
+ """
396
+ Check valences of all atoms.
397
+
398
+ :return: list of invalid atoms
399
+ """
400
+ # only invalid atoms have None hydrogens.
401
+ return [n for n, a in self.atoms() if a.implicit_hydrogens is None]
402
+
403
+ def clean_isotopes(self) -> bool:
404
+ """
405
+ Clean isotope marks from molecule.
406
+ Return True if any isotope found.
407
+ """
408
+ isotopes = [a for _, a in self.atoms() if a.isotope]
409
+ if isotopes:
410
+ for i in isotopes:
411
+ i._isotope = None
412
+ self.flush_cache(keep_sssr=True, keep_components=True, keep_special_connectivity=True)
413
+ self.fix_stereo()
414
+ return True
415
+ return False
416
+
417
+ def __standardize(self, rules, fix_tautomers):
418
+ atoms = self._atoms
419
+ bonds = self._bonds
420
+
421
+ log = []
422
+ fixed = set()
423
+ for r, (pattern, atom_fix, bonds_fix, any_atoms, is_tautomer) in enumerate(rules):
424
+ if not fix_tautomers and is_tautomer:
425
+ continue
426
+ keep_sssr = keep_components = True
427
+ hs = set()
428
+ seen = set()
429
+ for mapping in pattern.get_mapping(self, automorphism_filter=False):
430
+ match = set(mapping.values())
431
+ if not match.isdisjoint(seen): # skip intersected groups
432
+ continue
433
+ if any_atoms: # accept overlapping of Any-atoms
434
+ seen.update(match - {mapping[n] for n in any_atoms})
435
+ else:
436
+ seen.update(match)
437
+ for n, (ch, ir) in atom_fix.items():
438
+ n = mapping[n]
439
+ hs.add(n)
440
+ a = atoms[n]
441
+ a._charge += ch
442
+ if a.charge > 4:
443
+ a._charge -= ch
444
+ log.append((tuple(match), r, f'bad charge formed. changes omitted: {pattern}'))
445
+ break # skip changes
446
+ if ir is not None:
447
+ a._is_radical = ir
448
+ else:
449
+ for n, m, bo in bonds_fix:
450
+ n = mapping[n]
451
+ m = mapping[m]
452
+ hs.add(n)
453
+ hs.add(m)
454
+ if m in bonds[n]:
455
+ b = bonds[n][m]
456
+ if b == 8 or bo == 8:
457
+ keep_sssr = False
458
+ b._order = bo
459
+ else: # new bond
460
+ keep_sssr = keep_components = False
461
+ bonds[n][m] = bonds[m][n] = Bond(bo)
462
+ log.append((tuple(match), r, str(pattern)))
463
+
464
+ if not hs: # not matched
465
+ continue
466
+ self.flush_cache(keep_sssr=keep_sssr, keep_components=keep_components, keep_special_connectivity=keep_sssr)
467
+ # recalculate isomorphism labels
468
+ self.calc_labels()
469
+ for n in hs: # hydrogens count recalculation
470
+ self.calc_implicit(n)
471
+ fixed.update(hs)
472
+ return log, fixed
473
+
474
+
475
+ __all__ = ['Standardize']