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,473 @@
1
+ # -*- coding: utf-8 -*-
2
+ #
3
+ # Copyright 2020-2026 Ramil Nugmanov <nougmanoff@protonmail.com>
4
+ # Copyright 2021 Dmitrij Zanadvornykh <zandmitrij@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 abc import ABC, abstractmethod
21
+ from functools import cached_property
22
+ from typing import Tuple, Type, List, Union, Optional
23
+ from .element import Element
24
+ from .groups import GroupXVIII
25
+
26
+
27
+ def _validate(value, prop):
28
+ if value is None:
29
+ return ()
30
+ elif isinstance(value, int):
31
+ if value < 0 or value > 14:
32
+ raise ValueError(f'{prop} should be in range [0, 14]')
33
+ return (value,)
34
+ elif isinstance(value, (tuple, list)):
35
+ if not all(isinstance(x, int) for x in value):
36
+ raise TypeError(f'{prop} should be list or tuple of ints')
37
+ if any(x < 0 or x > 14 for x in value):
38
+ raise ValueError(f'{prop} should be in range [0, 14]')
39
+ if len(set(value)) != len(value):
40
+ raise ValueError(f'{prop} should be unique')
41
+ return tuple(sorted(value))
42
+ else:
43
+ raise TypeError(f'{prop} should be int or list or tuple of ints')
44
+
45
+
46
+ class Query(ABC):
47
+ __slots__ = ('_neighbors', '_hybridization', '_masked')
48
+
49
+ def __init__(self, neighbors: Union[int, Tuple[int, ...], None] = None,
50
+ hybridization: Union[int, Tuple[int, ...], None] = None, masked: bool = False):
51
+ self.neighbors = neighbors
52
+ self.hybridization = hybridization
53
+ self.masked = masked
54
+
55
+ @property
56
+ @abstractmethod
57
+ def atomic_symbol(self) -> str:
58
+ ...
59
+
60
+ @property
61
+ def neighbors(self) -> Tuple[int, ...]:
62
+ return self._neighbors
63
+
64
+ @neighbors.setter
65
+ def neighbors(self, value):
66
+ self._neighbors = _validate(value, 'neighbors')
67
+
68
+ @property
69
+ def hybridization(self) -> Tuple[int, ...]:
70
+ return self._hybridization
71
+
72
+ @hybridization.setter
73
+ def hybridization(self, value):
74
+ if value is None:
75
+ self._hybridization = ()
76
+ elif isinstance(value, int):
77
+ if value < 1 or value > 4:
78
+ raise ValueError('hybridization should be in range [1, 4]')
79
+ self._hybridization = (value,)
80
+ elif isinstance(value, (tuple, list)):
81
+ if not all(isinstance(h, int) for h in value):
82
+ raise TypeError('hybridizations should be list or tuple of ints')
83
+ if any(h < 1 or h > 4 for h in value):
84
+ raise ValueError('hybridizations should be in range [1, 4]')
85
+ if len(set(value)) != len(value):
86
+ raise ValueError('hybridizations should be unique')
87
+ self._hybridization = tuple(sorted(value))
88
+ else:
89
+ raise TypeError('hybridization should be int or list or tuple of ints')
90
+
91
+ @property
92
+ def masked(self):
93
+ return self._masked
94
+
95
+ @masked.setter
96
+ def masked(self, value):
97
+ if not isinstance(value, bool):
98
+ raise TypeError('masked should be bool')
99
+ self._masked = value
100
+
101
+ def copy(self, full=False):
102
+ copy = object.__new__(self.__class__)
103
+ copy._neighbors = self.neighbors
104
+ copy._hybridization = self.hybridization
105
+
106
+ copy._masked = self.masked if full else False
107
+ return copy
108
+
109
+ def __copy__(self):
110
+ return self.copy()
111
+
112
+ def __repr__(self):
113
+ return f'{self.__class__.__name__}()'
114
+
115
+
116
+ class ExtendedQuery(Query, ABC):
117
+ __slots__ = ('_charge', '_is_radical', '_heteroatoms', '_ring_sizes', '_implicit_hydrogens', '_stereo')
118
+
119
+ def __init__(self, charge: int = 0, is_radical: bool = False, heteroatoms: Union[int, Tuple[int, ...], None] = None,
120
+ ring_sizes: Union[int, Tuple[int, ...], None] = None,
121
+ implicit_hydrogens: Union[int, Tuple[int, ...], None] = None, stereo: Optional[bool] = None, **kwargs):
122
+ super().__init__(**kwargs)
123
+ self.charge = charge
124
+ self.is_radical = is_radical
125
+ self.heteroatoms = heteroatoms
126
+ self.ring_sizes = ring_sizes
127
+ self.implicit_hydrogens = implicit_hydrogens
128
+ self.stereo = stereo
129
+
130
+ @property
131
+ def charge(self) -> int:
132
+ """
133
+ Charge of atom
134
+ """
135
+ return self._charge
136
+
137
+ @charge.setter
138
+ def charge(self, value: int):
139
+ if not isinstance(value, int):
140
+ raise TypeError('formal charge should be int in range [-4, 4]')
141
+ elif value > 4 or value < -4:
142
+ raise ValueError('formal charge should be in range [-4, 4]')
143
+ self._charge = value
144
+
145
+ @property
146
+ def is_radical(self) -> bool:
147
+ """
148
+ Radical state of atoms
149
+ """
150
+ return self._is_radical
151
+
152
+ @is_radical.setter
153
+ def is_radical(self, value: bool):
154
+ if not isinstance(value, bool):
155
+ raise TypeError('bool expected')
156
+ self._is_radical = value
157
+
158
+ @property
159
+ def heteroatoms(self) -> Tuple[int, ...]:
160
+ return self._heteroatoms
161
+
162
+ @heteroatoms.setter
163
+ def heteroatoms(self, value):
164
+ self._heteroatoms = _validate(value, 'heteroatoms')
165
+
166
+ @property
167
+ def implicit_hydrogens(self) -> Tuple[int, ...]:
168
+ return self._implicit_hydrogens
169
+
170
+ @implicit_hydrogens.setter
171
+ def implicit_hydrogens(self, value):
172
+ self._implicit_hydrogens = _validate(value, 'implicit hydrogens')
173
+
174
+ @property
175
+ def ring_sizes(self) -> Tuple[int, ...]:
176
+ """
177
+ Atom rings sizes.
178
+ """
179
+ return self._ring_sizes
180
+
181
+ @ring_sizes.setter
182
+ def ring_sizes(self, value):
183
+ if value is None:
184
+ self._ring_sizes = ()
185
+ elif isinstance(value, int):
186
+ if value < 3 and value != 0:
187
+ raise ValueError('rings should be greater or equal 3. ring equal to zero is no ring atom mark')
188
+ self._ring_sizes = (value,)
189
+ elif isinstance(value, (tuple, list)):
190
+ if not all(isinstance(x, int) for x in value):
191
+ raise TypeError('rings should be list or tuple of ints')
192
+ if any(x < 3 for x in value):
193
+ raise ValueError('rings should be greater or equal 3')
194
+ if len(set(value)) != len(value):
195
+ raise ValueError('rings should be unique')
196
+ self._ring_sizes = tuple(sorted(value))
197
+ else:
198
+ raise TypeError('rings should be int or list or tuple of ints')
199
+
200
+ @property
201
+ def stereo(self):
202
+ return self._stereo
203
+
204
+ @stereo.setter
205
+ def stereo(self, value: Optional[bool]):
206
+ if value is not None and not isinstance(value, bool):
207
+ raise TypeError('stereo should be bool')
208
+ self._stereo = value
209
+
210
+ def copy(self, full=False):
211
+ copy = super().copy(full=full)
212
+ copy._charge = self.charge
213
+ copy._is_radical = self.is_radical
214
+ copy._heteroatoms = self.heteroatoms
215
+ copy._implicit_hydrogens = self.implicit_hydrogens
216
+ copy._ring_sizes = self.ring_sizes
217
+
218
+ copy._stereo = self.stereo if full else None
219
+ return copy
220
+
221
+
222
+ class AnyMetal(Query):
223
+ """
224
+ Charge and radical ignored any metal. Rings, hydrogens and heteroatoms count also ignored.
225
+
226
+ Class designed for d-elements matching in standardization.
227
+ """
228
+ __slots__ = ()
229
+
230
+ @property
231
+ def atomic_symbol(self) -> str:
232
+ return 'M'
233
+
234
+ def __eq__(self, other):
235
+ if not isinstance(other, Element):
236
+ return False
237
+ if other.is_forming_single_bonds or isinstance(other, GroupXVIII):
238
+ return False
239
+ if self.neighbors and other.neighbors not in self.neighbors:
240
+ return False
241
+ if self.hybridization and other.hybridization not in self.hybridization:
242
+ return False
243
+ return True
244
+
245
+
246
+ class AnyElement(ExtendedQuery):
247
+ __slots__ = ()
248
+
249
+ @property
250
+ def atomic_symbol(self) -> str:
251
+ return 'A'
252
+
253
+ def __eq__(self, other):
254
+ """
255
+ Compare attached to molecules elements and query elements
256
+ """
257
+ if not isinstance(other, Element):
258
+ return False
259
+ if self.charge != other.charge:
260
+ return False
261
+ if self.is_radical != other.is_radical:
262
+ return False
263
+ if self.neighbors and other.neighbors not in self.neighbors:
264
+ return False
265
+ if self.hybridization and other.hybridization not in self.hybridization:
266
+ return False
267
+ if self.ring_sizes:
268
+ if self.ring_sizes[0]:
269
+ if other.ring_sizes.isdisjoint(self.ring_sizes):
270
+ return False
271
+ elif other.ring_sizes: # not in ring expected
272
+ return False
273
+ if self.implicit_hydrogens and other.implicit_hydrogens not in self.implicit_hydrogens:
274
+ return False
275
+ if self.heteroatoms and other.heteroatoms not in self.heteroatoms:
276
+ return False
277
+ return True
278
+
279
+
280
+ class ListElement(ExtendedQuery):
281
+ __slots__ = ('_elements', '__dict__')
282
+
283
+ def __init__(self, elements: List[str], **kwargs):
284
+ """
285
+ Elements list
286
+ """
287
+ if not isinstance(elements, (list, tuple)) or not elements:
288
+ raise ValueError('invalid elements list')
289
+ tmp = []
290
+ for x in elements:
291
+ if isinstance(x, int):
292
+ tmp.append(Element.from_atomic_number(x).__name__)
293
+ elif isinstance(x, str):
294
+ tmp.append(Element.from_symbol(x).__name__)
295
+ else:
296
+ raise ValueError(f'invalid element: {x}')
297
+ super().__init__(**kwargs)
298
+ self._elements = tuple(tmp)
299
+
300
+ @property
301
+ def atomic_symbol(self) -> str:
302
+ return ','.join(self._elements)
303
+
304
+ @cached_property
305
+ def atomic_numbers(self):
306
+ return tuple(x.atomic_number.fget(None) for x in Element.__subclasses__() if x.__name__ in self._elements)
307
+
308
+ def copy(self, full=False):
309
+ copy = super().copy(full=full)
310
+ copy._elements = self._elements
311
+ return copy
312
+
313
+ def __eq__(self, other):
314
+ """
315
+ Compare attached to molecules elements and query elements
316
+ """
317
+ if not isinstance(other, Element):
318
+ return False
319
+ if other.atomic_number not in self.atomic_numbers:
320
+ return False
321
+ if self.charge != other.charge:
322
+ return False
323
+ if self.is_radical != other.is_radical:
324
+ return False
325
+ if self.neighbors and other.neighbors not in self.neighbors:
326
+ return False
327
+ if self.hybridization and other.hybridization not in self.hybridization:
328
+ return False
329
+ if self.ring_sizes:
330
+ if self.ring_sizes[0]:
331
+ if other.ring_sizes.isdisjoint(self.ring_sizes):
332
+ return False
333
+ elif other.ring_sizes: # not in ring expected
334
+ return False
335
+ if self.implicit_hydrogens and other.implicit_hydrogens not in self.implicit_hydrogens:
336
+ return False
337
+ if self.heteroatoms and other.heteroatoms not in self.heteroatoms:
338
+ return False
339
+ return True
340
+
341
+ def __repr__(self):
342
+ return f'{self.__class__.__name__}([{self.atomic_symbol}])'
343
+
344
+
345
+ class QueryElement(ExtendedQuery, ABC):
346
+ __slots__ = ('_isotope',)
347
+
348
+ def __init__(self, isotope: Optional[int] = None, **kwargs):
349
+ super().__init__(**kwargs)
350
+ self.isotope = isotope
351
+
352
+ def __repr__(self):
353
+ if self.isotope:
354
+ return f'{self.__class__.__name__}({self.isotope})'
355
+ return f'{self.__class__.__name__}()'
356
+
357
+ @property
358
+ def atomic_symbol(self) -> str:
359
+ return self.__class__.__name__[5:]
360
+
361
+ @property
362
+ @abstractmethod
363
+ def atomic_number(self) -> int:
364
+ """
365
+ Element number
366
+ """
367
+
368
+ @property
369
+ def isotope(self):
370
+ return self._isotope
371
+
372
+ @isotope.setter
373
+ def isotope(self, value: Optional[int]):
374
+ if value is not None and not isinstance(value, int):
375
+ raise TypeError('isotope must be an int')
376
+ self._isotope = value
377
+
378
+ @property
379
+ @abstractmethod
380
+ def mdl_isotope(self) -> int:
381
+ ...
382
+
383
+ @classmethod
384
+ def from_symbol(cls, symbol: str) -> Type[Union['QueryElement', 'AnyElement', 'AnyMetal']]:
385
+ """
386
+ get Element class by its symbol
387
+ """
388
+ if symbol == 'A':
389
+ return AnyElement
390
+ elif symbol == 'M':
391
+ return AnyMetal
392
+ try:
393
+ element = next(x for x in QueryElement.__subclasses__() if x.__name__ == f'Query{symbol}')
394
+ except StopIteration:
395
+ raise ValueError(f'QueryElement with symbol "{symbol}" not found')
396
+ return element
397
+
398
+ @classmethod
399
+ def from_atomic_number(cls, number: int) -> Type['QueryElement']:
400
+ """
401
+ get Element class by its number
402
+ """
403
+ try:
404
+ element = next(x for x in QueryElement.__subclasses__() if x.atomic_number.fget(None) == number)
405
+ except StopIteration:
406
+ raise ValueError(f'QueryElement with number "{number}" not found')
407
+ return element
408
+
409
+ @classmethod
410
+ def from_atom(cls, atom: 'Element', neighbors=False, hybridization=False, heteroatoms=False,
411
+ hydrogens=False, ring_sizes=False, stereo=False) -> 'QueryElement':
412
+ """
413
+ get QueryElement or AnyElement object from Element object or copy of QueryElement or AnyElement
414
+ """
415
+ if not isinstance(atom, Element):
416
+ raise TypeError('Element or Query expected')
417
+
418
+ # transfer true atomic props
419
+ query = cls.from_atomic_number(atom.atomic_number)(atom.isotope)
420
+ query._charge = atom.charge
421
+ query._is_radical = atom.is_radical
422
+
423
+ if neighbors:
424
+ query._neighbors = (atom.neighbors,)
425
+ if hybridization:
426
+ query._hybridization = (atom.hybridization,)
427
+ if heteroatoms:
428
+ query._heteroatoms = (atom.heteroatoms,)
429
+ if ring_sizes:
430
+ query._ring_sizes = atom.ring_sizes
431
+ if hydrogens and atom.implicit_hydrogens is not None:
432
+ query._implicit_hydrogens = (atom.implicit_hydrogens,)
433
+ if stereo:
434
+ query._stereo = atom.stereo
435
+ return query
436
+
437
+ def copy(self, full=False):
438
+ copy = super().copy(full=full)
439
+ copy._isotope = self.isotope
440
+ return copy
441
+
442
+ def __eq__(self, other):
443
+ """
444
+ compare attached to molecules elements and query elements
445
+ """
446
+ if not isinstance(other, Element):
447
+ return False
448
+ if self.atomic_number != other.atomic_number:
449
+ return False
450
+ if self.charge != other.charge:
451
+ return False
452
+ if self.is_radical != other.is_radical:
453
+ return False
454
+ if self.isotope and self.isotope != other.isotope:
455
+ return False
456
+ if self.neighbors and other.neighbors not in self.neighbors:
457
+ return False
458
+ if self.hybridization and other.hybridization not in self.hybridization:
459
+ return False
460
+ if self.ring_sizes:
461
+ if self.ring_sizes[0]:
462
+ if other.ring_sizes.isdisjoint(self.ring_sizes):
463
+ return False
464
+ elif other.ring_sizes: # not in ring expected
465
+ return False
466
+ if self.implicit_hydrogens and other.implicit_hydrogens not in self.implicit_hydrogens:
467
+ return False
468
+ if self.heteroatoms and other.heteroatoms not in self.heteroatoms:
469
+ return False
470
+ return True
471
+
472
+
473
+ __all__ = ['Query', 'ExtendedQuery', 'QueryElement', 'AnyElement', 'AnyMetal', 'ListElement']
@@ -0,0 +1,119 @@
1
+ # -*- coding: utf-8 -*-
2
+ #
3
+ # Copyright 2024 Denis Lipatov <denis.lipatov163@gmail.com>
4
+ # Copyright 2024 Vyacheslav Grigorev <slavick2000@yandex.ru>
5
+ # Copyright 2024 Timur Gimadiev <timur.gimadiev@gmail.com>
6
+ # Copyright 2024-2026 Ramil Nugmanov <nougmanoff@protonmail.com>
7
+ # This file is part of chython.
8
+ #
9
+ # chython is free software; you can redistribute it and/or modify
10
+ # it under the terms of the GNU Lesser General Public License as published by
11
+ # the Free Software Foundation; either version 3 of the License, or
12
+ # (at your option) any later version.
13
+ #
14
+ # This program is distributed in the hope that it will be useful,
15
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
16
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17
+ # GNU Lesser General Public License for more details.
18
+ #
19
+ # You should have received a copy of the GNU Lesser General Public License
20
+ # along with this program; if not, see <https://www.gnu.org/licenses/>.
21
+ #
22
+ from math import cos, sin, hypot, atan2
23
+
24
+
25
+ class Vector:
26
+ __slots__ = ('x', 'y')
27
+
28
+ def __init__(self, x: float = 0., y: float = 0.):
29
+ self.x = x
30
+ self.y = y
31
+
32
+ def __repr__(self):
33
+ return f'Vector({self.x}, {self.y})'
34
+
35
+ def __neg__(self):
36
+ """
37
+ A class method that inverts the current coordinates of objects of the class
38
+ """
39
+ return Vector(-self.x, -self.y)
40
+
41
+ def __sub__(self, vector: 'Vector'):
42
+ """
43
+ A method for the operation of subtraction between vectors
44
+ """
45
+ return Vector(self.x - vector.x, self.y - vector.y)
46
+
47
+ def __add__(self, vector: 'Vector'):
48
+ """
49
+ A method for the operation of addition between vectors
50
+ """
51
+ return Vector(self.x + vector.x, self.y + vector.y)
52
+
53
+ def __truediv__(self, scalar: float):
54
+ """
55
+ A class method that divides the coordinates of the vector by a given scalar
56
+ """
57
+ return Vector(self.x / scalar, self.y / scalar)
58
+
59
+ def __mul__(self, scalar: float):
60
+ """
61
+ Multiplies the coordinates of the current vector by an arbitrary real number
62
+ """
63
+ return Vector(self.x * scalar, self.y * scalar)
64
+
65
+ def __float__(self):
66
+ """
67
+ Calculates the length of the current vector
68
+
69
+ Returns float
70
+ """
71
+ return hypot(self.x, self.y)
72
+
73
+ def __iter__(self):
74
+ yield self.x
75
+ yield self.y
76
+
77
+ def __len__(self):
78
+ return 2
79
+
80
+ def __matmul__(self, vector: 'Vector'):
81
+ return self.x * vector.y - self.y * vector.x
82
+
83
+ def __or__(self, vector: 'Vector'):
84
+ """
85
+ Calculate distance between two vectors
86
+ """
87
+ return hypot(vector.x - self.x, vector.y - self.y)
88
+
89
+ def rotate(self, angle: float, vector: 'Vector' = None):
90
+ """
91
+ A method that rotates the vector by the angle in radians
92
+ """
93
+ c = cos(angle)
94
+ s = sin(angle)
95
+ if vector is None:
96
+ return Vector(self.x * c - self.y * s, self.x * s + self.y * c)
97
+ xy = self - vector
98
+ return vector + Vector(xy.x * c - xy.y * s, xy.x * s + xy.y * c)
99
+
100
+ def normalise(self):
101
+ """
102
+ Normalization of coordinates (dividing them by the length of the vector itself)
103
+ """
104
+ if ln := float(self):
105
+ return self / ln
106
+ return self
107
+
108
+ def angle(self, vector: 'Vector' = None) -> float:
109
+ """
110
+ A method calculates the angle of inclination of the current vector
111
+ or the vector between given vector and the current vector.
112
+ """
113
+ if vector is None:
114
+ return atan2(self.y, self.x)
115
+ else:
116
+ return atan2(vector.y - self.y, vector.x - self.x)
117
+
118
+
119
+ __all__ = ['Vector']