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