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,667 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
#
|
|
3
|
+
# Copyright 2018-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 array import array
|
|
20
|
+
from collections import defaultdict, deque
|
|
21
|
+
from functools import cached_property, partial
|
|
22
|
+
from io import BytesIO
|
|
23
|
+
from itertools import permutations
|
|
24
|
+
from struct import Struct
|
|
25
|
+
from typing import Any, Collection, Dict, Iterator, Optional, TYPE_CHECKING, Union
|
|
26
|
+
from .._functions import lazy_product
|
|
27
|
+
from ..periodictable import Element, Query, AnyElement, AnyMetal, ListElement, QueryElement, ExtendedQuery
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
if TYPE_CHECKING:
|
|
31
|
+
from chython.containers.graph import Graph
|
|
32
|
+
from chython.containers import MoleculeContainer
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
header_struct = Struct('I')
|
|
36
|
+
m_atom_struct = Struct('QQQQIII')
|
|
37
|
+
q_atom_struct = Struct('QQQQIIIII')
|
|
38
|
+
bond_struct = Struct('QI')
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class Isomorphism:
|
|
42
|
+
__slots__ = ()
|
|
43
|
+
|
|
44
|
+
def __lt__(self, other):
|
|
45
|
+
if len(self) >= len(other):
|
|
46
|
+
return False
|
|
47
|
+
return self.is_substructure(other)
|
|
48
|
+
|
|
49
|
+
def __le__(self, other):
|
|
50
|
+
return self.is_substructure(other)
|
|
51
|
+
|
|
52
|
+
def __gt__(self, other):
|
|
53
|
+
if len(self) <= len(other):
|
|
54
|
+
return False
|
|
55
|
+
return other.is_substructure(self)
|
|
56
|
+
|
|
57
|
+
def __ge__(self, other):
|
|
58
|
+
return other.is_substructure(self)
|
|
59
|
+
|
|
60
|
+
def is_substructure(self, other, /) -> bool:
|
|
61
|
+
"""
|
|
62
|
+
Test self is substructure of other
|
|
63
|
+
"""
|
|
64
|
+
try:
|
|
65
|
+
next(self.get_mapping(other, automorphism_filter=False))
|
|
66
|
+
except StopIteration:
|
|
67
|
+
return False
|
|
68
|
+
return True
|
|
69
|
+
|
|
70
|
+
def is_equal(self, other, /) -> bool:
|
|
71
|
+
"""
|
|
72
|
+
Test self is same structure as other
|
|
73
|
+
"""
|
|
74
|
+
if len(self) != len(other):
|
|
75
|
+
return False
|
|
76
|
+
try:
|
|
77
|
+
next(self.get_mapping(other, automorphism_filter=False))
|
|
78
|
+
except StopIteration:
|
|
79
|
+
return False
|
|
80
|
+
return True
|
|
81
|
+
|
|
82
|
+
def _get_mapping(self, other: 'MoleculeContainer', /, *, automorphism_filter=True, searching_scope=None,
|
|
83
|
+
components=None, get_mapping=None) -> Iterator[Dict[int, int]]:
|
|
84
|
+
if components is None: # ad-hoc for QueryContainer
|
|
85
|
+
components, closures = self._compiled_query
|
|
86
|
+
get_mapping = partial(_get_mapping, query_closures=closures, o_atoms=other._atoms, o_bonds=other._bonds)
|
|
87
|
+
|
|
88
|
+
if searching_scope is not None and not isinstance(searching_scope, set):
|
|
89
|
+
searching_scope = set(searching_scope)
|
|
90
|
+
|
|
91
|
+
seen = set()
|
|
92
|
+
if len(components) == 1:
|
|
93
|
+
for candidate in other.connected_components:
|
|
94
|
+
if searching_scope:
|
|
95
|
+
candidate = searching_scope.intersection(candidate)
|
|
96
|
+
if not candidate:
|
|
97
|
+
continue
|
|
98
|
+
for mapping in get_mapping(components[0], scope=candidate):
|
|
99
|
+
if automorphism_filter:
|
|
100
|
+
atoms = frozenset(mapping.values())
|
|
101
|
+
if atoms in seen:
|
|
102
|
+
continue
|
|
103
|
+
seen.add(atoms)
|
|
104
|
+
yield mapping
|
|
105
|
+
else:
|
|
106
|
+
for candidates in permutations(other.connected_components, len(components)):
|
|
107
|
+
mappers = []
|
|
108
|
+
for component, candidate in zip(components, candidates):
|
|
109
|
+
if searching_scope:
|
|
110
|
+
candidate = searching_scope.intersection(candidate)
|
|
111
|
+
if not candidate:
|
|
112
|
+
break
|
|
113
|
+
mappers.append(get_mapping(component, scope=candidate))
|
|
114
|
+
else:
|
|
115
|
+
for match in lazy_product(*mappers):
|
|
116
|
+
mapping = match[0].copy()
|
|
117
|
+
for m in match[1:]:
|
|
118
|
+
mapping.update(m)
|
|
119
|
+
if automorphism_filter:
|
|
120
|
+
atoms = frozenset(mapping.values())
|
|
121
|
+
if atoms in seen:
|
|
122
|
+
continue
|
|
123
|
+
seen.add(atoms)
|
|
124
|
+
yield mapping
|
|
125
|
+
|
|
126
|
+
@cached_property
|
|
127
|
+
def _compiled_query(self: 'Graph'):
|
|
128
|
+
return _compile_query(self._atoms, self._bonds)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
class MoleculeIsomorphism(Isomorphism):
|
|
132
|
+
__slots__ = ()
|
|
133
|
+
|
|
134
|
+
def __contains__(self: 'MoleculeContainer', other: Union[Element, Query, str]):
|
|
135
|
+
"""
|
|
136
|
+
Atom in Structure test.
|
|
137
|
+
"""
|
|
138
|
+
if isinstance(other, str):
|
|
139
|
+
return any(other == a.atomic_symbol for _, a in self.atoms())
|
|
140
|
+
return any(other == a for _, a in self.atoms())
|
|
141
|
+
|
|
142
|
+
def is_automorphic(self):
|
|
143
|
+
"""
|
|
144
|
+
Test for automorphism symmetry of graph.
|
|
145
|
+
"""
|
|
146
|
+
try:
|
|
147
|
+
next(self.get_automorphism_mapping())
|
|
148
|
+
except StopIteration:
|
|
149
|
+
return False
|
|
150
|
+
return True
|
|
151
|
+
|
|
152
|
+
def get_automorphism_mapping(self: 'MoleculeContainer') -> Iterator[Dict[int, int]]:
|
|
153
|
+
"""
|
|
154
|
+
Iterator of all possible automorphism mappings.
|
|
155
|
+
"""
|
|
156
|
+
return _get_automorphism_mapping(self._chiral_morgan, self._bonds)
|
|
157
|
+
|
|
158
|
+
def get_mapping(self, other: 'MoleculeContainer', /, *, automorphism_filter: bool = True,
|
|
159
|
+
searching_scope: Optional[Collection[int]] = None, match_stereo: bool = False):
|
|
160
|
+
"""
|
|
161
|
+
Get self to other Molecule substructure mapping generator.
|
|
162
|
+
|
|
163
|
+
:param other: Molecule
|
|
164
|
+
:param automorphism_filter: Skip matches to the same atoms.
|
|
165
|
+
:param searching_scope: substructure atoms list to localize isomorphism.
|
|
166
|
+
:param match_stereo: test stereo labels matches. slow algorithm, thus disabled by default.
|
|
167
|
+
"""
|
|
168
|
+
if not isinstance(other, MoleculeIsomorphism):
|
|
169
|
+
raise TypeError('MoleculeContainer expected')
|
|
170
|
+
|
|
171
|
+
for mapping in self._get_mapping(other, automorphism_filter=automorphism_filter or match_stereo,
|
|
172
|
+
searching_scope=searching_scope):
|
|
173
|
+
if match_stereo:
|
|
174
|
+
sub = other.substructure(mapping.values()) # extract matched subgraph
|
|
175
|
+
fm = self.get_fast_mapping(sub)
|
|
176
|
+
if not fm: # check mor matching with stereo labels too
|
|
177
|
+
continue
|
|
178
|
+
yield fm
|
|
179
|
+
if not automorphism_filter:
|
|
180
|
+
for auto in sub.get_automorphism_mapping(): # enumerate all possible automorphisms
|
|
181
|
+
yield {n: auto[m] for n, m in fm.items()}
|
|
182
|
+
else:
|
|
183
|
+
yield mapping
|
|
184
|
+
|
|
185
|
+
def get_fast_mapping(self, other: 'MoleculeContainer') -> Optional[Dict[int, int]]:
|
|
186
|
+
"""
|
|
187
|
+
Get self to other fast (suboptimal) structure mapping.
|
|
188
|
+
Only one possible atoms mapping returned.
|
|
189
|
+
Effective only for big molecules.
|
|
190
|
+
"""
|
|
191
|
+
if isinstance(other, MoleculeIsomorphism):
|
|
192
|
+
if len(self) != len(other):
|
|
193
|
+
return
|
|
194
|
+
so = self.smiles_atoms_order
|
|
195
|
+
oo = other.smiles_atoms_order
|
|
196
|
+
if self != other:
|
|
197
|
+
return
|
|
198
|
+
return dict(zip(so, oo))
|
|
199
|
+
raise TypeError('MoleculeContainer expected')
|
|
200
|
+
|
|
201
|
+
@cached_property
|
|
202
|
+
def _cython_compiled_structure(self: 'MoleculeContainer'):
|
|
203
|
+
# long I:
|
|
204
|
+
# bond: single, double, triple, aromatic, special = 5 bit
|
|
205
|
+
# bond in ring: 2 bit
|
|
206
|
+
# atom: H-Ba: 56 bit
|
|
207
|
+
# transfer bit
|
|
208
|
+
|
|
209
|
+
# long II:
|
|
210
|
+
# atom La-Mc: 59 bit
|
|
211
|
+
# Lv-Ts-Og: 3 elements packed into 1 bit.
|
|
212
|
+
# hybridizations: 1-4 = 4 bit
|
|
213
|
+
|
|
214
|
+
# long III:
|
|
215
|
+
# isotope: not specified, isotope - common_isotope = -8 - +8 = 18 bit
|
|
216
|
+
# is_radical: 2 bit
|
|
217
|
+
# charge: -4 - +4: 9 bit
|
|
218
|
+
# implicit_hydrogens: 0-4 = 5 bit
|
|
219
|
+
# neighbors: 0-14 = 15 bit
|
|
220
|
+
# heteroatoms: 0-14 = 15 bit
|
|
221
|
+
|
|
222
|
+
# long IV:
|
|
223
|
+
# ring_sizes: not-in-ring bit, 3-atom ring, 4-...., 65-atom ring
|
|
224
|
+
|
|
225
|
+
mapping = {}
|
|
226
|
+
numbers = []
|
|
227
|
+
bits1 = []
|
|
228
|
+
bits2 = []
|
|
229
|
+
bits3 = []
|
|
230
|
+
bits4 = []
|
|
231
|
+
for i, (n, a) in enumerate(self.atoms()):
|
|
232
|
+
mapping[n] = i
|
|
233
|
+
numbers.append(n)
|
|
234
|
+
v2 = 1 << (a.hybridization - 1)
|
|
235
|
+
if (an := a.atomic_number) > 56:
|
|
236
|
+
if an > 116: # Ts, Og
|
|
237
|
+
an = 116
|
|
238
|
+
v1 = 1 # transfer bit
|
|
239
|
+
v2 |= 1 << (120 - an)
|
|
240
|
+
else:
|
|
241
|
+
v1 = 1 << (57 - an)
|
|
242
|
+
|
|
243
|
+
if a.isotope:
|
|
244
|
+
v3 = 1 << (a.isotope - a.mdl_isotope + 54)
|
|
245
|
+
if a.is_radical:
|
|
246
|
+
v3 |= 0x200000000000
|
|
247
|
+
else:
|
|
248
|
+
v3 |= 0x100000000000
|
|
249
|
+
elif a.is_radical:
|
|
250
|
+
v3 = 0x8000200000000000
|
|
251
|
+
else:
|
|
252
|
+
v3 = 0x8000100000000000
|
|
253
|
+
|
|
254
|
+
v3 |= 1 << (a.charge + 39)
|
|
255
|
+
v3 |= 1 << ((a.implicit_hydrogens or 0) + 30)
|
|
256
|
+
v3 |= 1 << (a.neighbors + 15)
|
|
257
|
+
v3 |= 1 << a.heteroatoms
|
|
258
|
+
|
|
259
|
+
if a.ring_sizes:
|
|
260
|
+
v4 = 0
|
|
261
|
+
for r in a.ring_sizes:
|
|
262
|
+
if r > 65: # big rings not supported
|
|
263
|
+
continue
|
|
264
|
+
v4 |= 1 << (65 - r)
|
|
265
|
+
if not v4: # only 65+ rings. set as rings-free.
|
|
266
|
+
v4 = 0x8000000000000000
|
|
267
|
+
else: # not in rings
|
|
268
|
+
v4 = 0x8000000000000000
|
|
269
|
+
|
|
270
|
+
bits1.append(v1)
|
|
271
|
+
bits2.append(v2)
|
|
272
|
+
bits3.append(v3)
|
|
273
|
+
bits4.append(v4)
|
|
274
|
+
|
|
275
|
+
o_from = [0] * len(mapping)
|
|
276
|
+
o_to = [0] * len(mapping)
|
|
277
|
+
indices = [0] * self.bonds_count * 2
|
|
278
|
+
bonds = [0] * self.bonds_count * 2
|
|
279
|
+
start = 0
|
|
280
|
+
for n, ms in self._bonds.items():
|
|
281
|
+
i = mapping[n]
|
|
282
|
+
o_from[i] = start
|
|
283
|
+
for j, (m, b) in enumerate(ms.items(), start):
|
|
284
|
+
indices[j] = x = mapping[m]
|
|
285
|
+
v = bits1[x]
|
|
286
|
+
if b == 1:
|
|
287
|
+
v |= 0x0800000000000000
|
|
288
|
+
elif b == 2:
|
|
289
|
+
v |= 0x1000000000000000
|
|
290
|
+
elif b == 3:
|
|
291
|
+
v |= 0x2000000000000000
|
|
292
|
+
elif b == 4:
|
|
293
|
+
v |= 0x4000000000000000
|
|
294
|
+
else:
|
|
295
|
+
v |= 0x8000000000000000
|
|
296
|
+
v |= 0x0400000000000000 if b.in_ring else 0x0200000000000000
|
|
297
|
+
bonds[j] = v
|
|
298
|
+
start += len(ms)
|
|
299
|
+
o_to[i] = start
|
|
300
|
+
|
|
301
|
+
buffer = BytesIO()
|
|
302
|
+
buffer.write(header_struct.pack(len(numbers)))
|
|
303
|
+
for x in zip(bits1, bits2, bits3, bits4, o_from, o_to, numbers):
|
|
304
|
+
buffer.write(m_atom_struct.pack(*x))
|
|
305
|
+
for x in zip(bonds, indices):
|
|
306
|
+
buffer.write(bond_struct.pack(*x))
|
|
307
|
+
return buffer.getvalue()
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
class QueryIsomorphism(Isomorphism):
|
|
311
|
+
__slots__ = ()
|
|
312
|
+
|
|
313
|
+
def get_mapping(self, other: 'MoleculeContainer', /, *, automorphism_filter: bool = True,
|
|
314
|
+
searching_scope: Optional[Collection[int]] = None, _cython=True):
|
|
315
|
+
"""
|
|
316
|
+
Get Query to Molecule substructure mapping generator.
|
|
317
|
+
|
|
318
|
+
:param other: Molecule
|
|
319
|
+
:param automorphism_filter: Skip matches to the same atoms.
|
|
320
|
+
:param searching_scope: substructure atoms list to localize isomorphism.
|
|
321
|
+
"""
|
|
322
|
+
# _cython - by default cython implementation enabled.
|
|
323
|
+
# disable it by overriding method if Query Atoms or Containers logic changed.
|
|
324
|
+
# Lv, Ts and Og in cython optimized mode treated as equal.
|
|
325
|
+
if not isinstance(other, MoleculeIsomorphism):
|
|
326
|
+
raise TypeError('MoleculeContainer expected')
|
|
327
|
+
|
|
328
|
+
if _cython:
|
|
329
|
+
try: # windows? ;)
|
|
330
|
+
from ._isomorphism import get_mapping as _cython_get_mapping
|
|
331
|
+
except ImportError:
|
|
332
|
+
components = get_mapping = None
|
|
333
|
+
else:
|
|
334
|
+
components = self._cython_compiled_query # override to cython data
|
|
335
|
+
|
|
336
|
+
def get_mapping(query, scope):
|
|
337
|
+
return _cython_get_mapping(query, other._cython_compiled_structure,
|
|
338
|
+
array('I', [n in scope for n in other]))
|
|
339
|
+
else:
|
|
340
|
+
components = get_mapping = None
|
|
341
|
+
|
|
342
|
+
for mapping in self._get_mapping(other, automorphism_filter=automorphism_filter, searching_scope=searching_scope,
|
|
343
|
+
components=components, get_mapping=get_mapping):
|
|
344
|
+
reverse = None
|
|
345
|
+
# test stereo labels matches
|
|
346
|
+
for n, a in self.atoms():
|
|
347
|
+
if not isinstance(a, ExtendedQuery) or a.stereo is None:
|
|
348
|
+
continue # non-chiral atom matches any atom. no need for checks
|
|
349
|
+
m = mapping[n]
|
|
350
|
+
if other.atom(m).stereo is None: # stereo in query should match only stereo atom
|
|
351
|
+
break # reject mapping
|
|
352
|
+
|
|
353
|
+
if m in other.stereogenic_tetrahedrons:
|
|
354
|
+
if other._translate_tetrahedron_sign(m, [mapping[x] for x in self._bonds[n]]) != a.stereo:
|
|
355
|
+
break # stereo sign doesn't match
|
|
356
|
+
else: # allene case
|
|
357
|
+
if reverse is None:
|
|
358
|
+
reverse = {m: n for n, m in mapping.items()}
|
|
359
|
+
ot1, ot2 = other._stereo_allenes_terminals[m] # get terminal atoms
|
|
360
|
+
on1, om1, on2, om2 = other.stereogenic_allenes[m] # get neighbors
|
|
361
|
+
t1, t2 = reverse[ot1], reverse[ot2]
|
|
362
|
+
env = (reverse.get(on1), reverse.get(om1), reverse.get(on2), reverse.get(om2))
|
|
363
|
+
n1 = mapping[next(x for x in self._bonds[t1] if x in env)]
|
|
364
|
+
m1 = mapping[next(x for x in self._bonds[t2] if x in env)]
|
|
365
|
+
if other._translate_allene_sign(m, n1, m1) != a.stereo:
|
|
366
|
+
break
|
|
367
|
+
else:
|
|
368
|
+
for n, m, b in self.bonds():
|
|
369
|
+
if b.stereo is None:
|
|
370
|
+
continue
|
|
371
|
+
on, om = mapping[n], mapping[m]
|
|
372
|
+
if other.bond(on, om).stereo is None: # chiral query bond matches only chiral molecule bond
|
|
373
|
+
break
|
|
374
|
+
if reverse is None:
|
|
375
|
+
reverse = {m: n for n, m in mapping.items()}
|
|
376
|
+
|
|
377
|
+
ot1, ot2 = ots = other._stereo_cis_trans_terminals[on] # get terminal atoms
|
|
378
|
+
on1, om1, on2, om2 = other.stereogenic_cis_trans[ots] # get neighbors
|
|
379
|
+
t1, t2 = reverse[ot1], reverse[ot2]
|
|
380
|
+
env = (reverse.get(on1), reverse.get(om1), reverse.get(on2), reverse.get(om2))
|
|
381
|
+
n1 = mapping[next(x for x in self._bonds[t1] if x in env)]
|
|
382
|
+
m1 = mapping[next(x for x in self._bonds[t2] if x in env)]
|
|
383
|
+
if other._translate_cis_trans_sign(ot1, ot2, n1, m1) != b.stereo:
|
|
384
|
+
break
|
|
385
|
+
else:
|
|
386
|
+
yield mapping
|
|
387
|
+
|
|
388
|
+
@cached_property
|
|
389
|
+
def _cython_compiled_query(self):
|
|
390
|
+
# long I:
|
|
391
|
+
# bond: single, double, triple, aromatic, special = 5 bit
|
|
392
|
+
# bond in ring: 2 bit
|
|
393
|
+
# atom: H-Ba: 56 bit
|
|
394
|
+
# transfer bit
|
|
395
|
+
|
|
396
|
+
# long II:
|
|
397
|
+
# atom La-Mc: 59 bit
|
|
398
|
+
# Lv-Ts-Og: 3 elements packed into 1 bit.
|
|
399
|
+
# hybridizations: 1-4 = 4 bit
|
|
400
|
+
|
|
401
|
+
# long III:
|
|
402
|
+
# isotope: not specified, isotope - common_isotope = -8 - +8 = 18 bit
|
|
403
|
+
# is_radical: 2 bit
|
|
404
|
+
# charge: -4 - +4: 9 bit
|
|
405
|
+
# implicit_hydrogens: 0-4 = 5 bit
|
|
406
|
+
# neighbors: 0-14 = 15 bit
|
|
407
|
+
# heteroatoms: 0-14 = 15 bit
|
|
408
|
+
|
|
409
|
+
# long IV:
|
|
410
|
+
# ring_sizes: not-in-ring bit, 3-atom ring, 4-...., 65-atom ring
|
|
411
|
+
|
|
412
|
+
# int V: bonds closures
|
|
413
|
+
# padding: 1 bit
|
|
414
|
+
# bond: single, double, triple, aromatic, special = 5 bit
|
|
415
|
+
# bond in ring: 2 bit
|
|
416
|
+
|
|
417
|
+
_components, _closures = self._compiled_query
|
|
418
|
+
components = []
|
|
419
|
+
for c in _components:
|
|
420
|
+
mapping = {n: i for i, (n, *_) in enumerate(c)}
|
|
421
|
+
masks1 = []
|
|
422
|
+
masks2 = []
|
|
423
|
+
masks3 = []
|
|
424
|
+
masks4 = []
|
|
425
|
+
for *_, a, b in c:
|
|
426
|
+
if isinstance(a, AnyMetal): # isotope, radical, charge, hydrogens and heteroatoms states ignored
|
|
427
|
+
# except 1, 2, 5, 6, 7, 8, 9, 10, 14, 15, 16, 17, 18, 32, 33, 34, 35, 36, 51, 52, 53, 54, 85
|
|
428
|
+
v1 = 0x0060707ffc1fff87
|
|
429
|
+
v2 = 0xfffffff7fffffff0
|
|
430
|
+
v3 = 0xffffffffc0007fff
|
|
431
|
+
v4 = 0xffffffffffffffff
|
|
432
|
+
else:
|
|
433
|
+
if isinstance(a, AnyElement):
|
|
434
|
+
v1 = 0x01ffffffffffffff
|
|
435
|
+
v2 = 0xfffffffffffffff0
|
|
436
|
+
else:
|
|
437
|
+
if isinstance(a, ListElement):
|
|
438
|
+
v1 = v2 = 0
|
|
439
|
+
for n in a.atomic_numbers:
|
|
440
|
+
if n > 56:
|
|
441
|
+
if n > 116: # Ts, Og
|
|
442
|
+
n = 116
|
|
443
|
+
v1 |= 1 # set transfer bit
|
|
444
|
+
v2 |= 1 << (120 - n)
|
|
445
|
+
else:
|
|
446
|
+
v1 |= 1 << (57 - n)
|
|
447
|
+
elif (n := a.atomic_number) > 56:
|
|
448
|
+
if n > 116: # Ts, Og
|
|
449
|
+
n = 116
|
|
450
|
+
v1 = 1 # transfer bit
|
|
451
|
+
v2 = 1 << (120 - n)
|
|
452
|
+
else:
|
|
453
|
+
v1 = 1 << (57 - n)
|
|
454
|
+
v2 = 0
|
|
455
|
+
if isinstance(a, QueryElement) and a.isotope:
|
|
456
|
+
v3 = 1 << (a.isotope - a.mdl_isotope + 54)
|
|
457
|
+
if a.is_radical:
|
|
458
|
+
v3 |= 0x200000000000
|
|
459
|
+
else:
|
|
460
|
+
v3 |= 0x100000000000
|
|
461
|
+
elif a.is_radical: # any isotope
|
|
462
|
+
v3 = 0xffffe00000000000
|
|
463
|
+
else:
|
|
464
|
+
v3 = 0xffffd00000000000
|
|
465
|
+
|
|
466
|
+
v3 |= 1 << (a.charge + 39)
|
|
467
|
+
|
|
468
|
+
if not a.implicit_hydrogens:
|
|
469
|
+
v3 |= 0x7c0000000
|
|
470
|
+
else:
|
|
471
|
+
for h in a.implicit_hydrogens:
|
|
472
|
+
v3 |= 1 << (h + 30)
|
|
473
|
+
|
|
474
|
+
if not a.heteroatoms:
|
|
475
|
+
v3 |= 0x7fff
|
|
476
|
+
else:
|
|
477
|
+
for n in a.heteroatoms:
|
|
478
|
+
v3 |= 1 << n
|
|
479
|
+
|
|
480
|
+
if a.ring_sizes:
|
|
481
|
+
if a.ring_sizes[0]:
|
|
482
|
+
v4 = 0
|
|
483
|
+
for r in a.ring_sizes:
|
|
484
|
+
if r > 65: # big rings not supported
|
|
485
|
+
continue
|
|
486
|
+
v4 |= 1 << (65 - r)
|
|
487
|
+
if not v4: # only 65+ rings. set as rings-free.
|
|
488
|
+
v4 = 0x8000000000000000
|
|
489
|
+
else: # not in rings
|
|
490
|
+
v4 = 0x8000000000000000
|
|
491
|
+
else: # any rings
|
|
492
|
+
v4 = 0xffffffffffffffff
|
|
493
|
+
|
|
494
|
+
if not a.neighbors:
|
|
495
|
+
v3 |= 0x3fff8000
|
|
496
|
+
else:
|
|
497
|
+
for n in a.neighbors:
|
|
498
|
+
v3 |= 1 << (n + 15)
|
|
499
|
+
|
|
500
|
+
if not a.hybridization:
|
|
501
|
+
v2 |= 0xf
|
|
502
|
+
else:
|
|
503
|
+
for n in a.hybridization:
|
|
504
|
+
v2 |= 1 << (n - 1)
|
|
505
|
+
|
|
506
|
+
if b is not None:
|
|
507
|
+
for o in b.order:
|
|
508
|
+
if o == 1:
|
|
509
|
+
v1 |= 0x0800000000000000
|
|
510
|
+
elif o == 4:
|
|
511
|
+
v1 |= 0x4000000000000000
|
|
512
|
+
elif o == 2:
|
|
513
|
+
v1 |= 0x1000000000000000
|
|
514
|
+
elif o == 3:
|
|
515
|
+
v1 |= 0x2000000000000000
|
|
516
|
+
else:
|
|
517
|
+
v1 |= 0x8000000000000000
|
|
518
|
+
if b.in_ring is None:
|
|
519
|
+
v1 |= 0x0600000000000000
|
|
520
|
+
elif b.in_ring:
|
|
521
|
+
v1 |= 0x0400000000000000
|
|
522
|
+
else:
|
|
523
|
+
v1 |= 0x0200000000000000
|
|
524
|
+
|
|
525
|
+
masks1.append(v1)
|
|
526
|
+
masks2.append(v2)
|
|
527
|
+
masks3.append(v3)
|
|
528
|
+
masks4.append(v4)
|
|
529
|
+
|
|
530
|
+
closures = [0] * len(c) # closures amount
|
|
531
|
+
q_from = [0] * len(c)
|
|
532
|
+
q_to = [0] * len(c)
|
|
533
|
+
indices = [0] * sum(len(ms) for n, ms in _closures.items() if n in mapping)
|
|
534
|
+
bonds = indices.copy()
|
|
535
|
+
|
|
536
|
+
start = 0
|
|
537
|
+
for n, ms in _closures.items():
|
|
538
|
+
if (i := mapping.get(n)) is not None:
|
|
539
|
+
closures[i] = len(ms)
|
|
540
|
+
q_from[i] = start
|
|
541
|
+
for j, (m, b) in enumerate(ms, start):
|
|
542
|
+
v = 0x01ffffffffffffff # atom doesn't matter.
|
|
543
|
+
for o in b.order:
|
|
544
|
+
if o == 1:
|
|
545
|
+
v |= 0x0800000000000000
|
|
546
|
+
elif o == 4:
|
|
547
|
+
v |= 0x4000000000000000
|
|
548
|
+
elif o == 2:
|
|
549
|
+
v |= 0x1000000000000000
|
|
550
|
+
elif o == 3:
|
|
551
|
+
v |= 0x2000000000000000
|
|
552
|
+
else:
|
|
553
|
+
v |= 0x8000000000000000
|
|
554
|
+
if b.in_ring is None:
|
|
555
|
+
v |= 0x0600000000000000
|
|
556
|
+
elif b.in_ring:
|
|
557
|
+
v |= 0x0400000000000000
|
|
558
|
+
else:
|
|
559
|
+
v |= 0x0200000000000000
|
|
560
|
+
bonds[j] = v
|
|
561
|
+
indices[j] = mapping[m]
|
|
562
|
+
start += len(ms)
|
|
563
|
+
q_to[i] = start
|
|
564
|
+
|
|
565
|
+
back = [0] + [mapping[x] for _, x, *_ in c[1:]]
|
|
566
|
+
numbers = [n for n, *_ in c]
|
|
567
|
+
buffer = BytesIO()
|
|
568
|
+
buffer.write(header_struct.pack(len(numbers)))
|
|
569
|
+
for x in zip(masks1, masks2, masks3, masks4, back, closures, q_from, q_to, numbers):
|
|
570
|
+
buffer.write(q_atom_struct.pack(*x))
|
|
571
|
+
for x in zip(bonds, indices):
|
|
572
|
+
buffer.write(bond_struct.pack(*x))
|
|
573
|
+
components.append(buffer.getvalue())
|
|
574
|
+
return components
|
|
575
|
+
|
|
576
|
+
|
|
577
|
+
def _get_automorphism_mapping(atoms: Dict[int, int], bonds: Dict[int, Dict[int, Any]]) -> Iterator[Dict[int, int]]:
|
|
578
|
+
if len(atoms) == len(set(atoms.values())):
|
|
579
|
+
return # all atoms unique
|
|
580
|
+
|
|
581
|
+
components, closures = _compile_query(atoms, bonds)
|
|
582
|
+
mappers = [_get_mapping(order, closures, atoms, bonds, {x for x, *_ in order}) for order in components]
|
|
583
|
+
if len(mappers) == 1:
|
|
584
|
+
for mapping in mappers[0]:
|
|
585
|
+
if any(k != v for k, v in mapping.items()):
|
|
586
|
+
yield mapping
|
|
587
|
+
for match in lazy_product(*mappers):
|
|
588
|
+
mapping = match[0].copy()
|
|
589
|
+
for m in match[1:]:
|
|
590
|
+
mapping.update(m)
|
|
591
|
+
if any(k != v for k, v in mapping.items()):
|
|
592
|
+
yield mapping
|
|
593
|
+
|
|
594
|
+
|
|
595
|
+
def _get_mapping(linear_query, query_closures, o_atoms, o_bonds, scope):
|
|
596
|
+
size = len(linear_query) - 1
|
|
597
|
+
order_depth = {v[0]: k for k, v in enumerate(linear_query)}
|
|
598
|
+
|
|
599
|
+
stack = deque()
|
|
600
|
+
path = []
|
|
601
|
+
mapping = {}
|
|
602
|
+
reversed_mapping = {}
|
|
603
|
+
|
|
604
|
+
s_n, _, s_atom, _ = linear_query[0]
|
|
605
|
+
for n, o_atom in o_atoms.items():
|
|
606
|
+
if n in scope and s_atom == o_atom:
|
|
607
|
+
stack.append((n, 0))
|
|
608
|
+
|
|
609
|
+
while stack:
|
|
610
|
+
n, depth = stack.pop()
|
|
611
|
+
current = linear_query[depth][0]
|
|
612
|
+
if depth == size:
|
|
613
|
+
yield {**mapping, current: n}
|
|
614
|
+
else:
|
|
615
|
+
if len(path) != depth:
|
|
616
|
+
for x in path[depth:]:
|
|
617
|
+
del mapping[reversed_mapping.pop(x)]
|
|
618
|
+
path = path[:depth]
|
|
619
|
+
|
|
620
|
+
path.append(n)
|
|
621
|
+
mapping[current] = n
|
|
622
|
+
reversed_mapping[n] = current
|
|
623
|
+
|
|
624
|
+
depth += 1
|
|
625
|
+
s_n, back, s_atom, s_bond = linear_query[depth]
|
|
626
|
+
if back != current:
|
|
627
|
+
n = path[order_depth[back]]
|
|
628
|
+
|
|
629
|
+
for o_n, o_bond in o_bonds[n].items():
|
|
630
|
+
if o_n in scope and o_n not in reversed_mapping and s_bond == o_bond:
|
|
631
|
+
if s_atom == o_atoms[o_n]:
|
|
632
|
+
# check closures equality
|
|
633
|
+
o_closures = o_bonds[o_n].keys() & reversed_mapping.keys()
|
|
634
|
+
o_closures.discard(n)
|
|
635
|
+
if o_closures == {mapping[m] for m, _ in query_closures[s_n]}:
|
|
636
|
+
obon = o_bonds[o_n]
|
|
637
|
+
if all(bond == obon[mapping[m]] for m, bond in query_closures[s_n]):
|
|
638
|
+
stack.append((o_n, depth))
|
|
639
|
+
|
|
640
|
+
|
|
641
|
+
def _compile_query(atoms, bonds):
|
|
642
|
+
closures = defaultdict(list)
|
|
643
|
+
components = []
|
|
644
|
+
seen = set()
|
|
645
|
+
iter_atoms = iter(atoms)
|
|
646
|
+
while len(seen) < len(atoms):
|
|
647
|
+
start = next(x for x in iter_atoms if x not in seen)
|
|
648
|
+
seen.add(start)
|
|
649
|
+
stack = [(n, start, atoms[n], bond) for n, bond in reversed(bonds[start].items())]
|
|
650
|
+
order = [(start, None, atoms[start], None)]
|
|
651
|
+
components.append(order)
|
|
652
|
+
|
|
653
|
+
while stack:
|
|
654
|
+
front, back, *_ = atom = stack.pop()
|
|
655
|
+
if front not in seen:
|
|
656
|
+
order.append(atom)
|
|
657
|
+
for n, bond in reversed(bonds[front].items()):
|
|
658
|
+
if n != back:
|
|
659
|
+
if n not in seen:
|
|
660
|
+
stack.append((n, front, atoms[n], bond))
|
|
661
|
+
else:
|
|
662
|
+
closures[front].append((n, bond))
|
|
663
|
+
seen.add(front)
|
|
664
|
+
return components, closures
|
|
665
|
+
|
|
666
|
+
|
|
667
|
+
__all__ = ['MoleculeIsomorphism', 'QueryIsomorphism']
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
#
|
|
3
|
+
# Copyright 2022 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 .attention import Attention
|
|
20
|
+
from .fixmapper import FixMapper
|
|
21
|
+
from .groups import GroupsFix
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class Mapping(GroupsFix, Attention, FixMapper):
|
|
25
|
+
__slots__ = ()
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
__all__ = ['Mapping']
|