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,817 @@
1
+ # -*- coding: utf-8 -*-
2
+ #
3
+ # Copyright 2017-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 collections import Counter, defaultdict
20
+ from functools import cached_property
21
+ from lazy_object_proxy import Proxy
22
+ from numpy import uint, zeros
23
+ from typing import Dict, Iterable, List, Tuple, Union
24
+ from zlib import compress, decompress
25
+ from .bonds import Bond, DynamicBond
26
+ from .cgr import CGRContainer
27
+ from .chimera import Chimera
28
+ from .graph import Graph
29
+ from .rdkit import RDkit
30
+ from ..algorithms.aromatics import Aromatize
31
+ from ..algorithms.calculate2d import Calculate2DMolecule
32
+ from ..algorithms.conformers import Conformers
33
+ from ..algorithms.depict import DepictMolecule
34
+ from ..algorithms.groups import FunctionalGroups
35
+ from ..algorithms.isomorphism import MoleculeIsomorphism
36
+ from ..algorithms.fingerprints import Fingerprints
37
+ from ..algorithms.mcs import MCS
38
+ from ..algorithms.morgan import Morgan
39
+ from ..algorithms.rings import Rings
40
+ from ..algorithms.smiles import MoleculeSmiles
41
+ from ..algorithms.standardize import StandardizeMolecule
42
+ from ..algorithms.stereo import MoleculeStereo
43
+ from ..algorithms.tautomers import Tautomers
44
+ from ..algorithms.x3dom import X3domMolecule
45
+ from ..exceptions import ValenceError
46
+ from ..periodictable import DynamicElement, Element, H as _H
47
+
48
+
49
+ def _rotable_rules():
50
+ from .. import smarts
51
+
52
+ w = smarts('[A;D2,D3,D4]-;!@[A;D2,D3,D4]')
53
+ b = [smarts('[N;D2,D3]-;!@[C,S;D2,D3]=[O,N]'), smarts('[N;D2,D3]-;!@[S;D4](=[O,N])=[O,N]')]
54
+ return w, b
55
+
56
+
57
+ rotable_rules = Proxy(_rotable_rules)
58
+
59
+ # atomic number constants
60
+ H = 1
61
+ C = 6
62
+ N = 7
63
+ O = 8
64
+ S = 16
65
+
66
+
67
+ class MoleculeContainer(MoleculeStereo, Graph[Element, Bond], Morgan, Rings, MoleculeIsomorphism,
68
+ Aromatize, StandardizeMolecule, MoleculeSmiles, DepictMolecule, Calculate2DMolecule,
69
+ Conformers, Fingerprints, Tautomers, RDkit, Chimera, MCS, X3domMolecule, FunctionalGroups):
70
+ __slots__ = ('_meta', '_name', '_conformers', '_changed', '_backup')
71
+
72
+ def __init__(self):
73
+ super().__init__()
74
+ self._meta = None
75
+ self._name = None
76
+ self._changed = None
77
+ self._backup = None
78
+
79
+ @property
80
+ def meta(self) -> Dict:
81
+ if self._meta is None:
82
+ self._meta = {} # lazy
83
+ return self._meta
84
+
85
+ @property
86
+ def name(self) -> str:
87
+ return self._name or ''
88
+
89
+ @name.setter
90
+ def name(self, name):
91
+ if not isinstance(name, str):
92
+ raise TypeError('name should be a string preferably up to 80 symbols')
93
+ self._name = name
94
+
95
+ def environment(self, atom: int, include_bond: bool = True, include_atom: bool = True) -> \
96
+ Tuple[Union[Tuple[int, Bond, Element],
97
+ Tuple[int, Element],
98
+ Tuple[int, Bond],
99
+ int], ...]:
100
+ """
101
+ groups of (atom_number, bond, atom) connected to atom or
102
+ groups of (atom_number, bond) connected to atom or
103
+ groups of (atom_number, atom) connected to atom or
104
+ neighbors atoms connected to atom
105
+
106
+ :param atom: number
107
+ :param include_atom: include atom object
108
+ :param include_bond: include bond object
109
+ """
110
+ if include_atom:
111
+ atoms = self._atoms
112
+ if include_bond:
113
+ return tuple((n, bond, atoms[n]) for n, bond in self._bonds[atom].items())
114
+ return tuple((n, atoms[n]) for n in self._bonds[atom])
115
+ elif include_bond:
116
+ return tuple(self._bonds[atom].items())
117
+ return tuple(self._bonds[atom])
118
+
119
+ def adjacency_matrix(self, set_bonds=False, /):
120
+ """
121
+ Adjacency matrix of Graph.
122
+
123
+ :param set_bonds: if True set bond orders instead of 1.
124
+ """
125
+ adj = zeros((len(self), len(self)), dtype=uint)
126
+ mapping = {n: x for x, n in enumerate(self._atoms)}
127
+ if set_bonds:
128
+ for n, ms in self._bonds.items():
129
+ n = mapping[n]
130
+ for m, b in ms.items():
131
+ adj[n, mapping[m]] = int(b)
132
+ else:
133
+ for n, ms in self._bonds.items():
134
+ n = mapping[n]
135
+ for m, b in ms.items():
136
+ adj[n, mapping[m]] = 1
137
+ return adj
138
+
139
+ @cached_property
140
+ def molecular_charge(self) -> int:
141
+ """
142
+ Total charge of molecule
143
+ """
144
+ return sum(a.charge for _, a in self.atoms())
145
+
146
+ @cached_property
147
+ def is_radical(self) -> bool:
148
+ """
149
+ True if at least one atom is radical
150
+ """
151
+ return any(a.is_radical for _, a in self.atoms())
152
+
153
+ @cached_property
154
+ def molecular_mass(self) -> float:
155
+ h = _H().atomic_mass
156
+ return sum(a.atomic_mass + (a.implicit_hydrogens or 0) * h for _, a in self.atoms())
157
+
158
+ @cached_property
159
+ def rotatable_bonds_count(self) -> int:
160
+ """
161
+ Number of rotatable bonds: non-ring single bonds linked to non-terminal atoms except [sulfon]amide-like C(=O)-N.
162
+
163
+ Charged atoms like R-NO2, R-[N+]([C,H])=C are ignored.
164
+ """
165
+ w, bs = rotable_rules
166
+ return sum(1 for _ in w.get_mapping(self)) - sum(1 for b in bs for _ in b.get_mapping(self))
167
+
168
+ @cached_property
169
+ def hydrogen_bond_donors_count(self) -> int:
170
+ """
171
+ Number of hydrogen bond donors: N, O, S atoms with hydrogens and at least one neighbour
172
+ (not water, ammonia, hydrogen sulfide).
173
+ """
174
+ return sum(
175
+ 1 for _, a in self.atoms()
176
+ if a in (N, O, S)
177
+ and a.neighbors and not a.is_radical
178
+ and (a.implicit_hydrogens or a.explicit_hydrogens)
179
+ )
180
+
181
+ @cached_property
182
+ def hydrogen_bond_acceptors_count(self) -> int:
183
+ """
184
+ Number of hydrogen bond acceptors: O, N, S atoms with at least one neighbour
185
+ (not water, ammonia, hydrogen sulfide), Non-positively charged, with lone pairs, except R-SH and R-S(=O)-R sulfurs.
186
+ """
187
+ hba = 0
188
+ for _, a in self.atoms():
189
+ if not a.neighbors or a.is_radical: continue
190
+ elif a == O:
191
+ if a.charge <= 0:
192
+ hba += 1
193
+ elif a == N:
194
+ if a.charge > 0 or a.hybridization == 4 and (a.implicit_hydrogens or a.neighbors == 3): continue
195
+ hba += 1
196
+ elif a == S:
197
+ if a.charge == -1: # R-[S-]
198
+ hba += 1
199
+ elif a.charge == 0 and a.neighbors == 2 and a.hybridization == 1: # R-S-R
200
+ hba += 1
201
+ return hba
202
+
203
+ @cached_property
204
+ def carbon_sp3_fraction(self) -> float:
205
+ """
206
+ Fraction of carbon atoms with sp3 hybridisation among all carbon atoms.
207
+ """
208
+ if not self.carbon_count:
209
+ return 0.
210
+ return self.carbon_sp3_count / self.carbon_count
211
+
212
+ @cached_property
213
+ def carbon_sp3_count(self) -> int:
214
+ return sum(1 for _, a in self.atoms() if a == C and a.hybridization == 1)
215
+
216
+ @cached_property
217
+ def carbon_count(self) -> int:
218
+ return self.brutto.get('C', 0)
219
+
220
+ @cached_property
221
+ def brutto(self) -> Dict[str, int]:
222
+ """Counted atoms dict"""
223
+ c = Counter()
224
+ # make an order
225
+ c['C'] = 0
226
+ c['H'] = 0
227
+ c['O'] = 0
228
+ c['N'] = 0
229
+ c['B'] = 0
230
+ c.update(a.atomic_symbol for a in sorted((a for _, a in self.atoms()), key=lambda a: a.atomic_number))
231
+ c['H'] += sum(a.implicit_hydrogens or 0 for _, a in self.atoms())
232
+ return {k: v for k, v in c.items() if v}
233
+
234
+ @cached_property
235
+ def brutto_formula(self) -> str:
236
+ return ''.join(f'{a}{c}' if c > 1 else a for a, c in self.brutto.items())
237
+
238
+ @cached_property
239
+ def brutto_formula_html(self) -> str:
240
+ return ''.join(f'{a}<sub>{c}</sub>' if c > 1 else a for a, c in self.brutto.items())
241
+
242
+ @cached_property
243
+ def aromatic_rings(self) -> Tuple[Tuple[int, ...], ...]:
244
+ """
245
+ Aromatic rings atoms numbers
246
+ """
247
+ bonds = self._bonds
248
+ return tuple(ring for ring in self.sssr if bonds[ring[0]][ring[-1]] == 4
249
+ and all(bonds[n][m] == 4 for n, m in zip(ring, ring[1:])))
250
+
251
+ def add_atom(self, atom: Union[Element, int, str], *args, _skip_calculation=False, **kwargs):
252
+ """
253
+ Add new atom.
254
+ """
255
+ if not isinstance(atom, Element):
256
+ if isinstance(atom, str):
257
+ atom = Element.from_symbol(atom)()
258
+ elif isinstance(atom, int):
259
+ atom = Element.from_atomic_number(atom)()
260
+ else:
261
+ raise TypeError('Element object expected')
262
+
263
+ n = super().add_atom(atom, *args, **kwargs)
264
+ if self._changed is None:
265
+ self._changed = {n}
266
+ else:
267
+ self._changed.add(n)
268
+ if not _skip_calculation and self._backup is None:
269
+ self.fix_structure()
270
+ return n
271
+
272
+ def add_bond(self, n, m, bond: Union[Bond, int], *, _skip_calculation=False):
273
+ """
274
+ Connect atoms with bonds.
275
+
276
+ For Thiele forms of molecule causes invalidation of internal state.
277
+ Implicit hydrogens marks will not be set if atoms in aromatic rings.
278
+ Call `kekule()` and `thiele()` in sequence to fix marks.
279
+ """
280
+ if not isinstance(bond, Bond):
281
+ bond = Bond(bond)
282
+
283
+ super().add_bond(n, m, bond)
284
+ if bond == 8:
285
+ return # any bond doesn't change anything
286
+ if self._changed is None:
287
+ self._changed = {n, m}
288
+ else:
289
+ self._changed.add(n)
290
+ self._changed.add(m)
291
+ if not _skip_calculation and self._backup is None:
292
+ self.fix_structure()
293
+ self.fix_stereo()
294
+
295
+ def delete_atom(self, n: int, *, _skip_calculation=False):
296
+ """
297
+ Remove atom.
298
+
299
+ For Thiele forms of molecule causes invalidation of internal state.
300
+ Implicit hydrogens marks will not be set if atoms in aromatic rings.
301
+ Call `kekule()` and `thiele()` in sequence to fix marks.
302
+ """
303
+ del self._atoms[n]
304
+ for m, bond in self._bonds.pop(n).items():
305
+ del self._bonds[m][n]
306
+ if bond == 8:
307
+ continue
308
+ if self._changed is None:
309
+ self._changed = {m}
310
+ else:
311
+ self._changed.add(m)
312
+ if not _skip_calculation and self._backup is None:
313
+ self.fix_structure()
314
+ self.fix_stereo()
315
+
316
+ def delete_bond(self, n: int, m: int, *, _skip_calculation=False):
317
+ """
318
+ Disconnect atoms.
319
+
320
+ For Thiele forms of molecule causes invalidation of internal state.
321
+ Implicit hydrogens marks will not be set if atoms in aromatic rings.
322
+ Call `kekule()` and `thiele()` in sequence to fix marks.
323
+ """
324
+ del self._bonds[n][m]
325
+ if self._bonds[m].pop(n) != 8:
326
+ if self._changed is None:
327
+ self._changed = {n, m}
328
+ else:
329
+ self._changed.add(n)
330
+ self._changed.add(m)
331
+ if not _skip_calculation and self._backup is None:
332
+ self.fix_structure()
333
+ self.fix_stereo()
334
+
335
+ def copy(self, *, keep_sssr=False, keep_components=False) -> 'MoleculeContainer':
336
+ copy = super().copy()
337
+ copy._changed = copy._backup = None
338
+ copy._name = self._name
339
+ if self._meta is None:
340
+ copy._meta = None
341
+ else:
342
+ copy._meta = self._meta.copy()
343
+
344
+ if keep_sssr:
345
+ for k, v in self.__dict__.items():
346
+ if k in ('sssr', 'atoms_rings', 'atoms_rings_sizes', 'not_special_connectivity', 'rings_count'):
347
+ copy.__dict__[k] = v
348
+ if keep_components:
349
+ if 'connected_components' in self.__dict__:
350
+ copy.__dict__['connected_components'] = self.connected_components
351
+ return copy
352
+
353
+ def union(self, other: 'MoleculeContainer', *, remap: bool = False, copy: bool = True) -> 'MoleculeContainer':
354
+ if not isinstance(other, MoleculeContainer):
355
+ raise TypeError('MoleculeContainer expected')
356
+ return super().union(other, remap=remap, copy=copy)
357
+
358
+ def substructure(self, atoms: Iterable[int], *, recalculate_hydrogens=True) -> 'MoleculeContainer':
359
+ """
360
+ Create substructure containing atoms from atoms list.
361
+
362
+ For Thiele forms of molecule In Molecule substructure causes invalidation of internal state.
363
+ Implicit hydrogens marks will not be set if atoms in aromatic rings.
364
+ Call `kekule()` and `thiele()` in sequence to fix marks.
365
+
366
+ :param atoms: list of atoms numbers of substructure
367
+ :param recalculate_hydrogens: calculate implicit H count in substructure
368
+ """
369
+ if not atoms:
370
+ raise ValueError('empty atoms list not allowed')
371
+ atoms_set = set(atoms)
372
+ if atoms_set - self._atoms.keys():
373
+ raise ValueError('invalid atom numbers')
374
+ atoms = tuple(n for n in self if n in atoms_set) # save original order
375
+ sub = object.__new__(self.__class__)
376
+ sub._name = sub._meta = sub._changed = sub._backup = None
377
+ sub._atoms = {n: self._atoms[n].copy(hydrogens=True, stereo=True) for n in atoms}
378
+ sub._bonds = sb = {}
379
+ for n in atoms:
380
+ sb[n] = sbn = {}
381
+ for m, bond in self._bonds[n].items():
382
+ if m in sb: # bond partially exists. need back-connection.
383
+ sbn[m] = sb[m][n]
384
+ elif m in atoms_set:
385
+ sbn[m] = bond.copy(stereo=True)
386
+ if recalculate_hydrogens:
387
+ # Only recalculate H on atoms that lost neighbors
388
+ sub._changed = {n for n in atoms if not self._bonds[n].keys() <= atoms_set}
389
+ sub.fix_structure(recalculate_hydrogens=recalculate_hydrogens)
390
+ sub.fix_stereo()
391
+ return sub
392
+
393
+ def augmented_substructure(self, atoms: Iterable[int], deep: int = 1, **kwargs) -> 'MoleculeContainer':
394
+ """
395
+ Create substructure containing atoms and their neighbors
396
+
397
+ :param atoms: list of core atoms in graph
398
+ :param deep: number of bonds between atoms and neighbors
399
+ """
400
+ return self.substructure(self._augmented_substructure(atoms, deep)[-1], **kwargs)
401
+
402
+ def augmented_substructures(self, atoms: Iterable[int], deep: int = 1, **kwargs) -> List['MoleculeContainer']:
403
+ """
404
+ Create list of substructures containing atoms and their neighbors
405
+
406
+ :param atoms: list of core atoms in graph
407
+ :param deep: number of bonds between atoms and neighbors
408
+ :return: list of graphs containing atoms, atoms + first circle, atoms + 1st + 2nd,
409
+ etc up to deep or while new nodes available
410
+ """
411
+ return [self.substructure(a, **kwargs) for a in self._augmented_substructure(atoms, deep)]
412
+
413
+ def split(self) -> List['MoleculeContainer']:
414
+ """
415
+ Split disconnected structure to connected substructures
416
+ """
417
+ return [self.substructure(c, recalculate_hydrogens=False) for c in self.connected_components]
418
+
419
+ def compose(self, other: 'MoleculeContainer', dynamic=True) -> Union['CGRContainer', 'MoleculeContainer']:
420
+ """
421
+ Compose 2 graphs to CGR.
422
+
423
+ :param dynamic: produce CGR with dynamic bonds and atoms,
424
+ overwise keep reactants' electronic and implicit hydrogens state and label dynamic bonds as "any".
425
+ This representation can't catch atom-only changes like (de)protonation, red-ox, etc;
426
+ or ambiguous bond changes like triple to double bond reduction.
427
+ """
428
+ if not isinstance(other, MoleculeContainer):
429
+ raise TypeError('MoleculeContainer expected')
430
+ bonds = []
431
+ adj = defaultdict(lambda: defaultdict(lambda: [None, None]))
432
+ common = self._atoms.keys() & other._atoms.keys()
433
+
434
+ if dynamic:
435
+ h = CGRContainer()
436
+ from_atom = DynamicElement.from_atom
437
+ from_atoms = DynamicElement.from_atoms
438
+ from_bond = DynamicBond.from_bond
439
+ dynamic_bond = DynamicBond
440
+ else:
441
+ h = self.__class__()
442
+ from_atom = from_atoms = lambda x, *_: x.copy(hydrogens=True)
443
+ from_bond = lambda x: x.copy()
444
+ dynamic_bond = lambda x, y: Bond(8 if x != y else x)
445
+
446
+ ha = h._atoms
447
+ hb = h._bonds
448
+
449
+ for n in self._atoms.keys() - common: # cleavage atoms
450
+ ha[n] = from_atom(self._atoms[n])
451
+ hb[n] = {}
452
+ for m, bond in self._bonds[n].items():
453
+ if m not in ha:
454
+ if m in common: # bond to common atoms is broken bond
455
+ bond = dynamic_bond(bond.order, None)
456
+ else:
457
+ bond = from_bond(bond)
458
+ bonds.append((n, m, bond))
459
+ for n in other._atoms.keys() - common: # coupling atoms
460
+ ha[n] = from_atom(other._atoms[n])
461
+ hb[n] = {}
462
+
463
+ for m, bond in other._bonds[n].items():
464
+ if m not in ha:
465
+ if m in common: # bond to common atoms is formed bond
466
+ bond = dynamic_bond(None, bond.order)
467
+ else:
468
+ bond = from_bond(bond)
469
+ bonds.append((n, m, bond))
470
+ for n in common:
471
+ an = adj[n]
472
+ for m, bond in self._bonds[n].items():
473
+ if m in common:
474
+ an[m][0] = bond.order
475
+ for m, bond in other._bonds[n].items():
476
+ if m in common:
477
+ an[m][1] = bond.order
478
+ for n in common:
479
+ ha[n] = from_atoms(self._atoms[n], other._atoms[n])
480
+ hb[n] = {}
481
+
482
+ for m, (o1, o2) in adj[n].items():
483
+ if m not in ha:
484
+ bonds.append((n, m, dynamic_bond(o1, o2)))
485
+
486
+ for n, m, bond in bonds:
487
+ hb[n][m] = hb[m][n] = bond
488
+
489
+ if not dynamic:
490
+ h.calc_labels()
491
+ return h
492
+
493
+ def pack(self, *, compressed=True, check=True, version=2, order: List[int] = None) -> bytes:
494
+ """
495
+ Pack into compressed bytes.
496
+
497
+ Note:
498
+
499
+ * Less than 4096 atoms supported. Atoms mapping should be in range 1-4095.
500
+ * Implicit hydrogens count should be in range 0-6 or unspecified.
501
+ * Isotope shift should be in range -15 - 15 relatively chython.files._mdl.mol.common_isotopes
502
+ * Atoms neighbors should be in range 0-15
503
+
504
+ Format V2 specification::
505
+
506
+ Big endian bytes order
507
+ 8 bit - 0x02 (format specification version)
508
+ 12 bit - number of atoms
509
+ 12 bit - cis/trans stereo block size
510
+ Atom block 9 bytes (repeated):
511
+ 12 bit - atom number
512
+ 4 bit - number of neighbors
513
+ 2 bit tetrahedron sign (00 - not stereo, 10 or 11 - has stereo)
514
+ 2 bit - allene sign
515
+ 5 bit - isotope (00000 - not specified, over = isotope - common_isotope + 16)
516
+ 7 bit - atomic number (<=118)
517
+ 32 bit - XY float16 coordinates
518
+ 3 bit - hydrogens (0-7). Note: 7 == None
519
+ 4 bit - charge (charge + 4. possible range -4 - 4)
520
+ 1 bit - radical state
521
+ Connection table: flatten list of neighbors. neighbors count stored in atom block.
522
+ For example CC(=O)O - {1: [2], 2: [1, 3, 4], 3: [2], 4: [2]} >> [2, 1, 3, 4, 2, 2].
523
+ Repeated block (equal to bonds count).
524
+ 24 bit - paired 12 bit numbers.
525
+ Bonds order block 3 bit per bond zero-padded to full byte at the end.
526
+ Cis/trans data block (repeated):
527
+ 24 bit - atoms pair
528
+ 7 bit - zero padding. in future can be used for extra bond-level stereo, like atropoisomers.
529
+ 1 bit - sign
530
+
531
+ :param compressed: return zlib-compressed pack.
532
+ :param check: check molecule for format restrictions.
533
+ :param version: format version. Only V2 is supported.
534
+ :param order: atom order in V3
535
+ """
536
+ from ._pack_v2 import pack as pack_v2
537
+
538
+ if check:
539
+ bonds = self._bonds
540
+ if not bonds:
541
+ raise ValueError('Empty molecules not supported')
542
+ if max(bonds) > 4095:
543
+ raise ValueError('Big molecules not supported')
544
+ if any(len(x) > 15 for x in bonds.values()):
545
+ raise ValueError('To many neighbors not supported')
546
+
547
+ if version == 2:
548
+ data = pack_v2(self)
549
+ else:
550
+ raise ValueError('invalid specification version')
551
+ if compressed:
552
+ return compress(data, 9)
553
+ return data
554
+
555
+ def pach(self, *, compressed=True, check=True, version=2, order: List[int] = None) -> bytes:
556
+ return self.pack(compressed=compressed, check=check, version=version, order=order)
557
+
558
+ @classmethod
559
+ def pack_len(cls, data: bytes, /, *, compressed=True) -> int:
560
+ """
561
+ Returns atoms count in molecule pack.
562
+ """
563
+ if compressed:
564
+ data = decompress(data)
565
+ if data[0] not in (0, 2):
566
+ raise ValueError('invalid pack header')
567
+ return int.from_bytes(data[1:3], 'big') >> 4
568
+
569
+ @classmethod
570
+ def unpack(cls, data: Union[bytes, memoryview], /, *, compressed=True, skip_labels_calculation=False,
571
+ _return_pack_length=False) -> 'MoleculeContainer':
572
+ """
573
+ Unpack from compressed bytes.
574
+
575
+ :param compressed: decompress data before processing.
576
+ """
577
+ from ._unpack_v0v2 import unpack as unpack_v0v2
578
+
579
+ if compressed:
580
+ data = decompress(data)
581
+ if data[0] in (0, 2):
582
+ mol, cis_trans, pack_length = unpack_v0v2(data)
583
+ for n, m, s in cis_trans:
584
+ if n in mol._stereo_cis_trans_centers: # check for invalid CT data
585
+ mol.bond(*mol._stereo_cis_trans_centers[n])._stereo = s
586
+ else:
587
+ raise ValueError('invalid pack header')
588
+
589
+ if not skip_labels_calculation:
590
+ mol.calc_labels()
591
+
592
+ if _return_pack_length:
593
+ return mol, pack_length
594
+ return mol
595
+
596
+ @classmethod
597
+ def unpach(cls, data: Union[bytes, memoryview], /, *, compressed=True) -> 'MoleculeContainer':
598
+ """
599
+ Unpack from compressed bytes.
600
+ """
601
+ return cls.unpack(data, compressed=compressed)
602
+
603
+ def __bytes__(self):
604
+ return self.pack()
605
+
606
+ def _augmented_substructure(self, atoms: Iterable[int], deep: int):
607
+ atoms = set(atoms)
608
+ bonds = self._bonds
609
+ if atoms - bonds.keys():
610
+ raise ValueError('invalid atom numbers')
611
+ nodes = [atoms]
612
+ for _ in range(deep):
613
+ n = {y for x in nodes[-1] for y in bonds[x]} | nodes[-1]
614
+ if n in nodes:
615
+ break
616
+ nodes.append(n)
617
+ return nodes
618
+
619
+ def fix_structure(self, recalculate_hydrogens=True):
620
+ """
621
+ Fix molecule internal representation
622
+ """
623
+ self.flush_cache()
624
+ self.calc_labels() # refresh all labels
625
+
626
+ if recalculate_hydrogens:
627
+ if not self._changed:
628
+ for n in self._atoms:
629
+ self.calc_implicit(n)
630
+ else:
631
+ for n in self._changed.intersection(self._atoms):
632
+ self.calc_implicit(n) # fix Hs count
633
+ self._changed = None
634
+
635
+ def calc_labels(self):
636
+ atoms = self._atoms
637
+ atoms_rings_sizes = self.atoms_rings_sizes # expensive: sssr based
638
+ atoms_rings = {n: set(r) for n, r in self.atoms_rings.items()}
639
+
640
+ for n, m_bond in self._bonds.items():
641
+ neighbors = 0
642
+ heteroatoms = 0
643
+ hybridization = 1
644
+ explicit_hydrogens = 0
645
+ anr = atoms_rings.get(n) or False
646
+ for m, bond in m_bond.items():
647
+ bond._in_ring = anr and (amr := atoms_rings.get(m) or False) and not anr.isdisjoint(amr) # have common rings
648
+
649
+ if bond == 8:
650
+ continue
651
+ elif bond == 4:
652
+ hybridization = 4
653
+ elif hybridization != 4:
654
+ if bond == 3:
655
+ hybridization = 3
656
+ elif bond == 2:
657
+ if hybridization == 1:
658
+ hybridization = 2
659
+ elif hybridization == 2:
660
+ hybridization = 3
661
+
662
+ neighbors += 1
663
+ if (a := atoms[m]) == H:
664
+ explicit_hydrogens += 1
665
+ elif a != C:
666
+ heteroatoms += 1
667
+ atom = atoms[n]
668
+ atom._neighbors = neighbors
669
+ atom._heteroatoms = heteroatoms
670
+ atom._hybridization = hybridization
671
+ atom._explicit_hydrogens = explicit_hydrogens
672
+
673
+ atom._in_ring = n in atoms_rings_sizes
674
+ atom._ring_sizes = atoms_rings_sizes.get(n) or set()
675
+
676
+ def calc_implicit(self, n: int):
677
+ """
678
+ Set firs possible hydrogens count based on rules
679
+ """
680
+ if (atom := self._atoms[n]) == H: # hydrogen nether has implicit H
681
+ atom._implicit_hydrogens = 0
682
+ return
683
+
684
+ explicit_sum = 0
685
+ explicit_dict = defaultdict(int)
686
+ aroma = 0
687
+ for m, bond in self._bonds[n].items():
688
+ if bond == 4: # only neutral carbon aromatic rings supported
689
+ if not atom.charge and not atom.is_radical and atom == C:
690
+ aroma += 1
691
+ else: # use `kekule()` to calculate proper implicit hydrogens count
692
+ atom._implicit_hydrogens = None
693
+ return
694
+ elif bond != 8: # any bond used for complexes
695
+ explicit_sum += bond.order
696
+ explicit_dict[(bond.order, self._atoms[m].atomic_number)] += 1
697
+
698
+ if aroma == 2:
699
+ if explicit_sum == 0: # H-Ar
700
+ atom._implicit_hydrogens = 1
701
+ elif explicit_sum == 1: # R-Ar
702
+ atom._implicit_hydrogens = 0
703
+ else: # invalid aromaticity
704
+ atom._implicit_hydrogens = None
705
+ return
706
+ elif aroma == 3: # condensed rings
707
+ if explicit_sum: # invalid aromaticity
708
+ atom._implicit_hydrogens = None
709
+ else:
710
+ atom._implicit_hydrogens = 0
711
+ return
712
+ elif aroma:
713
+ atom._implicit_hydrogens = None
714
+ return
715
+
716
+ try:
717
+ rules = atom.valence_rules(explicit_sum)
718
+ except ValenceError:
719
+ atom._implicit_hydrogens = None
720
+ return
721
+ for s, d, h in rules:
722
+ if s.issubset(explicit_dict) and all(explicit_dict[k] >= c for k, c in d.items()):
723
+ atom._implicit_hydrogens = h
724
+ return
725
+ atom._implicit_hydrogens = None # rule not found
726
+
727
+ def check_implicit(self, n: int, h: int) -> bool:
728
+ if (atom := self._atoms[n]) == H: # hydrogen nether has implicit H
729
+ return h == 0
730
+
731
+ explicit_sum = 0
732
+ explicit_dict = defaultdict(int)
733
+
734
+ for m, bond in self._bonds[n].items():
735
+ if bond == 4: # can't check aromatic rings
736
+ return False
737
+ elif bond != 8: # any bond used for complexes
738
+ explicit_sum += bond.order
739
+ explicit_dict[(bond.order, self._atoms[m].atomic_number)] += 1
740
+
741
+ try:
742
+ rules = atom.valence_rules(explicit_sum)
743
+ except ValenceError:
744
+ return False
745
+ for s, d, _h in rules:
746
+ if h == _h and s.issubset(explicit_dict) and all(explicit_dict[k] >= c for k, c in d.items()):
747
+ return True
748
+ return False
749
+
750
+ def flush_cache(self, *, keep_sssr=False, keep_components=False, keep_special_connectivity=False):
751
+ backup = {}
752
+ if keep_sssr:
753
+ # good to keep if no new bonds or bonds deletions or bonds to/from any change
754
+ for k, v in self.__dict__.items():
755
+ if k in ('sssr', 'atoms_rings', 'atoms_rings_sizes', 'rings_count'):
756
+ backup[k] = v
757
+ if keep_special_connectivity and 'not_special_connectivity' in self.__dict__:
758
+ backup['not_special_connectivity'] = self.not_special_connectivity
759
+ if keep_components and 'connected_components' in self.__dict__:
760
+ backup['connected_components'] = self.connected_components
761
+ self.__dict__ = backup
762
+
763
+ def __int__(self):
764
+ """
765
+ Total charge of molecule
766
+ """
767
+ return self.molecular_charge
768
+
769
+ def __float__(self):
770
+ return self.molecular_mass
771
+
772
+ def __xor__(self, other):
773
+ """
774
+ G ^ H is CGR generation
775
+ """
776
+ return self.compose(other)
777
+
778
+ def __and__(self, other: Iterable[int]):
779
+ """
780
+ Substructure of graph with given nodes.
781
+ """
782
+ return self.substructure(other)
783
+
784
+ def __sub__(self, other: Iterable[int]):
785
+ """
786
+ Given nodes excluded substructure of graph.
787
+ """
788
+ atoms = set(other)
789
+ if atoms - self._atoms.keys():
790
+ raise ValueError('invalid atom numbers')
791
+ atoms = self._atoms.keys() - atoms
792
+ if atoms:
793
+ return self.substructure(atoms)
794
+ raise ValueError('full substitution not allowed')
795
+
796
+ def __enter__(self):
797
+ """
798
+ Transaction of changes. Keep current state for restoring on errors.
799
+ """
800
+ self._backup = self.copy(keep_sssr=True, keep_components=True)
801
+ return self
802
+
803
+ def __exit__(self, exc_type, exc_val, exc_tb):
804
+ if exc_type: # restore state
805
+ backup = self._backup
806
+ self._atoms = backup._atoms
807
+ self._bonds = backup._bonds
808
+ self._meta = backup._meta
809
+ self._name = backup._name
810
+ self.__dict__ = backup.__dict__
811
+ else: # update internal state
812
+ self.fix_structure()
813
+ self.fix_stereo()
814
+ self._backup = None # drop backup
815
+
816
+
817
+ __all__ = ['MoleculeContainer']