chython 2.4__cp312-cp312-win_amd64.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (171) hide show
  1. chython/__init__.py +31 -0
  2. chython/_functions.py +69 -0
  3. chython/algorithms/__init__.py +18 -0
  4. chython/algorithms/_isomorphism.cp312-win_amd64.pyd +0 -0
  5. chython/algorithms/_isomorphism.pyx +189 -0
  6. chython/algorithms/aromatics/__init__.py +27 -0
  7. chython/algorithms/aromatics/_rules.py +124 -0
  8. chython/algorithms/aromatics/kekule.py +530 -0
  9. chython/algorithms/aromatics/test/__init__.py +18 -0
  10. chython/algorithms/aromatics/test/test_kekule.py +160 -0
  11. chython/algorithms/aromatics/test/test_thiele.py +263 -0
  12. chython/algorithms/aromatics/thiele.py +226 -0
  13. chython/algorithms/calculate2d/__init__.py +23 -0
  14. chython/algorithms/calculate2d/clean2d.js +1 -0
  15. chython/algorithms/calculate2d/molecule.py +149 -0
  16. chython/algorithms/calculate2d/reaction.py +80 -0
  17. chython/algorithms/depict.py +523 -0
  18. chython/algorithms/fingerprints/__init__.py +48 -0
  19. chython/algorithms/fingerprints/linear.py +208 -0
  20. chython/algorithms/fingerprints/morgan.py +137 -0
  21. chython/algorithms/fingerprints/test/__init__.py +18 -0
  22. chython/algorithms/fingerprints/test/test_linear.py +127 -0
  23. chython/algorithms/fingerprints/test/test_morgan.py +79 -0
  24. chython/algorithms/isomorphism.py +667 -0
  25. chython/algorithms/mapping/__init__.py +28 -0
  26. chython/algorithms/mapping/_groups.py +94 -0
  27. chython/algorithms/mapping/_reactions.py +72 -0
  28. chython/algorithms/mapping/attention.py +216 -0
  29. chython/algorithms/mapping/fixmapper.py +84 -0
  30. chython/algorithms/mapping/groups.py +137 -0
  31. chython/algorithms/mcs.py +202 -0
  32. chython/algorithms/morgan.py +82 -0
  33. chython/algorithms/rings.py +551 -0
  34. chython/algorithms/smiles.py +556 -0
  35. chython/algorithms/standardize/__init__.py +30 -0
  36. chython/algorithms/standardize/_charged.py +115 -0
  37. chython/algorithms/standardize/_groups.py +926 -0
  38. chython/algorithms/standardize/_metal_organics.py +189 -0
  39. chython/algorithms/standardize/_reagents.py +44 -0
  40. chython/algorithms/standardize/_salts.py +68 -0
  41. chython/algorithms/standardize/molecule.py +479 -0
  42. chython/algorithms/standardize/reaction.py +429 -0
  43. chython/algorithms/standardize/resonance.py +196 -0
  44. chython/algorithms/standardize/salts.py +128 -0
  45. chython/algorithms/standardize/saturation.py +378 -0
  46. chython/algorithms/standardize/test/__init__.py +18 -0
  47. chython/algorithms/standardize/test/test_groups.py +118 -0
  48. chython/algorithms/stereo.py +1222 -0
  49. chython/algorithms/tautomers/__init__.py +171 -0
  50. chython/algorithms/tautomers/_acid.py +59 -0
  51. chython/algorithms/tautomers/_base.py +116 -0
  52. chython/algorithms/tautomers/_keto_enol.py +107 -0
  53. chython/algorithms/tautomers/acid_base.py +214 -0
  54. chython/algorithms/tautomers/heteroarenes.py +108 -0
  55. chython/algorithms/tautomers/keto_enol.py +148 -0
  56. chython/algorithms/tautomers/test/__init__.py +18 -0
  57. chython/algorithms/tautomers/test/test_tautomers.py +80 -0
  58. chython/algorithms/test/__init__.py +18 -0
  59. chython/algorithms/test/test_isomorphism.py +58 -0
  60. chython/algorithms/test/test_smiles.py +76 -0
  61. chython/algorithms/x3dom.py +355 -0
  62. chython/containers/__init__.py +43 -0
  63. chython/containers/_pack_v2.cp312-win_amd64.pyd +0 -0
  64. chython/containers/_pack_v2.pyx +271 -0
  65. chython/containers/_unpack_v0v2.cp312-win_amd64.pyd +0 -0
  66. chython/containers/_unpack_v0v2.pyx +300 -0
  67. chython/containers/bonds.py +255 -0
  68. chython/containers/cgr.py +115 -0
  69. chython/containers/graph.py +200 -0
  70. chython/containers/molecule.py +695 -0
  71. chython/containers/query.py +66 -0
  72. chython/containers/reaction.py +324 -0
  73. chython/exceptions.py +114 -0
  74. chython/files/MRVrw.py +523 -0
  75. chython/files/PDBrw.py +257 -0
  76. chython/files/RDFrw.py +298 -0
  77. chython/files/SDFrw.py +221 -0
  78. chython/files/__init__.py +29 -0
  79. chython/files/_convert.py +207 -0
  80. chython/files/_mapping.py +117 -0
  81. chython/files/_xyz.cp312-win_amd64.pyd +0 -0
  82. chython/files/_xyz.pyx +74 -0
  83. chython/files/daylight/__init__.py +24 -0
  84. chython/files/daylight/parser.py +157 -0
  85. chython/files/daylight/smarts.py +107 -0
  86. chython/files/daylight/smiles.py +245 -0
  87. chython/files/daylight/test/__init__.py +18 -0
  88. chython/files/daylight/test/test_daylight_smarts.py +289 -0
  89. chython/files/daylight/test/test_daylight_smiles.py +101 -0
  90. chython/files/daylight/test/test_parser.py +118 -0
  91. chython/files/daylight/test/test_tokenize.py +77 -0
  92. chython/files/daylight/tokenize.py +376 -0
  93. chython/files/libinchi/__init__.py +22 -0
  94. chython/files/libinchi/libinchi.dll +0 -0
  95. chython/files/libinchi/wrapper.py +555 -0
  96. chython/files/mdl/__init__.py +25 -0
  97. chython/files/mdl/emol.py +209 -0
  98. chython/files/mdl/erxn.py +67 -0
  99. chython/files/mdl/mol.py +144 -0
  100. chython/files/mdl/read.py +225 -0
  101. chython/files/mdl/rxn.py +67 -0
  102. chython/files/mdl/stereo.py +63 -0
  103. chython/files/mdl/write.py +165 -0
  104. chython/files/xyz.py +78 -0
  105. chython/periodictable/__init__.py +69 -0
  106. chython/periodictable/base/__init__.py +25 -0
  107. chython/periodictable/base/dynamic.py +149 -0
  108. chython/periodictable/base/element.py +452 -0
  109. chython/periodictable/base/groups.py +90 -0
  110. chython/periodictable/base/periods.py +46 -0
  111. chython/periodictable/base/query.py +473 -0
  112. chython/periodictable/base/vector.py +119 -0
  113. chython/periodictable/groupI.py +252 -0
  114. chython/periodictable/groupII.py +223 -0
  115. chython/periodictable/groupIII.py +1154 -0
  116. chython/periodictable/groupIV.py +210 -0
  117. chython/periodictable/groupIX.py +196 -0
  118. chython/periodictable/groupV.py +192 -0
  119. chython/periodictable/groupVI.py +183 -0
  120. chython/periodictable/groupVII.py +162 -0
  121. chython/periodictable/groupVIII.py +161 -0
  122. chython/periodictable/groupX.py +166 -0
  123. chython/periodictable/groupXI.py +164 -0
  124. chython/periodictable/groupXII.py +158 -0
  125. chython/periodictable/groupXIII.py +239 -0
  126. chython/periodictable/groupXIV.py +262 -0
  127. chython/periodictable/groupXV.py +276 -0
  128. chython/periodictable/groupXVI.py +457 -0
  129. chython/periodictable/groupXVII.py +324 -0
  130. chython/periodictable/groupXVIII.py +260 -0
  131. chython/reactor/__init__.py +24 -0
  132. chython/reactor/base.py +212 -0
  133. chython/reactor/deprotection.py +565 -0
  134. chython/reactor/groups.py +128 -0
  135. chython/reactor/reactions/__init__.py +157 -0
  136. chython/reactor/reactions/_amidation.py +58 -0
  137. chython/reactor/reactions/_amine_isocyanate.py +52 -0
  138. chython/reactor/reactions/_buchwald_hartwig.py +50 -0
  139. chython/reactor/reactions/_esterification.py +49 -0
  140. chython/reactor/reactions/_macmillan.py +43 -0
  141. chython/reactor/reactions/_reductive_amination.py +50 -0
  142. chython/reactor/reactions/_sonogashira.py +49 -0
  143. chython/reactor/reactions/_sulfonamidation.py +52 -0
  144. chython/reactor/reactions/_suzuki_miyaura.py +88 -0
  145. chython/reactor/reactions/ufe.py +99 -0
  146. chython/reactor/reactor.py +170 -0
  147. chython/reactor/retro/__init__.py +77 -0
  148. chython/reactor/retro/_amidation.py +39 -0
  149. chython/reactor/retro/_aryl_amination.py +32 -0
  150. chython/reactor/retro/_mitsunobu.py +49 -0
  151. chython/reactor/retro/_sonogashira.py +49 -0
  152. chython/reactor/retro/_suzuki_miyaura.py +54 -0
  153. chython/reactor/scaffold.py +122 -0
  154. chython/reactor/test/__init__.py +18 -0
  155. chython/reactor/test/test_deprotection.py +57 -0
  156. chython/reactor/test/test_reactor.py +40 -0
  157. chython/reactor/test/test_scaffold.py +57 -0
  158. chython/reactor/test/test_transformer.py +40 -0
  159. chython/reactor/transformer.py +62 -0
  160. chython/utils/__init__.py +57 -0
  161. chython/utils/free_wilson.py +114 -0
  162. chython/utils/grid.py +130 -0
  163. chython/utils/rdkit.py +165 -0
  164. chython/utils/retro.py +139 -0
  165. chython/utils/svg.py +57 -0
  166. chython/utils/test/__init__.py +18 -0
  167. chython/utils/test/test_rdkit.py +71 -0
  168. chython-2.4.dist-info/LICENSE +165 -0
  169. chython-2.4.dist-info/METADATA +101 -0
  170. chython-2.4.dist-info/RECORD +171 -0
  171. chython-2.4.dist-info/WHEEL +4 -0
@@ -0,0 +1,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)