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