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.
- chython/__init__.py +35 -0
- chython/_functions.py +95 -0
- chython/_java.py +57 -0
- chython/algorithms/__init__.py +18 -0
- chython/algorithms/_isomorphism.cp313-win_amd64.pyd +0 -0
- chython/algorithms/_isomorphism.pyx +189 -0
- chython/algorithms/_rings.cp313-win_amd64.pyd +0 -0
- chython/algorithms/_rings.pyx +478 -0
- chython/algorithms/aromatics/__init__.py +27 -0
- chython/algorithms/aromatics/_rules.py +124 -0
- chython/algorithms/aromatics/kekule.py +532 -0
- chython/algorithms/aromatics/test/__init__.py +18 -0
- chython/algorithms/aromatics/test/test_kekule.py +160 -0
- chython/algorithms/aromatics/test/test_thiele.py +263 -0
- chython/algorithms/aromatics/thiele.py +222 -0
- chython/algorithms/calculate2d/__init__.py +23 -0
- chython/algorithms/calculate2d/clean2d.js +1 -0
- chython/algorithms/calculate2d/molecule.py +191 -0
- chython/algorithms/calculate2d/reaction.py +75 -0
- chython/algorithms/conformers.py +106 -0
- chython/algorithms/depict.py +596 -0
- chython/algorithms/fingerprints/__init__.py +43 -0
- chython/algorithms/fingerprints/linear.py +202 -0
- chython/algorithms/fingerprints/morgan.py +132 -0
- chython/algorithms/fingerprints/test/__init__.py +18 -0
- chython/algorithms/fingerprints/test/test_linear.py +127 -0
- chython/algorithms/fingerprints/test/test_morgan.py +79 -0
- chython/algorithms/groups/__init__.py +212 -0
- chython/algorithms/groups/_functional.py +290 -0
- chython/algorithms/groups/_oxidations.py +70 -0
- chython/algorithms/groups/_protective.py +964 -0
- chython/algorithms/groups/_reactions.py +809 -0
- chython/algorithms/groups/_reductions.py +88 -0
- chython/algorithms/groups/_transformations.py +278 -0
- chython/algorithms/groups/test/__init__.py +18 -0
- chython/algorithms/groups/test/test_deprotection.py +108 -0
- chython/algorithms/groups/test/test_reactions.py +377 -0
- chython/algorithms/isomorphism.py +664 -0
- chython/algorithms/mapping/__init__.py +188 -0
- chython/algorithms/mapping/_groups.py +94 -0
- chython/algorithms/mapping/_reactions.py +72 -0
- chython/algorithms/mapping/attention.py +266 -0
- chython/algorithms/mapping/reconstruct.py +346 -0
- chython/algorithms/mcs.py +201 -0
- chython/algorithms/morgan.py +77 -0
- chython/algorithms/rings.py +178 -0
- chython/algorithms/smiles.py +597 -0
- chython/algorithms/standardize/__init__.py +30 -0
- chython/algorithms/standardize/_groups.py +958 -0
- chython/algorithms/standardize/_isomers.py +330 -0
- chython/algorithms/standardize/_metal_organics.py +189 -0
- chython/algorithms/standardize/_reagents.py +44 -0
- chython/algorithms/standardize/_salts.py +68 -0
- chython/algorithms/standardize/molecule.py +475 -0
- chython/algorithms/standardize/reaction.py +384 -0
- chython/algorithms/standardize/resonance.py +191 -0
- chython/algorithms/standardize/salts.py +128 -0
- chython/algorithms/standardize/saturation.py +373 -0
- chython/algorithms/standardize/test/__init__.py +18 -0
- chython/algorithms/standardize/test/test_canonicalize.py +66 -0
- chython/algorithms/standardize/test/test_groups.py +146 -0
- chython/algorithms/standardize/test/test_isomers.py +198 -0
- chython/algorithms/stereo.py +1215 -0
- chython/algorithms/tautomers/__init__.py +167 -0
- chython/algorithms/tautomers/_acid.py +59 -0
- chython/algorithms/tautomers/_base.py +116 -0
- chython/algorithms/tautomers/_keto_enol.py +107 -0
- chython/algorithms/tautomers/acid_base.py +208 -0
- chython/algorithms/tautomers/heteroarenes.py +103 -0
- chython/algorithms/tautomers/keto_enol.py +143 -0
- chython/algorithms/tautomers/test/__init__.py +18 -0
- chython/algorithms/tautomers/test/test_tautomers.py +80 -0
- chython/algorithms/test/__init__.py +18 -0
- chython/algorithms/test/test_isomorphism.py +58 -0
- chython/algorithms/test/test_smiles.py +76 -0
- chython/algorithms/x3dom.py +350 -0
- chython/containers/__init__.py +48 -0
- chython/containers/_pack_v2.cp313-win_amd64.pyd +0 -0
- chython/containers/_pack_v2.pyx +271 -0
- chython/containers/_unpack_v0v2.cp313-win_amd64.pyd +0 -0
- chython/containers/_unpack_v0v2.pyx +302 -0
- chython/containers/bonds.py +255 -0
- chython/containers/cgr.py +115 -0
- chython/containers/chimera.py +63 -0
- chython/containers/graph.py +199 -0
- chython/containers/molecule.py +817 -0
- chython/containers/query.py +71 -0
- chython/containers/rdkit.py +183 -0
- chython/containers/reaction.py +324 -0
- chython/containers/test/__init__.py +18 -0
- chython/containers/test/test_molecule.py +117 -0
- chython/containers/test/test_pack.py +60 -0
- chython/containers/test/test_rdkit.py +70 -0
- chython/containers/test/test_reaction.py +40 -0
- chython/exceptions.py +114 -0
- chython/files/MRVrw.py +532 -0
- chython/files/PDBrw.py +255 -0
- chython/files/RDFrw.py +309 -0
- chython/files/SDFrw.py +221 -0
- chython/files/__init__.py +30 -0
- chython/files/_convert.py +207 -0
- chython/files/_mapping.py +117 -0
- chython/files/_xyz.cp313-win_amd64.pyd +0 -0
- chython/files/_xyz.pyx +74 -0
- chython/files/daylight/__init__.py +24 -0
- chython/files/daylight/parser.py +157 -0
- chython/files/daylight/smarts.py +107 -0
- chython/files/daylight/smiles.py +260 -0
- chython/files/daylight/test/__init__.py +18 -0
- chython/files/daylight/test/test_daylight_smarts.py +289 -0
- chython/files/daylight/test/test_daylight_smiles.py +101 -0
- chython/files/daylight/test/test_parser.py +118 -0
- chython/files/daylight/test/test_tokenize.py +77 -0
- chython/files/daylight/tokenize.py +376 -0
- chython/files/libinchi/__init__.py +22 -0
- chython/files/libinchi/libinchi.dll +0 -0
- chython/files/libinchi/wrapper.py +554 -0
- chython/files/mdl/__init__.py +25 -0
- chython/files/mdl/emol.py +251 -0
- chython/files/mdl/erxn.py +67 -0
- chython/files/mdl/mol.py +186 -0
- chython/files/mdl/read.py +223 -0
- chython/files/mdl/rxn.py +67 -0
- chython/files/mdl/stereo.py +63 -0
- chython/files/mdl/write.py +201 -0
- chython/files/opsin.py +35 -0
- chython/files/test/__init__.py +0 -0
- chython/files/test/test_inchi.py +27 -0
- chython/files/test/test_mrv.py +47 -0
- chython/files/test/test_sdf_rdf.py +64 -0
- chython/files/test/test_xyz.py +20 -0
- chython/files/xyz.py +78 -0
- chython/periodictable/__init__.py +69 -0
- chython/periodictable/base/__init__.py +25 -0
- chython/periodictable/base/dynamic.py +149 -0
- chython/periodictable/base/element.py +451 -0
- chython/periodictable/base/groups.py +90 -0
- chython/periodictable/base/periods.py +46 -0
- chython/periodictable/base/query.py +473 -0
- chython/periodictable/base/vector.py +119 -0
- chython/periodictable/groupI.py +252 -0
- chython/periodictable/groupII.py +223 -0
- chython/periodictable/groupIII.py +1154 -0
- chython/periodictable/groupIV.py +210 -0
- chython/periodictable/groupIX.py +196 -0
- chython/periodictable/groupV.py +192 -0
- chython/periodictable/groupVI.py +183 -0
- chython/periodictable/groupVII.py +162 -0
- chython/periodictable/groupVIII.py +161 -0
- chython/periodictable/groupX.py +166 -0
- chython/periodictable/groupXI.py +164 -0
- chython/periodictable/groupXII.py +158 -0
- chython/periodictable/groupXIII.py +239 -0
- chython/periodictable/groupXIV.py +262 -0
- chython/periodictable/groupXV.py +276 -0
- chython/periodictable/groupXVI.py +457 -0
- chython/periodictable/groupXVII.py +324 -0
- chython/periodictable/groupXVIII.py +260 -0
- chython/periodictable/test/__init__.py +0 -0
- chython/periodictable/test/test_element.py +100 -0
- chython/reactor/__init__.py +24 -0
- chython/reactor/base.py +222 -0
- chython/reactor/reactor.py +177 -0
- chython/reactor/test/__init__.py +18 -0
- chython/reactor/test/test_reactor.py +40 -0
- chython/reactor/test/test_transformer.py +78 -0
- chython/reactor/transformer.py +72 -0
- chython/utils/__init__.py +50 -0
- chython/utils/free_wilson.py +114 -0
- chython/utils/grid.py +130 -0
- chython/utils/retro.py +139 -0
- chython-2.19.dist-info/METADATA +105 -0
- chython-2.19.dist-info/RECORD +175 -0
- chython-2.19.dist-info/WHEEL +4 -0
- 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']
|