chython 2.19__cp313-cp313-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 (175) hide show
  1. chython/__init__.py +35 -0
  2. chython/_functions.py +95 -0
  3. chython/_java.py +57 -0
  4. chython/algorithms/__init__.py +18 -0
  5. chython/algorithms/_isomorphism.cp313-win_amd64.pyd +0 -0
  6. chython/algorithms/_isomorphism.pyx +189 -0
  7. chython/algorithms/_rings.cp313-win_amd64.pyd +0 -0
  8. chython/algorithms/_rings.pyx +478 -0
  9. chython/algorithms/aromatics/__init__.py +27 -0
  10. chython/algorithms/aromatics/_rules.py +124 -0
  11. chython/algorithms/aromatics/kekule.py +532 -0
  12. chython/algorithms/aromatics/test/__init__.py +18 -0
  13. chython/algorithms/aromatics/test/test_kekule.py +160 -0
  14. chython/algorithms/aromatics/test/test_thiele.py +263 -0
  15. chython/algorithms/aromatics/thiele.py +222 -0
  16. chython/algorithms/calculate2d/__init__.py +23 -0
  17. chython/algorithms/calculate2d/clean2d.js +1 -0
  18. chython/algorithms/calculate2d/molecule.py +191 -0
  19. chython/algorithms/calculate2d/reaction.py +75 -0
  20. chython/algorithms/conformers.py +106 -0
  21. chython/algorithms/depict.py +596 -0
  22. chython/algorithms/fingerprints/__init__.py +43 -0
  23. chython/algorithms/fingerprints/linear.py +202 -0
  24. chython/algorithms/fingerprints/morgan.py +132 -0
  25. chython/algorithms/fingerprints/test/__init__.py +18 -0
  26. chython/algorithms/fingerprints/test/test_linear.py +127 -0
  27. chython/algorithms/fingerprints/test/test_morgan.py +79 -0
  28. chython/algorithms/groups/__init__.py +212 -0
  29. chython/algorithms/groups/_functional.py +290 -0
  30. chython/algorithms/groups/_oxidations.py +70 -0
  31. chython/algorithms/groups/_protective.py +964 -0
  32. chython/algorithms/groups/_reactions.py +809 -0
  33. chython/algorithms/groups/_reductions.py +88 -0
  34. chython/algorithms/groups/_transformations.py +278 -0
  35. chython/algorithms/groups/test/__init__.py +18 -0
  36. chython/algorithms/groups/test/test_deprotection.py +108 -0
  37. chython/algorithms/groups/test/test_reactions.py +377 -0
  38. chython/algorithms/isomorphism.py +664 -0
  39. chython/algorithms/mapping/__init__.py +188 -0
  40. chython/algorithms/mapping/_groups.py +94 -0
  41. chython/algorithms/mapping/_reactions.py +72 -0
  42. chython/algorithms/mapping/attention.py +266 -0
  43. chython/algorithms/mapping/reconstruct.py +346 -0
  44. chython/algorithms/mcs.py +201 -0
  45. chython/algorithms/morgan.py +77 -0
  46. chython/algorithms/rings.py +178 -0
  47. chython/algorithms/smiles.py +597 -0
  48. chython/algorithms/standardize/__init__.py +30 -0
  49. chython/algorithms/standardize/_groups.py +958 -0
  50. chython/algorithms/standardize/_isomers.py +330 -0
  51. chython/algorithms/standardize/_metal_organics.py +189 -0
  52. chython/algorithms/standardize/_reagents.py +44 -0
  53. chython/algorithms/standardize/_salts.py +68 -0
  54. chython/algorithms/standardize/molecule.py +475 -0
  55. chython/algorithms/standardize/reaction.py +384 -0
  56. chython/algorithms/standardize/resonance.py +191 -0
  57. chython/algorithms/standardize/salts.py +128 -0
  58. chython/algorithms/standardize/saturation.py +373 -0
  59. chython/algorithms/standardize/test/__init__.py +18 -0
  60. chython/algorithms/standardize/test/test_canonicalize.py +66 -0
  61. chython/algorithms/standardize/test/test_groups.py +146 -0
  62. chython/algorithms/standardize/test/test_isomers.py +198 -0
  63. chython/algorithms/stereo.py +1215 -0
  64. chython/algorithms/tautomers/__init__.py +167 -0
  65. chython/algorithms/tautomers/_acid.py +59 -0
  66. chython/algorithms/tautomers/_base.py +116 -0
  67. chython/algorithms/tautomers/_keto_enol.py +107 -0
  68. chython/algorithms/tautomers/acid_base.py +208 -0
  69. chython/algorithms/tautomers/heteroarenes.py +103 -0
  70. chython/algorithms/tautomers/keto_enol.py +143 -0
  71. chython/algorithms/tautomers/test/__init__.py +18 -0
  72. chython/algorithms/tautomers/test/test_tautomers.py +80 -0
  73. chython/algorithms/test/__init__.py +18 -0
  74. chython/algorithms/test/test_isomorphism.py +58 -0
  75. chython/algorithms/test/test_smiles.py +76 -0
  76. chython/algorithms/x3dom.py +350 -0
  77. chython/containers/__init__.py +48 -0
  78. chython/containers/_pack_v2.cp313-win_amd64.pyd +0 -0
  79. chython/containers/_pack_v2.pyx +271 -0
  80. chython/containers/_unpack_v0v2.cp313-win_amd64.pyd +0 -0
  81. chython/containers/_unpack_v0v2.pyx +302 -0
  82. chython/containers/bonds.py +255 -0
  83. chython/containers/cgr.py +115 -0
  84. chython/containers/chimera.py +63 -0
  85. chython/containers/graph.py +199 -0
  86. chython/containers/molecule.py +817 -0
  87. chython/containers/query.py +71 -0
  88. chython/containers/rdkit.py +183 -0
  89. chython/containers/reaction.py +324 -0
  90. chython/containers/test/__init__.py +18 -0
  91. chython/containers/test/test_molecule.py +117 -0
  92. chython/containers/test/test_pack.py +60 -0
  93. chython/containers/test/test_rdkit.py +70 -0
  94. chython/containers/test/test_reaction.py +40 -0
  95. chython/exceptions.py +114 -0
  96. chython/files/MRVrw.py +532 -0
  97. chython/files/PDBrw.py +255 -0
  98. chython/files/RDFrw.py +309 -0
  99. chython/files/SDFrw.py +221 -0
  100. chython/files/__init__.py +30 -0
  101. chython/files/_convert.py +207 -0
  102. chython/files/_mapping.py +117 -0
  103. chython/files/_xyz.cp313-win_amd64.pyd +0 -0
  104. chython/files/_xyz.pyx +74 -0
  105. chython/files/daylight/__init__.py +24 -0
  106. chython/files/daylight/parser.py +157 -0
  107. chython/files/daylight/smarts.py +107 -0
  108. chython/files/daylight/smiles.py +260 -0
  109. chython/files/daylight/test/__init__.py +18 -0
  110. chython/files/daylight/test/test_daylight_smarts.py +289 -0
  111. chython/files/daylight/test/test_daylight_smiles.py +101 -0
  112. chython/files/daylight/test/test_parser.py +118 -0
  113. chython/files/daylight/test/test_tokenize.py +77 -0
  114. chython/files/daylight/tokenize.py +376 -0
  115. chython/files/libinchi/__init__.py +22 -0
  116. chython/files/libinchi/libinchi.dll +0 -0
  117. chython/files/libinchi/wrapper.py +554 -0
  118. chython/files/mdl/__init__.py +25 -0
  119. chython/files/mdl/emol.py +251 -0
  120. chython/files/mdl/erxn.py +67 -0
  121. chython/files/mdl/mol.py +186 -0
  122. chython/files/mdl/read.py +223 -0
  123. chython/files/mdl/rxn.py +67 -0
  124. chython/files/mdl/stereo.py +63 -0
  125. chython/files/mdl/write.py +201 -0
  126. chython/files/opsin.py +35 -0
  127. chython/files/test/__init__.py +0 -0
  128. chython/files/test/test_inchi.py +27 -0
  129. chython/files/test/test_mrv.py +47 -0
  130. chython/files/test/test_sdf_rdf.py +64 -0
  131. chython/files/test/test_xyz.py +20 -0
  132. chython/files/xyz.py +78 -0
  133. chython/periodictable/__init__.py +69 -0
  134. chython/periodictable/base/__init__.py +25 -0
  135. chython/periodictable/base/dynamic.py +149 -0
  136. chython/periodictable/base/element.py +451 -0
  137. chython/periodictable/base/groups.py +90 -0
  138. chython/periodictable/base/periods.py +46 -0
  139. chython/periodictable/base/query.py +473 -0
  140. chython/periodictable/base/vector.py +119 -0
  141. chython/periodictable/groupI.py +252 -0
  142. chython/periodictable/groupII.py +223 -0
  143. chython/periodictable/groupIII.py +1154 -0
  144. chython/periodictable/groupIV.py +210 -0
  145. chython/periodictable/groupIX.py +196 -0
  146. chython/periodictable/groupV.py +192 -0
  147. chython/periodictable/groupVI.py +183 -0
  148. chython/periodictable/groupVII.py +162 -0
  149. chython/periodictable/groupVIII.py +161 -0
  150. chython/periodictable/groupX.py +166 -0
  151. chython/periodictable/groupXI.py +164 -0
  152. chython/periodictable/groupXII.py +158 -0
  153. chython/periodictable/groupXIII.py +239 -0
  154. chython/periodictable/groupXIV.py +262 -0
  155. chython/periodictable/groupXV.py +276 -0
  156. chython/periodictable/groupXVI.py +457 -0
  157. chython/periodictable/groupXVII.py +324 -0
  158. chython/periodictable/groupXVIII.py +260 -0
  159. chython/periodictable/test/__init__.py +0 -0
  160. chython/periodictable/test/test_element.py +100 -0
  161. chython/reactor/__init__.py +24 -0
  162. chython/reactor/base.py +222 -0
  163. chython/reactor/reactor.py +177 -0
  164. chython/reactor/test/__init__.py +18 -0
  165. chython/reactor/test/test_reactor.py +40 -0
  166. chython/reactor/test/test_transformer.py +78 -0
  167. chython/reactor/transformer.py +72 -0
  168. chython/utils/__init__.py +50 -0
  169. chython/utils/free_wilson.py +114 -0
  170. chython/utils/grid.py +130 -0
  171. chython/utils/retro.py +139 -0
  172. chython-2.19.dist-info/METADATA +105 -0
  173. chython-2.19.dist-info/RECORD +175 -0
  174. chython-2.19.dist-info/WHEEL +4 -0
  175. chython-2.19.dist-info/licenses/LICENSE +165 -0
@@ -0,0 +1,202 @@
1
+ # -*- coding: utf-8 -*-
2
+ #
3
+ # Copyright 2021-2026 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 ndarray, uint8, zeros
24
+
25
+
26
+ class LinearFingerprint:
27
+ __slots__ = ()
28
+ """
29
+ Linear fragments fingerprints.
30
+ Transform structures into fingerprints based on linear fragments descriptors.
31
+ Also count of fragments takes into account by activating multiple bits,
32
+ but less or equal to `number_bit_pairs`.To take into account
33
+ all repeating fragments put 0 as a value of `number_bit_pairs` parameter.
34
+
35
+ For example `CC` fragment found 4 times and `number_bit_pairs` is set to 3.
36
+ In this case will be activated 3 bits: for count 1, for count 2 and for count 3.
37
+ This gives intersection in bits with another structure with only 2 `CC` fragments.
38
+ """
39
+
40
+ def linear_fingerprint(self, min_radius: int = 1, max_radius: int = 4,
41
+ length: int = 1024, number_active_bits: int = 2,
42
+ number_bit_pairs: int = 4) -> ndarray:
43
+ """
44
+ Transform structures into array of binary features.
45
+
46
+ :param min_radius: minimal length of fragments
47
+ :param max_radius: maximum length of fragments
48
+ :param length: bit string's length. Should be power of 2
49
+ :param number_active_bits: number of active bits for each hashed tuple
50
+ :param number_bit_pairs: describe how much repeating fragments we can count in hashable fingerprint (if
51
+ number of fragment in molecule greater or equal this number, we will activate only this number of
52
+ fragments). To take into account all repeating fragments put 0 as a value.
53
+
54
+ :return: array(n_features)
55
+ """
56
+ bits = self.linear_bit_set(min_radius, max_radius, length, number_active_bits,
57
+ number_bit_pairs)
58
+ fingerprints = zeros(length, dtype=uint8)
59
+ fingerprints[list(bits)] = 1
60
+ return fingerprints
61
+
62
+ def linear_bit_set(self, min_radius: int = 1, max_radius: int = 4, length: int = 1024, number_active_bits: int = 2,
63
+ number_bit_pairs: int = 4) -> set[int]:
64
+ """
65
+ Transform structure into set of indexes of True-valued features.
66
+
67
+ :param min_radius: minimal length of fragments
68
+ :param max_radius: maximum length of fragments
69
+ :param length: bit string's length. Should be power of 2
70
+ :param number_active_bits: number of active bits for each hashed tuple
71
+ :param number_bit_pairs: describe how much repeating fragments we can count in hashable fingerprint (if
72
+ number of fragment in molecule greater or equal this number, we will activate only this number of
73
+ fragments). To take into account all repeating fragments put 0 as a value.
74
+ """
75
+ mask = length - 1
76
+ log = int(log2(length))
77
+
78
+ hashes = self.linear_hash_set(min_radius, max_radius, number_bit_pairs)
79
+ active_bits = set()
80
+ for tpl in hashes:
81
+ active_bits.add(tpl & mask)
82
+ if number_active_bits == 2:
83
+ active_bits.add((tpl >> log) & mask)
84
+ elif number_active_bits > 2:
85
+ for _ in range(1, number_active_bits):
86
+ tpl >>= log # shift
87
+ active_bits.add(tpl & mask)
88
+ return active_bits
89
+
90
+ def linear_hash_set(self, min_radius: int = 1, max_radius: int = 4, number_bit_pairs: int = 4) -> set[int]:
91
+ """
92
+ Transform structure into set of integer hashes of fragments with count information.
93
+
94
+ :param min_radius: minimal length of fragments
95
+ :param max_radius: maximum length of fragments
96
+ :param number_bit_pairs: describe how much repeating fragments we can count in hashable fingerprint (if
97
+ number of fragment in molecule greater or equal this number, we will activate only this number of
98
+ fragments). To take into account all repeating fragments put 0 as a value.
99
+ """
100
+ if not number_bit_pairs:
101
+ number_bit_pairs = 999_999_999 # unreachable count
102
+
103
+ return {hash((*tpl, cnt)) for tpl, count in
104
+ self._fragments(min_radius, max_radius).items()
105
+ for cnt in range(min(len(count), number_bit_pairs))}
106
+
107
+ def linear_hash_smiles(self, min_radius: int = 1, max_radius: int = 4,
108
+ number_bit_pairs: int = 4) -> dict[int, list[str]]:
109
+ """
110
+ Transform structure into dict of integer hashes of fragments with count information and
111
+ corresponding fragment SMILES.
112
+
113
+ :param min_radius: minimal length of fragments
114
+ :param max_radius: maximum length of fragments
115
+ :param number_bit_pairs: describe how much repeating fragments we can count in hashable fingerprint (if
116
+ number of fragment in molecule greater or equal this number, we will activate only this number of
117
+ fragments). To take into account all repeating fragments put 0 as a value.
118
+ """
119
+ if not number_bit_pairs:
120
+ number_bit_pairs = 999_999_999 # unreachable count
121
+
122
+ out = defaultdict(set)
123
+ for frg, chains in self._fragments(min_radius, max_radius).items():
124
+ chain = chains[0]
125
+ smiles = [self._format_atom(chain[0], None, stereo=False)]
126
+ for x, y in zip(chain, chain[1:]):
127
+ smiles.append(self._format_bond(x, y, None, stereo=False, aromatic=False))
128
+ smiles.append(self._format_atom(y, None, stereo=False))
129
+ smiles = ''.join(smiles)
130
+
131
+ for cnt in range(min(len(chains), number_bit_pairs)):
132
+ out[hash((*frg, cnt))].add(smiles) # collisions possible
133
+ return {k: list(v) for k, v in out.items()}
134
+
135
+ def linear_smiles_hash(self, min_radius: int = 1, max_radius: int = 4,
136
+ number_bit_pairs: int = 4) -> dict[str, list[int]]:
137
+ """
138
+ Transform structure into dict of fragment SMILES and list of corresponding integer hashes of fragments.
139
+
140
+ :param min_radius: minimal length of fragments
141
+ :param max_radius: maximum length of fragments
142
+ :param number_bit_pairs: describe how much repeating fragments we can count in hashable fingerprint (if
143
+ number of fragment in molecule greater or equal this number, we will activate only this number of
144
+ fragments). To take into account all repeating fragments put 0 as a value.
145
+ """
146
+ out = defaultdict(list)
147
+ for k, sl in self.linear_hash_smiles(min_radius, max_radius, number_bit_pairs).items():
148
+ for s in sl:
149
+ out[s].append(k)
150
+ return dict(out)
151
+
152
+ def _chains(self, min_radius: int = 1, max_radius: int = 4) -> set[tuple[int, ...]]:
153
+ queue: deque[tuple[int, ...]] # typing
154
+ atoms = self._atoms
155
+ bonds = self._bonds
156
+
157
+ if min_radius == 1:
158
+ arr = {(x,) for x in atoms}
159
+ if max_radius == 1: # special case
160
+ return arr
161
+ else:
162
+ queue = deque(arr)
163
+ else:
164
+ arr = set()
165
+ queue = deque((x,) for x in atoms)
166
+
167
+ while queue:
168
+ now = queue.popleft()
169
+ var = [now + (x,) for x in bonds[now[-1]] if x not in now]
170
+ if var:
171
+ if len(var[0]) < max_radius:
172
+ queue.extend(var)
173
+ if len(var[0]) >= min_radius:
174
+ for frag in var:
175
+ rev = frag[::-1]
176
+ arr.add(frag if frag > rev else rev)
177
+ return arr
178
+
179
+ def _fragments(self, min_radius: int = 1, max_radius: int = 4) -> dict[tuple[int, ...], list[tuple[int, ...]]]:
180
+ atoms = self._atom_identifiers
181
+ bonds = self._bonds
182
+ out = defaultdict(list)
183
+
184
+ for frag in self._chains(min_radius, max_radius):
185
+ var = [atoms[frag[0]]]
186
+ for x, y in zip(frag, frag[1:]):
187
+ var.append(int(bonds[x][y]))
188
+ var.append(atoms[y])
189
+ var = tuple(var)
190
+ rev_var = var[::-1]
191
+ if var > rev_var:
192
+ out[var].append(frag)
193
+ else:
194
+ out[rev_var].append(frag[::-1])
195
+ return dict(out)
196
+
197
+ @property
198
+ def _atom_identifiers(self) -> dict[int, int]:
199
+ raise NotImplementedError
200
+
201
+
202
+ __all__ = ['LinearFingerprint']
@@ -0,0 +1,132 @@
1
+ # -*- coding: utf-8 -*-
2
+ #
3
+ # Copyright 2021-2026 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 ndarray, uint8, zeros
24
+
25
+
26
+ class MorganFingerprint:
27
+ __slots__ = ()
28
+
29
+ def morgan_fingerprint(self, min_radius: int = 1, max_radius: int = 4,
30
+ length: int = 1024, number_active_bits: int = 2) -> ndarray:
31
+ """
32
+ Transform structures into array of binary features.
33
+ Morgan fingerprints. Similar to RDkit implementation.
34
+
35
+ :param min_radius: minimal radius of EC
36
+ :param max_radius: maximum radius of EC
37
+ :param length: bit string's length. Should be power of 2
38
+ :param number_active_bits: number of active bits for each hashed tuple
39
+
40
+ :return: array(n_features)
41
+ """
42
+ bits = self.morgan_bit_set(min_radius, max_radius, length, number_active_bits)
43
+ fingerprints = zeros(length, dtype=uint8)
44
+ fingerprints[list(bits)] = 1
45
+ return fingerprints
46
+
47
+ def morgan_bit_set(self, min_radius: int = 1, max_radius: int = 4,
48
+ length: int = 1024, number_active_bits: int = 2) -> set[int]:
49
+ """
50
+ Transform structures into set of indexes of True-valued features.
51
+
52
+ :param min_radius: minimal radius of EC
53
+ :param max_radius: maximum radius of EC
54
+ :param length: bit string's length. Should be power of 2
55
+ :param number_active_bits: number of active bits for each hashed tuple
56
+ """
57
+ mask = length - 1
58
+ log = int(log2(length))
59
+
60
+ active_bits = set()
61
+ for tpl in self.morgan_hash_set(min_radius, max_radius):
62
+ active_bits.add(tpl & mask)
63
+ if number_active_bits == 2:
64
+ active_bits.add(tpl >> log & mask)
65
+ elif number_active_bits > 2:
66
+ for _ in range(1, number_active_bits):
67
+ tpl >>= log
68
+ active_bits.add(tpl & mask)
69
+ return active_bits
70
+
71
+ def morgan_hash_set(self, min_radius: int = 1, max_radius: int = 4) -> set[int]:
72
+ """
73
+ Transform structures into integer hashes of atoms with EC.
74
+
75
+ :param min_radius: minimal radius of EC
76
+ :param max_radius: maximum radius of EC
77
+ """
78
+ return {x for x in self._morgan_hash_dict(min_radius, max_radius) for x in x.values()}
79
+
80
+ def morgan_hash_smiles(self, min_radius: int = 1, max_radius: int = 4) -> dict[int, list[str]]:
81
+ """
82
+ Transform structures into dictionary of hashes of atoms with EC and corresponding SMILES.
83
+
84
+ :param min_radius: minimal radius of EC
85
+ :param max_radius: maximum radius of EC
86
+ """
87
+ smiles_dict = defaultdict(set)
88
+ for radius, hash_dict in enumerate(self._morgan_hash_dict(min_radius, max_radius), min_radius - 1):
89
+ for atom, morgan_hash in hash_dict.items():
90
+ smiles_dict[morgan_hash].add(format(self.augmented_substructure((atom,), deep=radius), 'A'))
91
+ return {k: list(v) for k, v in smiles_dict.items()}
92
+
93
+ def morgan_smiles_hash(self, min_radius: int = 1, max_radius: int = 4) -> dict[str, list[int]]:
94
+ """
95
+ Transform structures into dictionary of smiles and corresponding hashes of atoms with EC.
96
+
97
+ :param min_radius: minimal radius of EC
98
+ :param max_radius: maximum radius of EC
99
+ """
100
+ out = defaultdict(list)
101
+ for k, sl in self.morgan_hash_smiles(min_radius, max_radius).items():
102
+ for s in sl:
103
+ out[s].append(k)
104
+ return dict(out)
105
+
106
+ def _morgan_hash_dict(self, min_radius: int = 1, max_radius: int = 4) -> list[dict[int, int]]:
107
+ """
108
+ Transform structures into integer hashes of atoms with EC.
109
+ Returns list of atom-hash pairs for different radii.
110
+
111
+ :param min_radius: minimal radius of EC
112
+ :param max_radius: maximum radius of EC
113
+ """
114
+ assert min_radius >= 1, 'min_radius should be positive'
115
+ assert max_radius >= min_radius, 'max_radius should be greater or equal to min_radius'
116
+ identifiers = self._atom_identifiers
117
+
118
+ bonds = self._bonds
119
+ out = [identifiers]
120
+ for _ in range(1, max_radius):
121
+ identifiers = {idx: hash((tpl, *(x for x in sorted((int(b), identifiers[ngb])
122
+ for ngb, b in bonds[idx].items()) for x in x)))
123
+ for idx, tpl in identifiers.items()}
124
+ out.append(identifiers)
125
+ return out[-(max_radius - min_radius + 1):] # slice [min, max] radii range
126
+
127
+ @property
128
+ def _atom_identifiers(self) -> dict[int, int]:
129
+ raise NotImplementedError
130
+
131
+
132
+ __all__ = ['MorganFingerprint']
@@ -0,0 +1,18 @@
1
+ # -*- coding: utf-8 -*-
2
+ #
3
+ # Copyright 2025, 2026 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, 2026 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, 2026 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)