chython 2.4__cp312-cp312-win_amd64.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (171) hide show
  1. chython/__init__.py +31 -0
  2. chython/_functions.py +69 -0
  3. chython/algorithms/__init__.py +18 -0
  4. chython/algorithms/_isomorphism.cp312-win_amd64.pyd +0 -0
  5. chython/algorithms/_isomorphism.pyx +189 -0
  6. chython/algorithms/aromatics/__init__.py +27 -0
  7. chython/algorithms/aromatics/_rules.py +124 -0
  8. chython/algorithms/aromatics/kekule.py +530 -0
  9. chython/algorithms/aromatics/test/__init__.py +18 -0
  10. chython/algorithms/aromatics/test/test_kekule.py +160 -0
  11. chython/algorithms/aromatics/test/test_thiele.py +263 -0
  12. chython/algorithms/aromatics/thiele.py +226 -0
  13. chython/algorithms/calculate2d/__init__.py +23 -0
  14. chython/algorithms/calculate2d/clean2d.js +1 -0
  15. chython/algorithms/calculate2d/molecule.py +149 -0
  16. chython/algorithms/calculate2d/reaction.py +80 -0
  17. chython/algorithms/depict.py +523 -0
  18. chython/algorithms/fingerprints/__init__.py +48 -0
  19. chython/algorithms/fingerprints/linear.py +208 -0
  20. chython/algorithms/fingerprints/morgan.py +137 -0
  21. chython/algorithms/fingerprints/test/__init__.py +18 -0
  22. chython/algorithms/fingerprints/test/test_linear.py +127 -0
  23. chython/algorithms/fingerprints/test/test_morgan.py +79 -0
  24. chython/algorithms/isomorphism.py +667 -0
  25. chython/algorithms/mapping/__init__.py +28 -0
  26. chython/algorithms/mapping/_groups.py +94 -0
  27. chython/algorithms/mapping/_reactions.py +72 -0
  28. chython/algorithms/mapping/attention.py +216 -0
  29. chython/algorithms/mapping/fixmapper.py +84 -0
  30. chython/algorithms/mapping/groups.py +137 -0
  31. chython/algorithms/mcs.py +202 -0
  32. chython/algorithms/morgan.py +82 -0
  33. chython/algorithms/rings.py +551 -0
  34. chython/algorithms/smiles.py +556 -0
  35. chython/algorithms/standardize/__init__.py +30 -0
  36. chython/algorithms/standardize/_charged.py +115 -0
  37. chython/algorithms/standardize/_groups.py +926 -0
  38. chython/algorithms/standardize/_metal_organics.py +189 -0
  39. chython/algorithms/standardize/_reagents.py +44 -0
  40. chython/algorithms/standardize/_salts.py +68 -0
  41. chython/algorithms/standardize/molecule.py +479 -0
  42. chython/algorithms/standardize/reaction.py +429 -0
  43. chython/algorithms/standardize/resonance.py +196 -0
  44. chython/algorithms/standardize/salts.py +128 -0
  45. chython/algorithms/standardize/saturation.py +378 -0
  46. chython/algorithms/standardize/test/__init__.py +18 -0
  47. chython/algorithms/standardize/test/test_groups.py +118 -0
  48. chython/algorithms/stereo.py +1222 -0
  49. chython/algorithms/tautomers/__init__.py +171 -0
  50. chython/algorithms/tautomers/_acid.py +59 -0
  51. chython/algorithms/tautomers/_base.py +116 -0
  52. chython/algorithms/tautomers/_keto_enol.py +107 -0
  53. chython/algorithms/tautomers/acid_base.py +214 -0
  54. chython/algorithms/tautomers/heteroarenes.py +108 -0
  55. chython/algorithms/tautomers/keto_enol.py +148 -0
  56. chython/algorithms/tautomers/test/__init__.py +18 -0
  57. chython/algorithms/tautomers/test/test_tautomers.py +80 -0
  58. chython/algorithms/test/__init__.py +18 -0
  59. chython/algorithms/test/test_isomorphism.py +58 -0
  60. chython/algorithms/test/test_smiles.py +76 -0
  61. chython/algorithms/x3dom.py +355 -0
  62. chython/containers/__init__.py +43 -0
  63. chython/containers/_pack_v2.cp312-win_amd64.pyd +0 -0
  64. chython/containers/_pack_v2.pyx +271 -0
  65. chython/containers/_unpack_v0v2.cp312-win_amd64.pyd +0 -0
  66. chython/containers/_unpack_v0v2.pyx +300 -0
  67. chython/containers/bonds.py +255 -0
  68. chython/containers/cgr.py +115 -0
  69. chython/containers/graph.py +200 -0
  70. chython/containers/molecule.py +695 -0
  71. chython/containers/query.py +66 -0
  72. chython/containers/reaction.py +324 -0
  73. chython/exceptions.py +114 -0
  74. chython/files/MRVrw.py +523 -0
  75. chython/files/PDBrw.py +257 -0
  76. chython/files/RDFrw.py +298 -0
  77. chython/files/SDFrw.py +221 -0
  78. chython/files/__init__.py +29 -0
  79. chython/files/_convert.py +207 -0
  80. chython/files/_mapping.py +117 -0
  81. chython/files/_xyz.cp312-win_amd64.pyd +0 -0
  82. chython/files/_xyz.pyx +74 -0
  83. chython/files/daylight/__init__.py +24 -0
  84. chython/files/daylight/parser.py +157 -0
  85. chython/files/daylight/smarts.py +107 -0
  86. chython/files/daylight/smiles.py +245 -0
  87. chython/files/daylight/test/__init__.py +18 -0
  88. chython/files/daylight/test/test_daylight_smarts.py +289 -0
  89. chython/files/daylight/test/test_daylight_smiles.py +101 -0
  90. chython/files/daylight/test/test_parser.py +118 -0
  91. chython/files/daylight/test/test_tokenize.py +77 -0
  92. chython/files/daylight/tokenize.py +376 -0
  93. chython/files/libinchi/__init__.py +22 -0
  94. chython/files/libinchi/libinchi.dll +0 -0
  95. chython/files/libinchi/wrapper.py +555 -0
  96. chython/files/mdl/__init__.py +25 -0
  97. chython/files/mdl/emol.py +209 -0
  98. chython/files/mdl/erxn.py +67 -0
  99. chython/files/mdl/mol.py +144 -0
  100. chython/files/mdl/read.py +225 -0
  101. chython/files/mdl/rxn.py +67 -0
  102. chython/files/mdl/stereo.py +63 -0
  103. chython/files/mdl/write.py +165 -0
  104. chython/files/xyz.py +78 -0
  105. chython/periodictable/__init__.py +69 -0
  106. chython/periodictable/base/__init__.py +25 -0
  107. chython/periodictable/base/dynamic.py +149 -0
  108. chython/periodictable/base/element.py +452 -0
  109. chython/periodictable/base/groups.py +90 -0
  110. chython/periodictable/base/periods.py +46 -0
  111. chython/periodictable/base/query.py +473 -0
  112. chython/periodictable/base/vector.py +119 -0
  113. chython/periodictable/groupI.py +252 -0
  114. chython/periodictable/groupII.py +223 -0
  115. chython/periodictable/groupIII.py +1154 -0
  116. chython/periodictable/groupIV.py +210 -0
  117. chython/periodictable/groupIX.py +196 -0
  118. chython/periodictable/groupV.py +192 -0
  119. chython/periodictable/groupVI.py +183 -0
  120. chython/periodictable/groupVII.py +162 -0
  121. chython/periodictable/groupVIII.py +161 -0
  122. chython/periodictable/groupX.py +166 -0
  123. chython/periodictable/groupXI.py +164 -0
  124. chython/periodictable/groupXII.py +158 -0
  125. chython/periodictable/groupXIII.py +239 -0
  126. chython/periodictable/groupXIV.py +262 -0
  127. chython/periodictable/groupXV.py +276 -0
  128. chython/periodictable/groupXVI.py +457 -0
  129. chython/periodictable/groupXVII.py +324 -0
  130. chython/periodictable/groupXVIII.py +260 -0
  131. chython/reactor/__init__.py +24 -0
  132. chython/reactor/base.py +212 -0
  133. chython/reactor/deprotection.py +565 -0
  134. chython/reactor/groups.py +128 -0
  135. chython/reactor/reactions/__init__.py +157 -0
  136. chython/reactor/reactions/_amidation.py +58 -0
  137. chython/reactor/reactions/_amine_isocyanate.py +52 -0
  138. chython/reactor/reactions/_buchwald_hartwig.py +50 -0
  139. chython/reactor/reactions/_esterification.py +49 -0
  140. chython/reactor/reactions/_macmillan.py +43 -0
  141. chython/reactor/reactions/_reductive_amination.py +50 -0
  142. chython/reactor/reactions/_sonogashira.py +49 -0
  143. chython/reactor/reactions/_sulfonamidation.py +52 -0
  144. chython/reactor/reactions/_suzuki_miyaura.py +88 -0
  145. chython/reactor/reactions/ufe.py +99 -0
  146. chython/reactor/reactor.py +170 -0
  147. chython/reactor/retro/__init__.py +77 -0
  148. chython/reactor/retro/_amidation.py +39 -0
  149. chython/reactor/retro/_aryl_amination.py +32 -0
  150. chython/reactor/retro/_mitsunobu.py +49 -0
  151. chython/reactor/retro/_sonogashira.py +49 -0
  152. chython/reactor/retro/_suzuki_miyaura.py +54 -0
  153. chython/reactor/scaffold.py +122 -0
  154. chython/reactor/test/__init__.py +18 -0
  155. chython/reactor/test/test_deprotection.py +57 -0
  156. chython/reactor/test/test_reactor.py +40 -0
  157. chython/reactor/test/test_scaffold.py +57 -0
  158. chython/reactor/test/test_transformer.py +40 -0
  159. chython/reactor/transformer.py +62 -0
  160. chython/utils/__init__.py +57 -0
  161. chython/utils/free_wilson.py +114 -0
  162. chython/utils/grid.py +130 -0
  163. chython/utils/rdkit.py +165 -0
  164. chython/utils/retro.py +139 -0
  165. chython/utils/svg.py +57 -0
  166. chython/utils/test/__init__.py +18 -0
  167. chython/utils/test/test_rdkit.py +71 -0
  168. chython-2.4.dist-info/LICENSE +165 -0
  169. chython-2.4.dist-info/METADATA +101 -0
  170. chython-2.4.dist-info/RECORD +171 -0
  171. chython-2.4.dist-info/WHEEL +4 -0
@@ -0,0 +1,695 @@
1
+ # -*- coding: utf-8 -*-
2
+ #
3
+ # Copyright 2017-2025 Ramil Nugmanov <nougmanoff@protonmail.com>
4
+ # This file is part of chython.
5
+ #
6
+ # chython is free software; you can redistribute it and/or modify
7
+ # it under the terms of the GNU Lesser General Public License as published by
8
+ # the Free Software Foundation; either version 3 of the License, or
9
+ # (at your option) any later version.
10
+ #
11
+ # This program is distributed in the hope that it will be useful,
12
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
13
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
+ # GNU Lesser General Public License for more details.
15
+ #
16
+ # You should have received a copy of the GNU Lesser General Public License
17
+ # along with this program; if not, see <https://www.gnu.org/licenses/>.
18
+ #
19
+ from CachedMethods import cached_args_method
20
+ from collections import Counter, defaultdict
21
+ from functools import cached_property
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 .graph import Graph
28
+ from ..algorithms.aromatics import Aromatize
29
+ from ..algorithms.calculate2d import Calculate2DMolecule
30
+ from ..algorithms.depict import DepictMolecule
31
+ from ..algorithms.isomorphism import MoleculeIsomorphism
32
+ from ..algorithms.fingerprints import Fingerprints
33
+ from ..algorithms.mcs import MCS
34
+ from ..algorithms.morgan import Morgan
35
+ from ..algorithms.rings import Rings
36
+ from ..algorithms.smiles import MoleculeSmiles
37
+ from ..algorithms.standardize import StandardizeMolecule
38
+ from ..algorithms.stereo import MoleculeStereo
39
+ from ..algorithms.tautomers import Tautomers
40
+ from ..algorithms.x3dom import X3domMolecule
41
+ from ..exceptions import ValenceError
42
+ from ..periodictable import DynamicElement, Element, H as _H
43
+
44
+
45
+ # atomic number constants
46
+ H = 1
47
+ C = 6
48
+
49
+
50
+ class MoleculeContainer(MoleculeStereo, Graph[Element, Bond], Morgan, Rings, MoleculeIsomorphism,
51
+ Aromatize, StandardizeMolecule, MoleculeSmiles, DepictMolecule, Calculate2DMolecule,
52
+ Fingerprints, Tautomers, MCS, X3domMolecule):
53
+ __slots__ = ('_meta', '_name', '_conformers', '_changed', '_backup')
54
+
55
+ def __init__(self):
56
+ super().__init__()
57
+ self._meta = None
58
+ self._name = None
59
+ self._changed = None
60
+ self._backup = None
61
+
62
+ @property
63
+ def meta(self) -> Dict:
64
+ if self._meta is None:
65
+ self._meta = {} # lazy
66
+ return self._meta
67
+
68
+ @property
69
+ def name(self) -> str:
70
+ return self._name or ''
71
+
72
+ @name.setter
73
+ def name(self, name):
74
+ if not isinstance(name, str):
75
+ raise TypeError('name should be a string preferably up to 80 symbols')
76
+ self._name = name
77
+
78
+ def environment(self, atom: int, include_bond: bool = True, include_atom: bool = True) -> \
79
+ Tuple[Union[Tuple[int, Bond, Element],
80
+ Tuple[int, Element],
81
+ Tuple[int, Bond],
82
+ int], ...]:
83
+ """
84
+ groups of (atom_number, bond, atom) connected to atom or
85
+ groups of (atom_number, bond) connected to atom or
86
+ groups of (atom_number, atom) connected to atom or
87
+ neighbors atoms connected to atom
88
+
89
+ :param atom: number
90
+ :param include_atom: include atom object
91
+ :param include_bond: include bond object
92
+ """
93
+ if include_atom:
94
+ atoms = self._atoms
95
+ if include_bond:
96
+ return tuple((n, bond, atoms[n]) for n, bond in self._bonds[atom].items())
97
+ return tuple((n, atoms[n]) for n in self._bonds[atom])
98
+ elif include_bond:
99
+ return tuple(self._bonds[atom].items())
100
+ return tuple(self._bonds[atom])
101
+
102
+ @cached_args_method
103
+ def adjacency_matrix(self, set_bonds=False, /):
104
+ """
105
+ Adjacency matrix of Graph.
106
+
107
+ :param set_bonds: if True set bond orders instead of 1.
108
+ """
109
+ adj = zeros((len(self), len(self)), dtype=uint)
110
+ mapping = {n: x for x, n in enumerate(self._atoms)}
111
+ if set_bonds:
112
+ for n, ms in self._bonds.items():
113
+ n = mapping[n]
114
+ for m, b in ms.items():
115
+ adj[n, mapping[m]] = int(b)
116
+ else:
117
+ for n, ms in self._bonds.items():
118
+ n = mapping[n]
119
+ for m, b in ms.items():
120
+ adj[n, mapping[m]] = 1
121
+ return adj
122
+
123
+ @cached_property
124
+ def molecular_charge(self) -> int:
125
+ """
126
+ Total charge of molecule
127
+ """
128
+ return sum(a.charge for _, a in self.atoms())
129
+
130
+ @cached_property
131
+ def is_radical(self) -> bool:
132
+ """
133
+ True if at least one atom is radical
134
+ """
135
+ return any(a.is_radical for _, a in self.atoms())
136
+
137
+ @cached_property
138
+ def molecular_mass(self) -> float:
139
+ h = _H().atomic_mass
140
+ return sum(a.atomic_mass + a.implicit_hydrogens * h for _, a in self.atoms())
141
+
142
+ @cached_property
143
+ def brutto(self) -> Dict[str, int]:
144
+ """Counted atoms dict"""
145
+ c = Counter(a.atomic_symbol for _, a in self.atoms())
146
+ c['H'] += sum(a.implicit_hydrogens for _, a in self.atoms())
147
+ return dict(c)
148
+
149
+ @cached_property
150
+ def aromatic_rings(self) -> Tuple[Tuple[int, ...], ...]:
151
+ """
152
+ Aromatic rings atoms numbers
153
+ """
154
+ bonds = self._bonds
155
+ return tuple(ring for ring in self.sssr if bonds[ring[0]][ring[-1]] == 4
156
+ and all(bonds[n][m] == 4 for n, m in zip(ring, ring[1:])))
157
+
158
+ def add_atom(self, atom: Union[Element, int, str], *args, _skip_calculation=False, **kwargs):
159
+ """
160
+ Add new atom.
161
+ """
162
+ if not isinstance(atom, Element):
163
+ if isinstance(atom, str):
164
+ atom = Element.from_symbol(atom)()
165
+ elif isinstance(atom, int):
166
+ atom = Element.from_atomic_number(atom)()
167
+ else:
168
+ raise TypeError('Element object expected')
169
+
170
+ n = super().add_atom(atom, *args, **kwargs)
171
+ if self._changed is None:
172
+ self._changed = {n}
173
+ else:
174
+ self._changed.add(n)
175
+ if not _skip_calculation and self._backup is None:
176
+ self.fix_structure()
177
+ return n
178
+
179
+ def add_bond(self, n, m, bond: Union[Bond, int], *, _skip_calculation=False):
180
+ """
181
+ Connect atoms with bonds.
182
+
183
+ For Thiele forms of molecule causes invalidation of internal state.
184
+ Implicit hydrogens marks will not be set if atoms in aromatic rings.
185
+ Call `kekule()` and `thiele()` in sequence to fix marks.
186
+ """
187
+ if not isinstance(bond, Bond):
188
+ bond = Bond(bond)
189
+
190
+ super().add_bond(n, m, bond)
191
+ if bond == 8:
192
+ return # any bond doesn't change anything
193
+ if self._changed is None:
194
+ self._changed = {n, m}
195
+ else:
196
+ self._changed.add(n)
197
+ self._changed.add(m)
198
+ if not _skip_calculation and self._backup is None:
199
+ self.fix_structure()
200
+ self.fix_stereo()
201
+
202
+ def delete_atom(self, n: int, *, _skip_calculation=False):
203
+ """
204
+ Remove atom.
205
+
206
+ For Thiele forms of molecule causes invalidation of internal state.
207
+ Implicit hydrogens marks will not be set if atoms in aromatic rings.
208
+ Call `kekule()` and `thiele()` in sequence to fix marks.
209
+ """
210
+ del self._atoms[n]
211
+ for m, bond in self._bonds.pop(n).items():
212
+ del self._bonds[m][n]
213
+ if bond == 8:
214
+ continue
215
+ if self._changed is None:
216
+ self._changed = {m}
217
+ else:
218
+ self._changed.add(m)
219
+ if not _skip_calculation and self._backup is None:
220
+ self.fix_structure()
221
+ self.fix_stereo()
222
+
223
+ def delete_bond(self, n: int, m: int, *, _skip_calculation=False):
224
+ """
225
+ Disconnect atoms.
226
+
227
+ For Thiele forms of molecule causes invalidation of internal state.
228
+ Implicit hydrogens marks will not be set if atoms in aromatic rings.
229
+ Call `kekule()` and `thiele()` in sequence to fix marks.
230
+ """
231
+ del self._bonds[n][m]
232
+ if self._bonds[m].pop(n) != 8:
233
+ if self._changed is None:
234
+ self._changed = {n, m}
235
+ else:
236
+ self._changed.add(n)
237
+ self._changed.add(m)
238
+ if not _skip_calculation and self._backup is None:
239
+ self.fix_structure()
240
+ self.fix_stereo()
241
+
242
+ def copy(self, *, keep_sssr=False, keep_components=False) -> 'MoleculeContainer':
243
+ copy = super().copy()
244
+ copy._name = self._name
245
+ if self._meta is None:
246
+ copy._meta = None
247
+ else:
248
+ copy._meta = self._meta.copy()
249
+
250
+ if keep_sssr:
251
+ for k, v in self.__dict__.items():
252
+ if k in ('sssr', 'atoms_rings', 'atoms_rings_sizes', 'not_special_connectivity', 'rings_count'):
253
+ copy.__dict__[k] = v
254
+ if keep_components:
255
+ if 'connected_components' in self.__dict__:
256
+ copy.__dict__['connected_components'] = self.connected_components
257
+ return copy
258
+
259
+ def union(self, other: 'MoleculeContainer', *, remap: bool = False, copy: bool = True) -> 'MoleculeContainer':
260
+ if not isinstance(other, MoleculeContainer):
261
+ raise TypeError('MoleculeContainer expected')
262
+ return super().union(other, remap=remap, copy=copy)
263
+
264
+ def substructure(self, atoms: Iterable[int], *, recalculate_hydrogens=True) -> 'MoleculeContainer':
265
+ """
266
+ Create substructure containing atoms from atoms list.
267
+
268
+ For Thiele forms of molecule In Molecule substructure causes invalidation of internal state.
269
+ Implicit hydrogens marks will not be set if atoms in aromatic rings.
270
+ Call `kekule()` and `thiele()` in sequence to fix marks.
271
+
272
+ :param atoms: list of atoms numbers of substructure
273
+ :param recalculate_hydrogens: calculate implicit H count in substructure
274
+ """
275
+ if not atoms:
276
+ raise ValueError('empty atoms list not allowed')
277
+ if set(atoms) - self._atoms.keys():
278
+ raise ValueError('invalid atom numbers')
279
+ atoms = tuple(n for n in self if n in atoms) # save original order
280
+ sub = object.__new__(self.__class__)
281
+ sub._name = sub._meta = sub._changed = None
282
+ sub._atoms = {n: self._atoms[n].copy(hydrogens=not recalculate_hydrogens, stereo=True) for n in atoms}
283
+ sub._bonds = sb = {}
284
+ for n in atoms:
285
+ sb[n] = sbn = {}
286
+ for m, bond in self._bonds[n].items():
287
+ if m in sb: # bond partially exists. need back-connection.
288
+ sbn[m] = sb[m][n]
289
+ elif m in atoms:
290
+ sbn[m] = bond.copy(stereo=True)
291
+ sub.fix_structure(recalculate_hydrogens=recalculate_hydrogens)
292
+ sub.fix_stereo()
293
+ return sub
294
+
295
+ def augmented_substructure(self, atoms: Iterable[int], deep: int = 1, **kwargs) -> 'MoleculeContainer':
296
+ """
297
+ Create substructure containing atoms and their neighbors
298
+
299
+ :param atoms: list of core atoms in graph
300
+ :param deep: number of bonds between atoms and neighbors
301
+ """
302
+ return self.substructure(self._augmented_substructure(atoms, deep)[-1], **kwargs)
303
+
304
+ def augmented_substructures(self, atoms: Iterable[int], deep: int = 1, **kwargs) -> List['MoleculeContainer']:
305
+ """
306
+ Create list of substructures containing atoms and their neighbors
307
+
308
+ :param atoms: list of core atoms in graph
309
+ :param deep: number of bonds between atoms and neighbors
310
+ :return: list of graphs containing atoms, atoms + first circle, atoms + 1st + 2nd,
311
+ etc up to deep or while new nodes available
312
+ """
313
+ return [self.substructure(a, **kwargs) for a in self._augmented_substructure(atoms, deep)]
314
+
315
+ def split(self) -> List['MoleculeContainer']:
316
+ """
317
+ Split disconnected structure to connected substructures
318
+ """
319
+ return [self.substructure(c, recalculate_hydrogens=False) for c in self.connected_components]
320
+
321
+ def compose(self, other: 'MoleculeContainer') -> 'CGRContainer':
322
+ """
323
+ Compose 2 graphs to CGR.
324
+ """
325
+ if not isinstance(other, MoleculeContainer):
326
+ raise TypeError('MoleculeContainer expected')
327
+ bonds = []
328
+ adj = defaultdict(lambda: defaultdict(lambda: [None, None]))
329
+ common = self._atoms.keys() & other._atoms.keys()
330
+
331
+ h = CGRContainer()
332
+ ha = h._atoms
333
+ hb = h._bonds
334
+
335
+ for n in self._atoms.keys() - common: # cleavage atoms
336
+ ha[n] = DynamicElement.from_atom(self._atoms[n])
337
+ hb[n] = {}
338
+ for m, bond in self._bonds[n].items():
339
+ if m not in ha:
340
+ if m in common: # bond to common atoms is broken bond
341
+ bond = DynamicBond(bond.order, None)
342
+ else:
343
+ bond = DynamicBond.from_bond(bond)
344
+ bonds.append((n, m, bond))
345
+ for n in other._atoms.keys() - common: # coupling atoms
346
+ ha[n] = DynamicElement.from_atom(other._atoms[n])
347
+ hb[n] = {}
348
+
349
+ for m, bond in other._bonds[n].items():
350
+ if m not in ha:
351
+ if m in common: # bond to common atoms is formed bond
352
+ bond = DynamicBond(None, bond.order)
353
+ else:
354
+ bond = DynamicBond.from_bond(bond)
355
+ bonds.append((n, m, bond))
356
+ for n in common:
357
+ an = adj[n]
358
+ for m, bond in self._bonds[n].items():
359
+ if m in common:
360
+ an[m][0] = bond.order
361
+ for m, bond in other._bonds[n].items():
362
+ if m in common:
363
+ an[m][1] = bond.order
364
+ for n in common:
365
+ ha[n] = DynamicElement.from_atoms(self._atoms[n], other._atoms[n])
366
+ hb[n] = {}
367
+
368
+ for m, (o1, o2) in adj[n].items():
369
+ if m not in ha:
370
+ bonds.append((n, m, DynamicBond(o1, o2)))
371
+
372
+ for n, m, bond in bonds:
373
+ hb[n][m] = hb[m][n] = bond
374
+ return h
375
+
376
+ def pack(self, *, compressed=True, check=True, version=2, order: List[int] = None) -> bytes:
377
+ """
378
+ Pack into compressed bytes.
379
+
380
+ Note:
381
+
382
+ * Less than 4096 atoms supported. Atoms mapping should be in range 1-4095.
383
+ * Implicit hydrogens count should be in range 0-6 or unspecified.
384
+ * Isotope shift should be in range -15 - 15 relatively chython.files._mdl.mol.common_isotopes
385
+ * Atoms neighbors should be in range 0-15
386
+
387
+ Format V2 specification::
388
+
389
+ Big endian bytes order
390
+ 8 bit - 0x02 (format specification version)
391
+ 12 bit - number of atoms
392
+ 12 bit - cis/trans stereo block size
393
+ Atom block 9 bytes (repeated):
394
+ 12 bit - atom number
395
+ 4 bit - number of neighbors
396
+ 2 bit tetrahedron sign (00 - not stereo, 10 or 11 - has stereo)
397
+ 2 bit - allene sign
398
+ 5 bit - isotope (00000 - not specified, over = isotope - common_isotope + 16)
399
+ 7 bit - atomic number (<=118)
400
+ 32 bit - XY float16 coordinates
401
+ 3 bit - hydrogens (0-7). Note: 7 == None
402
+ 4 bit - charge (charge + 4. possible range -4 - 4)
403
+ 1 bit - radical state
404
+ Connection table: flatten list of neighbors. neighbors count stored in atom block.
405
+ For example CC(=O)O - {1: [2], 2: [1, 3, 4], 3: [2], 4: [2]} >> [2, 1, 3, 4, 2, 2].
406
+ Repeated block (equal to bonds count).
407
+ 24 bit - paired 12 bit numbers.
408
+ Bonds order block 3 bit per bond zero-padded to full byte at the end.
409
+ Cis/trans data block (repeated):
410
+ 24 bit - atoms pair
411
+ 7 bit - zero padding. in future can be used for extra bond-level stereo, like atropoisomers.
412
+ 1 bit - sign
413
+
414
+ :param compressed: return zlib-compressed pack.
415
+ :param check: check molecule for format restrictions.
416
+ :param version: format version. Only V2 is supported.
417
+ :param order: atom order in V3
418
+ """
419
+ from ._pack_v2 import pack as pack_v2
420
+
421
+ if check:
422
+ bonds = self._bonds
423
+ if not bonds:
424
+ raise ValueError('Empty molecules not supported')
425
+ if max(bonds) > 4095:
426
+ raise ValueError('Big molecules not supported')
427
+ if any(len(x) > 15 for x in bonds.values()):
428
+ raise ValueError('To many neighbors not supported')
429
+
430
+ if version == 2:
431
+ data = pack_v2(self)
432
+ else:
433
+ raise ValueError('invalid specification version')
434
+ if compressed:
435
+ return compress(data, 9)
436
+ return data
437
+
438
+ def pach(self, *, compressed=True, check=True, version=2, order: List[int] = None) -> bytes:
439
+ return self.pack(compressed=compressed, check=check, version=version, order=order)
440
+
441
+ @classmethod
442
+ def pack_len(cls, data: bytes, /, *, compressed=True) -> int:
443
+ """
444
+ Returns atoms count in molecule pack.
445
+ """
446
+ if compressed:
447
+ data = decompress(data)
448
+ if data[0] not in (0, 2):
449
+ raise ValueError('invalid pack header')
450
+ return int.from_bytes(data[1:3], 'big') >> 4
451
+
452
+ @classmethod
453
+ def unpack(cls, data: Union[bytes, memoryview], /, *, compressed=True, skip_labels_calculation=False,
454
+ _return_pack_length=False) -> 'MoleculeContainer':
455
+ """
456
+ Unpack from compressed bytes.
457
+
458
+ :param compressed: decompress data before processing.
459
+ """
460
+ from ._unpack_v0v2 import unpack as unpack_v0v2
461
+
462
+ if compressed:
463
+ data = decompress(data)
464
+ if data[0] in (0, 2):
465
+ mol, cis_trans, pack_length = unpack_v0v2(data)
466
+ for n, m, s in cis_trans:
467
+ if n in mol._stereo_cis_trans_centers: # check for invalid CT data
468
+ mol.bond(*mol._stereo_cis_trans_centers[n])._stereo = s
469
+ else:
470
+ raise ValueError('invalid pack header')
471
+
472
+ if not skip_labels_calculation:
473
+ mol.calc_labels()
474
+
475
+ if _return_pack_length:
476
+ return mol, pack_length
477
+ return mol
478
+
479
+ @classmethod
480
+ def unpach(cls, data: Union[bytes, memoryview], /, *, compressed=True) -> 'MoleculeContainer':
481
+ """
482
+ Unpack from compressed bytes.
483
+ """
484
+ return cls.unpack(data, compressed=compressed)
485
+
486
+ def __bytes__(self):
487
+ return self.pack()
488
+
489
+ def _augmented_substructure(self, atoms: Iterable[int], deep: int):
490
+ atoms = set(atoms)
491
+ bonds = self._bonds
492
+ if atoms - bonds.keys():
493
+ raise ValueError('invalid atom numbers')
494
+ nodes = [atoms]
495
+ for _ in range(deep):
496
+ n = {y for x in nodes[-1] for y in bonds[x]} | nodes[-1]
497
+ if n in nodes:
498
+ break
499
+ nodes.append(n)
500
+ return nodes
501
+
502
+ def fix_structure(self, recalculate_hydrogens=True):
503
+ """
504
+ Fix molecule internal representation
505
+ """
506
+ self.calc_labels() # refresh all labels
507
+
508
+ if recalculate_hydrogens:
509
+ for n in (self._changed or self._atoms):
510
+ self.calc_implicit(n) # fix Hs count
511
+ self._changed = None
512
+
513
+ def calc_labels(self):
514
+ atoms = self._atoms
515
+ atoms_rings_sizes = self.atoms_rings_sizes # expensive: sssr based
516
+ atoms_rings = {n: set(r) for n, r in self.atoms_rings.items()}
517
+
518
+ for n, m_bond in self._bonds.items():
519
+ neighbors = 0
520
+ heteroatoms = 0
521
+ hybridization = 1
522
+ explicit_hydrogens = 0
523
+ anr = atoms_rings.get(n) or False
524
+ for m, bond in m_bond.items():
525
+ bond._in_ring = anr and (amr := atoms_rings.get(m) or False) and not anr.isdisjoint(amr) # have common rings
526
+
527
+ if bond == 8:
528
+ continue
529
+ elif bond == 4:
530
+ hybridization = 4
531
+ elif hybridization != 4:
532
+ if bond == 3:
533
+ hybridization = 3
534
+ elif bond == 2:
535
+ if hybridization == 1:
536
+ hybridization = 2
537
+ elif hybridization == 2:
538
+ hybridization = 3
539
+
540
+ neighbors += 1
541
+ if (a := atoms[m]) == H:
542
+ explicit_hydrogens += 1
543
+ elif a != C:
544
+ heteroatoms += 1
545
+ atom = atoms[n]
546
+ atom._neighbors = neighbors
547
+ atom._heteroatoms = heteroatoms
548
+ atom._hybridization = hybridization
549
+ atom._explicit_hydrogens = explicit_hydrogens
550
+
551
+ atom._in_ring = n in atoms_rings_sizes
552
+ atom._ring_sizes = atoms_rings_sizes.get(n) or set()
553
+
554
+ def calc_implicit(self, n: int):
555
+ """
556
+ Set firs possible hydrogens count based on rules
557
+ """
558
+ if (atom := self._atoms[n]) == H: # hydrogen nether has implicit H
559
+ atom._implicit_hydrogens = 0
560
+ return
561
+
562
+ explicit_sum = 0
563
+ explicit_dict = defaultdict(int)
564
+ aroma = 0
565
+ for m, bond in self._bonds[n].items():
566
+ if bond == 4: # only neutral carbon aromatic rings supported
567
+ if not atom.charge and not atom.is_radical and atom == C:
568
+ aroma += 1
569
+ else: # use `kekule()` to calculate proper implicit hydrogens count
570
+ atom._implicit_hydrogens = None
571
+ return
572
+ elif bond != 8: # any bond used for complexes
573
+ explicit_sum += bond.order
574
+ explicit_dict[(bond.order, self._atoms[m].atomic_number)] += 1
575
+
576
+ if aroma == 2:
577
+ if explicit_sum == 0: # H-Ar
578
+ atom._implicit_hydrogens = 1
579
+ elif explicit_sum == 1: # R-Ar
580
+ atom._implicit_hydrogens = 0
581
+ else: # invalid aromaticity
582
+ atom._implicit_hydrogens = None
583
+ return
584
+ elif aroma == 3: # condensed rings
585
+ if explicit_sum: # invalid aromaticity
586
+ atom._implicit_hydrogens = None
587
+ else:
588
+ atom._implicit_hydrogens = 0
589
+ return
590
+ elif aroma:
591
+ atom._implicit_hydrogens = None
592
+ return
593
+
594
+ try:
595
+ rules = atom.valence_rules(explicit_sum)
596
+ except ValenceError:
597
+ atom._implicit_hydrogens = None
598
+ return
599
+ for s, d, h in rules:
600
+ if s.issubset(explicit_dict) and all(explicit_dict[k] >= c for k, c in d.items()):
601
+ atom._implicit_hydrogens = h
602
+ return
603
+ atom._implicit_hydrogens = None # rule not found
604
+
605
+ def check_implicit(self, n: int, h: int) -> bool:
606
+ if (atom := self._atoms[n]) == H: # hydrogen nether has implicit H
607
+ return h == 0
608
+
609
+ explicit_sum = 0
610
+ explicit_dict = defaultdict(int)
611
+
612
+ for m, bond in self._bonds[n].items():
613
+ if bond == 4: # can't check aromatic rings
614
+ return False
615
+ elif bond != 8: # any bond used for complexes
616
+ explicit_sum += bond.order
617
+ explicit_dict[(bond.order, self._atoms[m].atomic_number)] += 1
618
+
619
+ try:
620
+ rules = atom.valence_rules(explicit_sum)
621
+ except ValenceError:
622
+ return False
623
+ for s, d, _h in rules:
624
+ if h == _h and s.issubset(explicit_dict) and all(explicit_dict[k] >= c for k, c in d.items()):
625
+ return True
626
+ return False
627
+
628
+ def flush_cache(self, *, keep_sssr=False, keep_components=False):
629
+ backup = {}
630
+ if keep_sssr:
631
+ # good to keep if no new bonds or bonds deletions or bonds to/from any change
632
+ for k, v in self.__dict__.items():
633
+ if k in ('sssr', 'atoms_rings', 'atoms_rings_sizes', 'not_special_connectivity', 'rings_count'):
634
+ backup[k] = v
635
+ if keep_components:
636
+ # good to keep if no new bonds or bonds deletions
637
+ if 'connected_components' in self.__dict__:
638
+ backup['connected_components'] = self.connected_components
639
+ self.__dict__ = backup
640
+
641
+ def __int__(self):
642
+ """
643
+ Total charge of molecule
644
+ """
645
+ return self.molecular_charge
646
+
647
+ def __float__(self):
648
+ return self.molecular_mass
649
+
650
+ def __xor__(self, other):
651
+ """
652
+ G ^ H is CGR generation
653
+ """
654
+ return self.compose(other)
655
+
656
+ def __and__(self, other: Iterable[int]):
657
+ """
658
+ Substructure of graph with given nodes.
659
+ """
660
+ return self.substructure(other)
661
+
662
+ def __sub__(self, other: Iterable[int]):
663
+ """
664
+ Given nodes excluded substructure of graph.
665
+ """
666
+ atoms = set(other)
667
+ if atoms - self._atoms.keys():
668
+ raise ValueError('invalid atom numbers')
669
+ atoms = self._atoms.keys() - atoms
670
+ if atoms:
671
+ return self.substructure(atoms)
672
+ raise ValueError('full substitution not allowed')
673
+
674
+ def __enter__(self):
675
+ """
676
+ Transaction of changes. Keep current state for restoring on errors.
677
+ """
678
+ self._backup = self.copy(keep_sssr=True, keep_components=True)
679
+ return self
680
+
681
+ def __exit__(self, exc_type, exc_val, exc_tb):
682
+ if exc_type: # restore state
683
+ backup = self._backup
684
+ self._atoms = backup._atoms
685
+ self._bonds = backup._bonds
686
+ self._meta = backup._meta
687
+ self._name = backup._name
688
+ self.__dict__ = backup.__dict__
689
+ else: # update internal state
690
+ self.fix_structure()
691
+ self.fix_stereo()
692
+ self._backup = None # drop backup
693
+
694
+
695
+ __all__ = ['MoleculeContainer']