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,208 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
#
|
|
3
|
+
# Copyright 2021-2023 Ramil Nugmanov <nougmanoff@protonmail.com>
|
|
4
|
+
# Copyright 2021 Aleksandr Sizov <murkyrussian@gmail.com>
|
|
5
|
+
# Copyright 2023 Timur Gimadiev <timur.gimadiev@gmail.com>
|
|
6
|
+
# This file is part of chython.
|
|
7
|
+
#
|
|
8
|
+
# chython is free software; you can redistribute it and/or modify
|
|
9
|
+
# it under the terms of the GNU Lesser General Public License as published by
|
|
10
|
+
# the Free Software Foundation; either version 3 of the License, or
|
|
11
|
+
# (at your option) any later version.
|
|
12
|
+
#
|
|
13
|
+
# This program is distributed in the hope that it will be useful,
|
|
14
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
15
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
16
|
+
# GNU Lesser General Public License for more details.
|
|
17
|
+
#
|
|
18
|
+
# You should have received a copy of the GNU Lesser General Public License
|
|
19
|
+
# along with this program; if not, see <https://www.gnu.org/licenses/>.
|
|
20
|
+
#
|
|
21
|
+
from collections import defaultdict, deque
|
|
22
|
+
from math import log2
|
|
23
|
+
from numpy import uint8, zeros
|
|
24
|
+
from typing import Deque, Dict, List, Set, Tuple, TYPE_CHECKING
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
if TYPE_CHECKING:
|
|
28
|
+
from chython import MoleculeContainer
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class LinearFingerprint:
|
|
32
|
+
__slots__ = ()
|
|
33
|
+
"""
|
|
34
|
+
Linear fragments fingerprints.
|
|
35
|
+
Transform structures into fingerprints based on linear fragments descriptors.
|
|
36
|
+
Also count of fragments takes into account by activating multiple bits,
|
|
37
|
+
but less or equal to `number_bit_pairs`.To take into account
|
|
38
|
+
all repeating fragments put 0 as a value of `number_bit_pairs` parameter.
|
|
39
|
+
|
|
40
|
+
For example `CC` fragment found 4 times and `number_bit_pairs` is set to 3.
|
|
41
|
+
In this case will be activated 3 bits: for count 1, for count 2 and for count 3.
|
|
42
|
+
This gives intersection in bits with another structure with only 2 `CC` fragments.
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
def linear_fingerprint(self, min_radius: int = 1, max_radius: int = 4,
|
|
46
|
+
length: int = 1024, number_active_bits: int = 2,
|
|
47
|
+
number_bit_pairs: int = 4):
|
|
48
|
+
"""
|
|
49
|
+
Transform structures into array of binary features.
|
|
50
|
+
|
|
51
|
+
:param min_radius: minimal length of fragments
|
|
52
|
+
:param max_radius: maximum length of fragments
|
|
53
|
+
:param length: bit string's length. Should be power of 2
|
|
54
|
+
:param number_active_bits: number of active bits for each hashed tuple
|
|
55
|
+
:param number_bit_pairs: describe how much repeating fragments we can count in hashable fingerprint (if
|
|
56
|
+
number of fragment in molecule greater or equal this number, we will activate only this number of
|
|
57
|
+
fragments). To take into account all repeating fragments put 0 as a value.
|
|
58
|
+
|
|
59
|
+
:return: array(n_features)
|
|
60
|
+
"""
|
|
61
|
+
bits = self.linear_bit_set(min_radius, max_radius, length, number_active_bits,
|
|
62
|
+
number_bit_pairs)
|
|
63
|
+
fingerprints = zeros(length, dtype=uint8)
|
|
64
|
+
fingerprints[list(bits)] = 1
|
|
65
|
+
return fingerprints
|
|
66
|
+
|
|
67
|
+
def linear_bit_set(self, min_radius: int = 1, max_radius: int = 4, length: int = 1024, number_active_bits: int = 2,
|
|
68
|
+
number_bit_pairs: int = 4) -> Set[int]:
|
|
69
|
+
"""
|
|
70
|
+
Transform structure into set of indexes of True-valued features.
|
|
71
|
+
|
|
72
|
+
:param min_radius: minimal length of fragments
|
|
73
|
+
:param max_radius: maximum length of fragments
|
|
74
|
+
:param length: bit string's length. Should be power of 2
|
|
75
|
+
:param number_active_bits: number of active bits for each hashed tuple
|
|
76
|
+
:param number_bit_pairs: describe how much repeating fragments we can count in hashable fingerprint (if
|
|
77
|
+
number of fragment in molecule greater or equal this number, we will activate only this number of
|
|
78
|
+
fragments). To take into account all repeating fragments put 0 as a value.
|
|
79
|
+
"""
|
|
80
|
+
mask = length - 1
|
|
81
|
+
log = int(log2(length))
|
|
82
|
+
|
|
83
|
+
hashes = self.linear_hash_set(min_radius, max_radius, number_bit_pairs)
|
|
84
|
+
active_bits = set()
|
|
85
|
+
for tpl in hashes:
|
|
86
|
+
active_bits.add(tpl & mask)
|
|
87
|
+
if number_active_bits == 2:
|
|
88
|
+
active_bits.add((tpl >> log) & mask)
|
|
89
|
+
elif number_active_bits > 2:
|
|
90
|
+
for _ in range(1, number_active_bits):
|
|
91
|
+
tpl >>= log # shift
|
|
92
|
+
active_bits.add(tpl & mask)
|
|
93
|
+
return active_bits
|
|
94
|
+
|
|
95
|
+
def linear_hash_set(self, min_radius: int = 1, max_radius: int = 4, number_bit_pairs: int = 4) -> Set[int]:
|
|
96
|
+
"""
|
|
97
|
+
Transform structure into set of integer hashes of fragments with count information.
|
|
98
|
+
|
|
99
|
+
:param min_radius: minimal length of fragments
|
|
100
|
+
:param max_radius: maximum length of fragments
|
|
101
|
+
:param number_bit_pairs: describe how much repeating fragments we can count in hashable fingerprint (if
|
|
102
|
+
number of fragment in molecule greater or equal this number, we will activate only this number of
|
|
103
|
+
fragments). To take into account all repeating fragments put 0 as a value.
|
|
104
|
+
"""
|
|
105
|
+
if not number_bit_pairs:
|
|
106
|
+
number_bit_pairs = 999_999_999 # unreachable count
|
|
107
|
+
|
|
108
|
+
return {hash((*tpl, cnt)) for tpl, count in
|
|
109
|
+
self._fragments(min_radius, max_radius).items()
|
|
110
|
+
for cnt in range(min(len(count), number_bit_pairs))}
|
|
111
|
+
|
|
112
|
+
def linear_hash_smiles(self: 'MoleculeContainer', min_radius: int = 1, max_radius: int = 4,
|
|
113
|
+
number_bit_pairs: int = 4) -> Dict[int, List[str]]:
|
|
114
|
+
"""
|
|
115
|
+
Transform structure into dict of integer hashes of fragments with count information and
|
|
116
|
+
corresponding fragment SMILES.
|
|
117
|
+
|
|
118
|
+
:param min_radius: minimal length of fragments
|
|
119
|
+
:param max_radius: maximum length of fragments
|
|
120
|
+
:param number_bit_pairs: describe how much repeating fragments we can count in hashable fingerprint (if
|
|
121
|
+
number of fragment in molecule greater or equal this number, we will activate only this number of
|
|
122
|
+
fragments). To take into account all repeating fragments put 0 as a value.
|
|
123
|
+
"""
|
|
124
|
+
if not number_bit_pairs:
|
|
125
|
+
number_bit_pairs = 999_999_999 # unreachable count
|
|
126
|
+
|
|
127
|
+
out = defaultdict(set)
|
|
128
|
+
for frg, chains in self._fragments(min_radius, max_radius).items():
|
|
129
|
+
chain = chains[0]
|
|
130
|
+
smiles = [self._format_atom(chain[0], None, stereo=False)]
|
|
131
|
+
for x, y in zip(chain, chain[1:]):
|
|
132
|
+
smiles.append(self._format_bond(x, y, None, stereo=False, aromatic=False))
|
|
133
|
+
smiles.append(self._format_atom(y, None, stereo=False))
|
|
134
|
+
smiles = ''.join(smiles)
|
|
135
|
+
|
|
136
|
+
for cnt in range(min(len(chains), number_bit_pairs)):
|
|
137
|
+
out[hash((*frg, cnt))].add(smiles) # collisions possible
|
|
138
|
+
return {k: list(v) for k, v in out.items()}
|
|
139
|
+
|
|
140
|
+
def linear_smiles_hash(self, min_radius: int = 1, max_radius: int = 4,
|
|
141
|
+
number_bit_pairs: int = 4) -> Dict[str, List[int]]:
|
|
142
|
+
"""
|
|
143
|
+
Transform structure into dict of fragment SMILES and list of corresponding integer hashes of fragments.
|
|
144
|
+
|
|
145
|
+
:param min_radius: minimal length of fragments
|
|
146
|
+
:param max_radius: maximum length of fragments
|
|
147
|
+
:param number_bit_pairs: describe how much repeating fragments we can count in hashable fingerprint (if
|
|
148
|
+
number of fragment in molecule greater or equal this number, we will activate only this number of
|
|
149
|
+
fragments). To take into account all repeating fragments put 0 as a value.
|
|
150
|
+
"""
|
|
151
|
+
out = defaultdict(list)
|
|
152
|
+
for k, sl in self.linear_hash_smiles(min_radius, max_radius, number_bit_pairs).items():
|
|
153
|
+
for s in sl:
|
|
154
|
+
out[s].append(k)
|
|
155
|
+
return dict(out)
|
|
156
|
+
|
|
157
|
+
def _chains(self: 'MoleculeContainer', min_radius: int = 1, max_radius: int = 4) -> Set[Tuple[int, ...]]:
|
|
158
|
+
queue: Deque[Tuple[int, ...]] # typing
|
|
159
|
+
atoms = self._atoms
|
|
160
|
+
bonds = self._bonds
|
|
161
|
+
|
|
162
|
+
if min_radius == 1:
|
|
163
|
+
arr = {(x,) for x in atoms}
|
|
164
|
+
if max_radius == 1: # special case
|
|
165
|
+
return arr
|
|
166
|
+
else:
|
|
167
|
+
queue = deque(arr)
|
|
168
|
+
else:
|
|
169
|
+
arr = set()
|
|
170
|
+
queue = deque((x,) for x in atoms)
|
|
171
|
+
|
|
172
|
+
while queue:
|
|
173
|
+
now = queue.popleft()
|
|
174
|
+
var = [now + (x,) for x in bonds[now[-1]] if x not in now]
|
|
175
|
+
if var:
|
|
176
|
+
if len(var[0]) < max_radius:
|
|
177
|
+
queue.extend(var)
|
|
178
|
+
if len(var[0]) >= min_radius:
|
|
179
|
+
for frag in var:
|
|
180
|
+
rev = frag[::-1]
|
|
181
|
+
arr.add(frag if frag > rev else rev)
|
|
182
|
+
return arr
|
|
183
|
+
|
|
184
|
+
def _fragments(self: 'MoleculeContainer', min_radius: int = 1,
|
|
185
|
+
max_radius: int = 4) -> Dict[Tuple[int, ...], List[Tuple[int, ...]]]:
|
|
186
|
+
atoms = self._atom_identifiers
|
|
187
|
+
bonds = self._bonds
|
|
188
|
+
out = defaultdict(list)
|
|
189
|
+
|
|
190
|
+
for frag in self._chains(min_radius, max_radius):
|
|
191
|
+
var = [atoms[frag[0]]]
|
|
192
|
+
for x, y in zip(frag, frag[1:]):
|
|
193
|
+
var.append(int(bonds[x][y]))
|
|
194
|
+
var.append(atoms[y])
|
|
195
|
+
var = tuple(var)
|
|
196
|
+
rev_var = var[::-1]
|
|
197
|
+
if var > rev_var:
|
|
198
|
+
out[var].append(frag)
|
|
199
|
+
else:
|
|
200
|
+
out[rev_var].append(frag[::-1])
|
|
201
|
+
return dict(out)
|
|
202
|
+
|
|
203
|
+
@property
|
|
204
|
+
def _atom_identifiers(self) -> Dict[int, int]:
|
|
205
|
+
raise NotImplementedError
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
__all__ = ['LinearFingerprint']
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
#
|
|
3
|
+
# Copyright 2021-2023 Ramil Nugmanov <nougmanoff@protonmail.com>
|
|
4
|
+
# Copyright 2023 Timur Gimadiev <timur.gimadiev@gmail.com>
|
|
5
|
+
# Copyright 2021 Aleksandr Sizov <murkyrussian@gmail.com>
|
|
6
|
+
# This file is part of chython.
|
|
7
|
+
#
|
|
8
|
+
# chython is free software; you can redistribute it and/or modify
|
|
9
|
+
# it under the terms of the GNU Lesser General Public License as published by
|
|
10
|
+
# the Free Software Foundation; either version 3 of the License, or
|
|
11
|
+
# (at your option) any later version.
|
|
12
|
+
#
|
|
13
|
+
# This program is distributed in the hope that it will be useful,
|
|
14
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
15
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
16
|
+
# GNU Lesser General Public License for more details.
|
|
17
|
+
#
|
|
18
|
+
# You should have received a copy of the GNU Lesser General Public License
|
|
19
|
+
# along with this program; if not, see <https://www.gnu.org/licenses/>.
|
|
20
|
+
#
|
|
21
|
+
from collections import defaultdict
|
|
22
|
+
from math import log2
|
|
23
|
+
from numpy import uint8, zeros
|
|
24
|
+
from typing import Dict, List, Set, TYPE_CHECKING
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
if TYPE_CHECKING:
|
|
28
|
+
from chython import MoleculeContainer
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class MorganFingerprint:
|
|
32
|
+
__slots__ = ()
|
|
33
|
+
|
|
34
|
+
def morgan_fingerprint(self, min_radius: int = 1, max_radius: int = 4,
|
|
35
|
+
length: int = 1024, number_active_bits: int = 2):
|
|
36
|
+
"""
|
|
37
|
+
Transform structures into array of binary features.
|
|
38
|
+
Morgan fingerprints. Similar to RDkit implementation.
|
|
39
|
+
|
|
40
|
+
:param min_radius: minimal radius of EC
|
|
41
|
+
:param max_radius: maximum radius of EC
|
|
42
|
+
:param length: bit string's length. Should be power of 2
|
|
43
|
+
:param number_active_bits: number of active bits for each hashed tuple
|
|
44
|
+
|
|
45
|
+
:return: array(n_features)
|
|
46
|
+
"""
|
|
47
|
+
bits = self.morgan_bit_set(min_radius, max_radius, length, number_active_bits)
|
|
48
|
+
fingerprints = zeros(length, dtype=uint8)
|
|
49
|
+
fingerprints[list(bits)] = 1
|
|
50
|
+
return fingerprints
|
|
51
|
+
|
|
52
|
+
def morgan_bit_set(self, min_radius: int = 1, max_radius: int = 4,
|
|
53
|
+
length: int = 1024, number_active_bits: int = 2) -> Set[int]:
|
|
54
|
+
"""
|
|
55
|
+
Transform structures into set of indexes of True-valued features.
|
|
56
|
+
|
|
57
|
+
:param min_radius: minimal radius of EC
|
|
58
|
+
:param max_radius: maximum radius of EC
|
|
59
|
+
:param length: bit string's length. Should be power of 2
|
|
60
|
+
:param number_active_bits: number of active bits for each hashed tuple
|
|
61
|
+
"""
|
|
62
|
+
mask = length - 1
|
|
63
|
+
log = int(log2(length))
|
|
64
|
+
|
|
65
|
+
active_bits = set()
|
|
66
|
+
for tpl in self.morgan_hash_set(min_radius, max_radius):
|
|
67
|
+
active_bits.add(tpl & mask)
|
|
68
|
+
if number_active_bits == 2:
|
|
69
|
+
active_bits.add(tpl >> log & mask)
|
|
70
|
+
elif number_active_bits > 2:
|
|
71
|
+
for _ in range(1, number_active_bits):
|
|
72
|
+
tpl >>= log
|
|
73
|
+
active_bits.add(tpl & mask)
|
|
74
|
+
return active_bits
|
|
75
|
+
|
|
76
|
+
def morgan_hash_set(self: 'MoleculeContainer', min_radius: int = 1, max_radius: int = 4) -> Set[int]:
|
|
77
|
+
"""
|
|
78
|
+
Transform structures into integer hashes of atoms with EC.
|
|
79
|
+
|
|
80
|
+
:param min_radius: minimal radius of EC
|
|
81
|
+
:param max_radius: maximum radius of EC
|
|
82
|
+
"""
|
|
83
|
+
return {x for x in self._morgan_hash_dict(min_radius, max_radius) for x in x.values()}
|
|
84
|
+
|
|
85
|
+
def morgan_hash_smiles(self: 'MoleculeContainer', min_radius: int = 1, max_radius: int = 4) -> Dict[int, List[str]]:
|
|
86
|
+
"""
|
|
87
|
+
Transform structures into dictionary of hashes of atoms with EC and corresponding SMILES.
|
|
88
|
+
|
|
89
|
+
:param min_radius: minimal radius of EC
|
|
90
|
+
:param max_radius: maximum radius of EC
|
|
91
|
+
"""
|
|
92
|
+
smiles_dict = defaultdict(set)
|
|
93
|
+
for radius, hash_dict in enumerate(self._morgan_hash_dict(min_radius, max_radius), min_radius - 1):
|
|
94
|
+
for atom, morgan_hash in hash_dict.items():
|
|
95
|
+
smiles_dict[morgan_hash].add(format(self.augmented_substructure((atom,), deep=radius), 'A'))
|
|
96
|
+
return {k: list(v) for k, v in smiles_dict.items()}
|
|
97
|
+
|
|
98
|
+
def morgan_smiles_hash(self: 'MoleculeContainer', min_radius: int = 1, max_radius: int = 4) -> Dict[str, List[int]]:
|
|
99
|
+
"""
|
|
100
|
+
Transform structures into dictionary of smiles and corresponding hashes of atoms with EC.
|
|
101
|
+
|
|
102
|
+
:param min_radius: minimal radius of EC
|
|
103
|
+
:param max_radius: maximum radius of EC
|
|
104
|
+
"""
|
|
105
|
+
out = defaultdict(list)
|
|
106
|
+
for k, sl in self.morgan_hash_smiles(min_radius, max_radius).items():
|
|
107
|
+
for s in sl:
|
|
108
|
+
out[s].append(k)
|
|
109
|
+
return dict(out)
|
|
110
|
+
|
|
111
|
+
def _morgan_hash_dict(self: 'MoleculeContainer', min_radius: int = 1, max_radius: int = 4) -> List[Dict[int, int]]:
|
|
112
|
+
"""
|
|
113
|
+
Transform structures into integer hashes of atoms with EC.
|
|
114
|
+
Returns list of atom-hash pairs for different radii.
|
|
115
|
+
|
|
116
|
+
:param min_radius: minimal radius of EC
|
|
117
|
+
:param max_radius: maximum radius of EC
|
|
118
|
+
"""
|
|
119
|
+
assert min_radius >= 1, 'min_radius should be positive'
|
|
120
|
+
assert max_radius >= min_radius, 'max_radius should be greater or equal to min_radius'
|
|
121
|
+
identifiers = self._atom_identifiers
|
|
122
|
+
|
|
123
|
+
bonds = self._bonds
|
|
124
|
+
out = [identifiers]
|
|
125
|
+
for _ in range(1, max_radius):
|
|
126
|
+
identifiers = {idx: hash((tpl, *(x for x in sorted((int(b), identifiers[ngb])
|
|
127
|
+
for ngb, b in bonds[idx].items()) for x in x)))
|
|
128
|
+
for idx, tpl in identifiers.items()}
|
|
129
|
+
out.append(identifiers)
|
|
130
|
+
return out[-(max_radius - min_radius + 1):] # slice [min, max] radii range
|
|
131
|
+
|
|
132
|
+
@property
|
|
133
|
+
def _atom_identifiers(self) -> Dict[int, int]:
|
|
134
|
+
raise NotImplementedError
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
__all__ = ['MorganFingerprint']
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
#
|
|
3
|
+
# Copyright 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
|
+
#
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
#
|
|
3
|
+
# Copyright 2025 Ramil Nugmanov <nougmanoff@protonmail.com>
|
|
4
|
+
# Copyright 2025 Tagir Akhmetshin <tagirshin@gmail.com>
|
|
5
|
+
# This file is part of chython.
|
|
6
|
+
#
|
|
7
|
+
# chython is free software; you can redistribute it and/or modify
|
|
8
|
+
# it under the terms of the GNU Lesser General Public License as published by
|
|
9
|
+
# the Free Software Foundation; either version 3 of the License, or
|
|
10
|
+
# (at your option) any later version.
|
|
11
|
+
#
|
|
12
|
+
# This program is distributed in the hope that it will be useful,
|
|
13
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
14
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
15
|
+
# GNU Lesser General Public License for more details.
|
|
16
|
+
#
|
|
17
|
+
# You should have received a copy of the GNU Lesser General Public License
|
|
18
|
+
# along with this program; if not, see <https://www.gnu.org/licenses/>.
|
|
19
|
+
#
|
|
20
|
+
import numpy as np
|
|
21
|
+
from chython import smiles
|
|
22
|
+
from pytest import mark, raises
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def test_basic():
|
|
26
|
+
# Test basic fingerprint generation
|
|
27
|
+
mol = smiles('CCO')
|
|
28
|
+
fp = mol.linear_fingerprint(min_radius=1, max_radius=2, length=1024)
|
|
29
|
+
|
|
30
|
+
# Test array properties
|
|
31
|
+
assert isinstance(fp, np.ndarray)
|
|
32
|
+
assert fp.dtype == np.uint8
|
|
33
|
+
assert fp.shape == (1024,)
|
|
34
|
+
|
|
35
|
+
# Test binary nature
|
|
36
|
+
assert set(np.unique(fp)).issubset({0, 1})
|
|
37
|
+
|
|
38
|
+
# Test different lengths
|
|
39
|
+
fp = mol.linear_fingerprint(length=2048)
|
|
40
|
+
assert isinstance(fp, np.ndarray)
|
|
41
|
+
assert fp.dtype == np.uint8
|
|
42
|
+
assert fp.shape == (2048,)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@mark.parametrize('radius', [(0, 1), (1, 0), (-1, 2), (2, 1)])
|
|
46
|
+
def test_invalid_radius(radius):
|
|
47
|
+
mol = smiles('CCO')
|
|
48
|
+
min_r, max_r = radius
|
|
49
|
+
with raises(AssertionError):
|
|
50
|
+
mol.morgan_fingerprint(min_radius=min_r, max_radius=max_r)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def test_consistency():
|
|
54
|
+
# Test that fingerprints are consistent for the same molecule
|
|
55
|
+
fp1 = smiles('CCO').linear_fingerprint()
|
|
56
|
+
fp2 = smiles('OCC').linear_fingerprint()
|
|
57
|
+
|
|
58
|
+
# Test exact equality of arrays
|
|
59
|
+
assert np.array_equal(fp1, fp2)
|
|
60
|
+
|
|
61
|
+
# Test different molecules give different fingerprints)
|
|
62
|
+
fp3 = smiles('CCC').linear_fingerprint()
|
|
63
|
+
assert not np.array_equal(fp1, fp3)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def test_parameters():
|
|
67
|
+
mol = smiles('CCO')
|
|
68
|
+
|
|
69
|
+
# Test different radius parameters
|
|
70
|
+
fp1 = mol.linear_fingerprint(min_radius=1, max_radius=2)
|
|
71
|
+
fp2 = mol.linear_fingerprint(min_radius=1, max_radius=3)
|
|
72
|
+
assert not np.array_equal(fp1, fp2)
|
|
73
|
+
assert np.array_equal(fp1 & fp2, fp1)
|
|
74
|
+
|
|
75
|
+
# Test number of active bits
|
|
76
|
+
fp3 = mol.linear_fingerprint(number_active_bits=2)
|
|
77
|
+
fp4 = mol.linear_fingerprint(number_active_bits=3)
|
|
78
|
+
assert not np.array_equal(fp3, fp4)
|
|
79
|
+
assert np.array_equal(fp3 & fp4, fp3)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def test_bit_pairs():
|
|
83
|
+
# Test the number_bit_pairs parameter
|
|
84
|
+
mol = smiles('CCCCCCCCCCCCCCCCCCCCCCCC') # molecule with multiple similar fragments
|
|
85
|
+
|
|
86
|
+
# Compare different number_bit_pairs settings
|
|
87
|
+
fp1 = mol.linear_fingerprint(number_bit_pairs=2)
|
|
88
|
+
fp2 = mol.linear_fingerprint(number_bit_pairs=3)
|
|
89
|
+
|
|
90
|
+
assert not np.array_equal(fp1, fp2)
|
|
91
|
+
assert np.array_equal(fp1 & fp2, fp1)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def test_edge_cases():
|
|
95
|
+
fp1 = smiles('C').linear_fingerprint()
|
|
96
|
+
assert fp1.sum() == 2
|
|
97
|
+
|
|
98
|
+
fp1 = smiles('CC').linear_fingerprint()
|
|
99
|
+
fp2 = smiles('CC.CC').linear_fingerprint()
|
|
100
|
+
assert not np.array_equal(fp1, fp2)
|
|
101
|
+
assert np.array_equal(fp1 & fp2, fp1)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def test_comparison():
|
|
105
|
+
# Test fingerprint comparison between similar molecules
|
|
106
|
+
mol1 = smiles('CCO')
|
|
107
|
+
mol2 = smiles('CCC')
|
|
108
|
+
mol3 = smiles('CCCO')
|
|
109
|
+
|
|
110
|
+
fp1 = mol1.linear_fingerprint()
|
|
111
|
+
fp2 = mol2.linear_fingerprint()
|
|
112
|
+
fp3 = mol3.linear_fingerprint()
|
|
113
|
+
|
|
114
|
+
# Calculate Tanimoto similarities
|
|
115
|
+
def tanimoto(a, b):
|
|
116
|
+
intersection = np.sum(np.logical_and(a, b))
|
|
117
|
+
union = np.sum(np.logical_or(a, b))
|
|
118
|
+
return intersection / union if union > 0 else 0.0
|
|
119
|
+
|
|
120
|
+
# Similar molecules should have higher similarity
|
|
121
|
+
sim12 = tanimoto(fp1, fp2)
|
|
122
|
+
sim13 = tanimoto(fp1, fp3)
|
|
123
|
+
sim23 = tanimoto(fp2, fp3)
|
|
124
|
+
|
|
125
|
+
assert 0 <= sim12 <= 1
|
|
126
|
+
assert 0 <= sim13 <= 1
|
|
127
|
+
assert 0 <= sim23 <= 1
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
#
|
|
3
|
+
# Copyright 2025 Ramil Nugmanov <nougmanoff@protonmail.com>
|
|
4
|
+
# Copyright 2025 Tagir Akhmetshin <tagirshin@gmail.com>
|
|
5
|
+
# This file is part of chython.
|
|
6
|
+
#
|
|
7
|
+
# chython is free software; you can redistribute it and/or modify
|
|
8
|
+
# it under the terms of the GNU Lesser General Public License as published by
|
|
9
|
+
# the Free Software Foundation; either version 3 of the License, or
|
|
10
|
+
# (at your option) any later version.
|
|
11
|
+
#
|
|
12
|
+
# This program is distributed in the hope that it will be useful,
|
|
13
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
14
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
15
|
+
# GNU Lesser General Public License for more details.
|
|
16
|
+
#
|
|
17
|
+
# You should have received a copy of the GNU Lesser General Public License
|
|
18
|
+
# along with this program; if not, see <https://www.gnu.org/licenses/>.
|
|
19
|
+
#
|
|
20
|
+
from chython import smiles
|
|
21
|
+
import numpy as np
|
|
22
|
+
from pytest import mark, raises
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def test_basic():
|
|
26
|
+
# Test basic fingerprint generation
|
|
27
|
+
mol = smiles('CCO')
|
|
28
|
+
fp = mol.morgan_fingerprint(min_radius=1, max_radius=2, length=1024)
|
|
29
|
+
|
|
30
|
+
# Test array properties
|
|
31
|
+
assert isinstance(fp, np.ndarray)
|
|
32
|
+
assert fp.dtype == np.uint8
|
|
33
|
+
assert fp.shape == (1024,)
|
|
34
|
+
|
|
35
|
+
# Test binary nature
|
|
36
|
+
assert set(np.unique(fp)).issubset({0, 1})
|
|
37
|
+
|
|
38
|
+
# Test different lengths
|
|
39
|
+
fp = mol.morgan_fingerprint(length=2048)
|
|
40
|
+
assert isinstance(fp, np.ndarray)
|
|
41
|
+
assert fp.dtype == np.uint8
|
|
42
|
+
assert fp.shape == (2048,)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@mark.parametrize('radius', [(0, 1), (1, 0), (-1, 2), (2, 1)])
|
|
46
|
+
def test_invalid_radius(radius):
|
|
47
|
+
mol = smiles('CCO')
|
|
48
|
+
min_r, max_r = radius
|
|
49
|
+
with raises(AssertionError):
|
|
50
|
+
mol.morgan_fingerprint(min_radius=min_r, max_radius=max_r)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def test_consistency():
|
|
54
|
+
# Test that fingerprints are consistent for the same molecule
|
|
55
|
+
fp1 = smiles('CCO').morgan_fingerprint()
|
|
56
|
+
fp2 = smiles('OCC').morgan_fingerprint()
|
|
57
|
+
|
|
58
|
+
# Test exact equality of arrays
|
|
59
|
+
assert np.array_equal(fp1, fp2)
|
|
60
|
+
|
|
61
|
+
# Test different molecules give different fingerprints)
|
|
62
|
+
fp3 = smiles('CCC').morgan_fingerprint()
|
|
63
|
+
assert not np.array_equal(fp1, fp3)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def test_parameters():
|
|
67
|
+
mol = smiles('CCO')
|
|
68
|
+
|
|
69
|
+
# Test different radius parameters
|
|
70
|
+
fp1 = mol.morgan_fingerprint(min_radius=1, max_radius=2)
|
|
71
|
+
fp2 = mol.morgan_fingerprint(min_radius=1, max_radius=3)
|
|
72
|
+
assert not np.array_equal(fp1, fp2)
|
|
73
|
+
assert np.array_equal(fp1 & fp2, fp1)
|
|
74
|
+
|
|
75
|
+
# Test number of active bits
|
|
76
|
+
fp3 = mol.morgan_fingerprint(number_active_bits=2)
|
|
77
|
+
fp4 = mol.morgan_fingerprint(number_active_bits=3)
|
|
78
|
+
assert not np.array_equal(fp3, fp4)
|
|
79
|
+
assert np.array_equal(fp3 & fp4, fp3)
|