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.
- chython/__init__.py +31 -0
- chython/_functions.py +69 -0
- chython/algorithms/__init__.py +18 -0
- chython/algorithms/_isomorphism.cp312-win_amd64.pyd +0 -0
- chython/algorithms/_isomorphism.pyx +189 -0
- chython/algorithms/aromatics/__init__.py +27 -0
- chython/algorithms/aromatics/_rules.py +124 -0
- chython/algorithms/aromatics/kekule.py +530 -0
- chython/algorithms/aromatics/test/__init__.py +18 -0
- chython/algorithms/aromatics/test/test_kekule.py +160 -0
- chython/algorithms/aromatics/test/test_thiele.py +263 -0
- chython/algorithms/aromatics/thiele.py +226 -0
- chython/algorithms/calculate2d/__init__.py +23 -0
- chython/algorithms/calculate2d/clean2d.js +1 -0
- chython/algorithms/calculate2d/molecule.py +149 -0
- chython/algorithms/calculate2d/reaction.py +80 -0
- chython/algorithms/depict.py +523 -0
- chython/algorithms/fingerprints/__init__.py +48 -0
- chython/algorithms/fingerprints/linear.py +208 -0
- chython/algorithms/fingerprints/morgan.py +137 -0
- chython/algorithms/fingerprints/test/__init__.py +18 -0
- chython/algorithms/fingerprints/test/test_linear.py +127 -0
- chython/algorithms/fingerprints/test/test_morgan.py +79 -0
- chython/algorithms/isomorphism.py +667 -0
- chython/algorithms/mapping/__init__.py +28 -0
- chython/algorithms/mapping/_groups.py +94 -0
- chython/algorithms/mapping/_reactions.py +72 -0
- chython/algorithms/mapping/attention.py +216 -0
- chython/algorithms/mapping/fixmapper.py +84 -0
- chython/algorithms/mapping/groups.py +137 -0
- chython/algorithms/mcs.py +202 -0
- chython/algorithms/morgan.py +82 -0
- chython/algorithms/rings.py +551 -0
- chython/algorithms/smiles.py +556 -0
- chython/algorithms/standardize/__init__.py +30 -0
- chython/algorithms/standardize/_charged.py +115 -0
- chython/algorithms/standardize/_groups.py +926 -0
- chython/algorithms/standardize/_metal_organics.py +189 -0
- chython/algorithms/standardize/_reagents.py +44 -0
- chython/algorithms/standardize/_salts.py +68 -0
- chython/algorithms/standardize/molecule.py +479 -0
- chython/algorithms/standardize/reaction.py +429 -0
- chython/algorithms/standardize/resonance.py +196 -0
- chython/algorithms/standardize/salts.py +128 -0
- chython/algorithms/standardize/saturation.py +378 -0
- chython/algorithms/standardize/test/__init__.py +18 -0
- chython/algorithms/standardize/test/test_groups.py +118 -0
- chython/algorithms/stereo.py +1222 -0
- chython/algorithms/tautomers/__init__.py +171 -0
- chython/algorithms/tautomers/_acid.py +59 -0
- chython/algorithms/tautomers/_base.py +116 -0
- chython/algorithms/tautomers/_keto_enol.py +107 -0
- chython/algorithms/tautomers/acid_base.py +214 -0
- chython/algorithms/tautomers/heteroarenes.py +108 -0
- chython/algorithms/tautomers/keto_enol.py +148 -0
- chython/algorithms/tautomers/test/__init__.py +18 -0
- chython/algorithms/tautomers/test/test_tautomers.py +80 -0
- chython/algorithms/test/__init__.py +18 -0
- chython/algorithms/test/test_isomorphism.py +58 -0
- chython/algorithms/test/test_smiles.py +76 -0
- chython/algorithms/x3dom.py +355 -0
- chython/containers/__init__.py +43 -0
- chython/containers/_pack_v2.cp312-win_amd64.pyd +0 -0
- chython/containers/_pack_v2.pyx +271 -0
- chython/containers/_unpack_v0v2.cp312-win_amd64.pyd +0 -0
- chython/containers/_unpack_v0v2.pyx +300 -0
- chython/containers/bonds.py +255 -0
- chython/containers/cgr.py +115 -0
- chython/containers/graph.py +200 -0
- chython/containers/molecule.py +695 -0
- chython/containers/query.py +66 -0
- chython/containers/reaction.py +324 -0
- chython/exceptions.py +114 -0
- chython/files/MRVrw.py +523 -0
- chython/files/PDBrw.py +257 -0
- chython/files/RDFrw.py +298 -0
- chython/files/SDFrw.py +221 -0
- chython/files/__init__.py +29 -0
- chython/files/_convert.py +207 -0
- chython/files/_mapping.py +117 -0
- chython/files/_xyz.cp312-win_amd64.pyd +0 -0
- chython/files/_xyz.pyx +74 -0
- chython/files/daylight/__init__.py +24 -0
- chython/files/daylight/parser.py +157 -0
- chython/files/daylight/smarts.py +107 -0
- chython/files/daylight/smiles.py +245 -0
- chython/files/daylight/test/__init__.py +18 -0
- chython/files/daylight/test/test_daylight_smarts.py +289 -0
- chython/files/daylight/test/test_daylight_smiles.py +101 -0
- chython/files/daylight/test/test_parser.py +118 -0
- chython/files/daylight/test/test_tokenize.py +77 -0
- chython/files/daylight/tokenize.py +376 -0
- chython/files/libinchi/__init__.py +22 -0
- chython/files/libinchi/libinchi.dll +0 -0
- chython/files/libinchi/wrapper.py +555 -0
- chython/files/mdl/__init__.py +25 -0
- chython/files/mdl/emol.py +209 -0
- chython/files/mdl/erxn.py +67 -0
- chython/files/mdl/mol.py +144 -0
- chython/files/mdl/read.py +225 -0
- chython/files/mdl/rxn.py +67 -0
- chython/files/mdl/stereo.py +63 -0
- chython/files/mdl/write.py +165 -0
- chython/files/xyz.py +78 -0
- chython/periodictable/__init__.py +69 -0
- chython/periodictable/base/__init__.py +25 -0
- chython/periodictable/base/dynamic.py +149 -0
- chython/periodictable/base/element.py +452 -0
- chython/periodictable/base/groups.py +90 -0
- chython/periodictable/base/periods.py +46 -0
- chython/periodictable/base/query.py +473 -0
- chython/periodictable/base/vector.py +119 -0
- chython/periodictable/groupI.py +252 -0
- chython/periodictable/groupII.py +223 -0
- chython/periodictable/groupIII.py +1154 -0
- chython/periodictable/groupIV.py +210 -0
- chython/periodictable/groupIX.py +196 -0
- chython/periodictable/groupV.py +192 -0
- chython/periodictable/groupVI.py +183 -0
- chython/periodictable/groupVII.py +162 -0
- chython/periodictable/groupVIII.py +161 -0
- chython/periodictable/groupX.py +166 -0
- chython/periodictable/groupXI.py +164 -0
- chython/periodictable/groupXII.py +158 -0
- chython/periodictable/groupXIII.py +239 -0
- chython/periodictable/groupXIV.py +262 -0
- chython/periodictable/groupXV.py +276 -0
- chython/periodictable/groupXVI.py +457 -0
- chython/periodictable/groupXVII.py +324 -0
- chython/periodictable/groupXVIII.py +260 -0
- chython/reactor/__init__.py +24 -0
- chython/reactor/base.py +212 -0
- chython/reactor/deprotection.py +565 -0
- chython/reactor/groups.py +128 -0
- chython/reactor/reactions/__init__.py +157 -0
- chython/reactor/reactions/_amidation.py +58 -0
- chython/reactor/reactions/_amine_isocyanate.py +52 -0
- chython/reactor/reactions/_buchwald_hartwig.py +50 -0
- chython/reactor/reactions/_esterification.py +49 -0
- chython/reactor/reactions/_macmillan.py +43 -0
- chython/reactor/reactions/_reductive_amination.py +50 -0
- chython/reactor/reactions/_sonogashira.py +49 -0
- chython/reactor/reactions/_sulfonamidation.py +52 -0
- chython/reactor/reactions/_suzuki_miyaura.py +88 -0
- chython/reactor/reactions/ufe.py +99 -0
- chython/reactor/reactor.py +170 -0
- chython/reactor/retro/__init__.py +77 -0
- chython/reactor/retro/_amidation.py +39 -0
- chython/reactor/retro/_aryl_amination.py +32 -0
- chython/reactor/retro/_mitsunobu.py +49 -0
- chython/reactor/retro/_sonogashira.py +49 -0
- chython/reactor/retro/_suzuki_miyaura.py +54 -0
- chython/reactor/scaffold.py +122 -0
- chython/reactor/test/__init__.py +18 -0
- chython/reactor/test/test_deprotection.py +57 -0
- chython/reactor/test/test_reactor.py +40 -0
- chython/reactor/test/test_scaffold.py +57 -0
- chython/reactor/test/test_transformer.py +40 -0
- chython/reactor/transformer.py +62 -0
- chython/utils/__init__.py +57 -0
- chython/utils/free_wilson.py +114 -0
- chython/utils/grid.py +130 -0
- chython/utils/rdkit.py +165 -0
- chython/utils/retro.py +139 -0
- chython/utils/svg.py +57 -0
- chython/utils/test/__init__.py +18 -0
- chython/utils/test/test_rdkit.py +71 -0
- chython-2.4.dist-info/LICENSE +165 -0
- chython-2.4.dist-info/METADATA +101 -0
- chython-2.4.dist-info/RECORD +171 -0
- 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']
|