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,223 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
#
|
|
3
|
+
# Copyright 2021-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 abc import ABCMeta, abstractmethod
|
|
20
|
+
from base64 import urlsafe_b64encode
|
|
21
|
+
from itertools import islice
|
|
22
|
+
from os.path import abspath, join
|
|
23
|
+
from pathlib import Path
|
|
24
|
+
from pickle import load, UnpicklingError
|
|
25
|
+
from sys import platform
|
|
26
|
+
from tempfile import gettempdir
|
|
27
|
+
from typing import Union, Iterator, List, Dict, Optional
|
|
28
|
+
from ...containers import ReactionContainer, MoleculeContainer
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class MDLReadMeta(ABCMeta):
|
|
32
|
+
def __call__(cls, *args, **kwargs):
|
|
33
|
+
if kwargs.get('indexable'):
|
|
34
|
+
_cls = type(cls.__name__, (cls,), {'__len__': lambda x: len(x._shifts), '__module__': cls.__module__})
|
|
35
|
+
obj = object.__new__(_cls) # noqa
|
|
36
|
+
else:
|
|
37
|
+
obj = object.__new__(cls) # noqa
|
|
38
|
+
obj.__init__(*args, **kwargs)
|
|
39
|
+
return obj
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class MDLRead(metaclass=MDLReadMeta):
|
|
43
|
+
def __init__(self, file, buffer_size=1000, indexable=False, ignore=True, remap=False, ignore_bad_isotopes=False,
|
|
44
|
+
ignore_stereo=False, calc_cis_trans=False):
|
|
45
|
+
if isinstance(file, str):
|
|
46
|
+
self._file = open(file)
|
|
47
|
+
self._is_buffer = False
|
|
48
|
+
elif isinstance(file, Path):
|
|
49
|
+
self._file = file.open()
|
|
50
|
+
self._is_buffer = False
|
|
51
|
+
elif hasattr(file, '__iter__') and hasattr(file, 'read'):
|
|
52
|
+
self._file = file
|
|
53
|
+
self._is_buffer = True
|
|
54
|
+
else:
|
|
55
|
+
raise TypeError('invalid file. file-like object or path to file expected')
|
|
56
|
+
self._shifts = None
|
|
57
|
+
self._tell = 0
|
|
58
|
+
self._buffer_size = buffer_size
|
|
59
|
+
self._buffer = None
|
|
60
|
+
self._ignore = ignore
|
|
61
|
+
self._remap = remap
|
|
62
|
+
self._ignore_bad_isotopes = ignore_bad_isotopes
|
|
63
|
+
self._ignore_stereo = ignore_stereo
|
|
64
|
+
self._calc_cis_trans = calc_cis_trans
|
|
65
|
+
if indexable:
|
|
66
|
+
self._load_cache()
|
|
67
|
+
|
|
68
|
+
def read(self, amount: Optional[int] = None) -> List[Union[ReactionContainer, MoleculeContainer]]:
|
|
69
|
+
"""
|
|
70
|
+
Parse whole file
|
|
71
|
+
|
|
72
|
+
:param amount: number of records to read
|
|
73
|
+
"""
|
|
74
|
+
if amount:
|
|
75
|
+
return list(islice(iter(self), amount))
|
|
76
|
+
return list(iter(self))
|
|
77
|
+
|
|
78
|
+
@abstractmethod
|
|
79
|
+
def read_structure(self, *, current: bool = True):
|
|
80
|
+
"""
|
|
81
|
+
Read Reaction or Molecule container.
|
|
82
|
+
|
|
83
|
+
:param current: return current structure if already parsed, otherwise read next
|
|
84
|
+
"""
|
|
85
|
+
|
|
86
|
+
@abstractmethod
|
|
87
|
+
def read_metadata(self, *, current: bool = True) -> Dict[str, str]:
|
|
88
|
+
"""
|
|
89
|
+
Read metadata block
|
|
90
|
+
"""
|
|
91
|
+
|
|
92
|
+
def close(self, force: bool = False):
|
|
93
|
+
"""
|
|
94
|
+
Close opened file
|
|
95
|
+
|
|
96
|
+
:param force: force closing of externally opened file or buffer
|
|
97
|
+
"""
|
|
98
|
+
if not self._is_buffer or force:
|
|
99
|
+
self._file.close()
|
|
100
|
+
|
|
101
|
+
def tell(self):
|
|
102
|
+
"""
|
|
103
|
+
Number of records processed from the original file
|
|
104
|
+
"""
|
|
105
|
+
return self._tell
|
|
106
|
+
|
|
107
|
+
def seek(self, offset):
|
|
108
|
+
"""
|
|
109
|
+
Shift to a given record number
|
|
110
|
+
"""
|
|
111
|
+
if self._shifts:
|
|
112
|
+
if 0 <= offset < len(self._shifts):
|
|
113
|
+
self._tell = offset
|
|
114
|
+
self._buffer = None
|
|
115
|
+
self._file.seek(self._shifts[offset])
|
|
116
|
+
else:
|
|
117
|
+
raise IndexError('invalid offset')
|
|
118
|
+
else:
|
|
119
|
+
raise NotImplementedError('Indexable supported in unix-like o.s. and for files stored on disk')
|
|
120
|
+
|
|
121
|
+
@abstractmethod
|
|
122
|
+
def reset_index(self):
|
|
123
|
+
"""
|
|
124
|
+
Create (rewrite) indexation table. Implemented only for object that
|
|
125
|
+
is a real file (the path to the file is specified) because the external grep utility is used.
|
|
126
|
+
"""
|
|
127
|
+
|
|
128
|
+
def read_block(self, *, current: bool = True) -> str:
|
|
129
|
+
"""
|
|
130
|
+
Read full record block with metadata
|
|
131
|
+
"""
|
|
132
|
+
return ''.join(self._read_block(current=current))
|
|
133
|
+
|
|
134
|
+
@abstractmethod
|
|
135
|
+
def _read_block(self, *, current: bool = True) -> List[str]:
|
|
136
|
+
"""
|
|
137
|
+
Read full record block with metadata
|
|
138
|
+
"""
|
|
139
|
+
|
|
140
|
+
def _load_cache(self):
|
|
141
|
+
"""
|
|
142
|
+
Load existing cache or create new. Working only for UNIX-like systems and local files (not buffers).
|
|
143
|
+
"""
|
|
144
|
+
if platform == 'win32' or self._is_buffer:
|
|
145
|
+
return
|
|
146
|
+
try:
|
|
147
|
+
with open(self._cache_path, 'rb') as f:
|
|
148
|
+
self._shifts = load(f)
|
|
149
|
+
except FileNotFoundError: # cache not found
|
|
150
|
+
self.reset_index()
|
|
151
|
+
except IsADirectoryError as e:
|
|
152
|
+
raise IsADirectoryError(f'Please delete {self._cache_path} directory') from e
|
|
153
|
+
except (UnpicklingError, EOFError) as e: # invalid file. ask user to check it.
|
|
154
|
+
raise UnpicklingError(f'Invalid cache file {self._cache_path}. Please delete it') from e
|
|
155
|
+
|
|
156
|
+
@property
|
|
157
|
+
def _cache_path(self):
|
|
158
|
+
return abspath(join(gettempdir(), 'chython_' + urlsafe_b64encode(abspath(self._file.name).encode()).decode()))
|
|
159
|
+
|
|
160
|
+
def __enter__(self):
|
|
161
|
+
return self
|
|
162
|
+
|
|
163
|
+
def __exit__(self, _type, value, traceback):
|
|
164
|
+
self.close()
|
|
165
|
+
|
|
166
|
+
def __iter__(self) -> Iterator[Union[ReactionContainer, MoleculeContainer]]:
|
|
167
|
+
while True:
|
|
168
|
+
try:
|
|
169
|
+
yield self.read_structure(current=False)
|
|
170
|
+
except ValueError:
|
|
171
|
+
pass
|
|
172
|
+
except EOFError:
|
|
173
|
+
return
|
|
174
|
+
|
|
175
|
+
def __next__(self) -> Union[ReactionContainer, MoleculeContainer]:
|
|
176
|
+
return next(iter(self))
|
|
177
|
+
|
|
178
|
+
def __getitem__(self, item) -> Union[ReactionContainer, MoleculeContainer,
|
|
179
|
+
List[Union[ReactionContainer, MoleculeContainer]]]:
|
|
180
|
+
"""
|
|
181
|
+
Getting the item by index from the original file,
|
|
182
|
+
For slices records with errors skipped.
|
|
183
|
+
"""
|
|
184
|
+
if self._shifts:
|
|
185
|
+
_len = len(self._shifts)
|
|
186
|
+
if isinstance(item, int):
|
|
187
|
+
if item >= _len or item < -_len:
|
|
188
|
+
raise IndexError('List index out of range')
|
|
189
|
+
if item < 0:
|
|
190
|
+
item += _len
|
|
191
|
+
self.seek(item)
|
|
192
|
+
return self.read_structure()
|
|
193
|
+
elif isinstance(item, slice):
|
|
194
|
+
start, stop, step = item.indices(_len)
|
|
195
|
+
if start == stop:
|
|
196
|
+
return []
|
|
197
|
+
if step == 1:
|
|
198
|
+
self.seek(start)
|
|
199
|
+
records = []
|
|
200
|
+
for _ in range(start, stop):
|
|
201
|
+
try:
|
|
202
|
+
records.append(self.read_structure(current=False))
|
|
203
|
+
except EOFError:
|
|
204
|
+
break
|
|
205
|
+
except ValueError:
|
|
206
|
+
pass
|
|
207
|
+
else:
|
|
208
|
+
records = []
|
|
209
|
+
for index in range(start, stop, step):
|
|
210
|
+
self.seek(index)
|
|
211
|
+
try:
|
|
212
|
+
records.append(self.read_structure())
|
|
213
|
+
except EOFError:
|
|
214
|
+
break
|
|
215
|
+
except ValueError:
|
|
216
|
+
pass
|
|
217
|
+
return records
|
|
218
|
+
else:
|
|
219
|
+
raise TypeError('Indices must be integers or slices')
|
|
220
|
+
raise NotImplementedError('Indexable supported in unix-like o.s. and for files stored on disk')
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
__all__ = ['MDLRead']
|
chython/files/mdl/rxn.py
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
#
|
|
3
|
+
# Copyright 2020-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 .mol import parse_mol_v2000
|
|
20
|
+
from ...exceptions import EmptyMolecule, EmptyReaction, InvalidV2000
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def parse_rxn_v2000(data, *, ignore=True):
|
|
24
|
+
line = data[4]
|
|
25
|
+
reactants_count = int(line[:3])
|
|
26
|
+
products_count = int(line[3:6]) + reactants_count
|
|
27
|
+
reagents_count = int(line[6:].rstrip() or 0) + products_count
|
|
28
|
+
|
|
29
|
+
if not reagents_count:
|
|
30
|
+
raise EmptyReaction
|
|
31
|
+
|
|
32
|
+
title = data[1].strip() or None
|
|
33
|
+
log = []
|
|
34
|
+
molecules = []
|
|
35
|
+
|
|
36
|
+
start = -1
|
|
37
|
+
for n in range(0, reagents_count):
|
|
38
|
+
try:
|
|
39
|
+
start = next(n for n, x in enumerate(data[start + 6:], start + 7) if x.startswith('$MOL'))
|
|
40
|
+
except StopIteration:
|
|
41
|
+
raise InvalidV2000
|
|
42
|
+
|
|
43
|
+
try:
|
|
44
|
+
molecules.append(parse_mol_v2000(data[start:]))
|
|
45
|
+
except ValueError as e:
|
|
46
|
+
if isinstance(e, EmptyMolecule):
|
|
47
|
+
log.append(f'ignored empty molecule {n}')
|
|
48
|
+
elif ignore:
|
|
49
|
+
log.append(f'ignored molecule {n} with {e}')
|
|
50
|
+
else:
|
|
51
|
+
raise
|
|
52
|
+
|
|
53
|
+
if (lm := len(molecules)) < reactants_count:
|
|
54
|
+
reactants_count -= 1
|
|
55
|
+
products_count -= 1
|
|
56
|
+
reagents_count -= 1
|
|
57
|
+
elif lm < products_count:
|
|
58
|
+
products_count -= 1
|
|
59
|
+
reagents_count -= 1
|
|
60
|
+
else:
|
|
61
|
+
reagents_count -= 1
|
|
62
|
+
|
|
63
|
+
return {'reactants': molecules[:reactants_count], 'products': molecules[reactants_count:products_count],
|
|
64
|
+
'reagents': molecules[products_count:], 'title': title, 'log': log}
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
__all__ = ['parse_rxn_v2000']
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
#
|
|
3
|
+
# Copyright 2020-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 ...exceptions import NotChiral, IsChiral, ValenceError
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def postprocess_molecule(molecule, data, *, ignore_stereo=False, calc_cis_trans=False):
|
|
23
|
+
if ignore_stereo:
|
|
24
|
+
return
|
|
25
|
+
mapping = data['mapping']
|
|
26
|
+
log = []
|
|
27
|
+
|
|
28
|
+
if calc_cis_trans:
|
|
29
|
+
molecule.calculate_cis_trans_from_2d(clean_cache=False)
|
|
30
|
+
|
|
31
|
+
stereo = [(mapping[n], mapping[m], s) for n, m, s in data['stereo']]
|
|
32
|
+
while stereo:
|
|
33
|
+
fail_stereo = []
|
|
34
|
+
old_stereo = len(stereo)
|
|
35
|
+
for n, m, s in stereo:
|
|
36
|
+
try:
|
|
37
|
+
molecule.add_wedge(n, m, s, clean_cache=False)
|
|
38
|
+
except NotChiral:
|
|
39
|
+
fail_stereo.append((n, m, s))
|
|
40
|
+
except IsChiral:
|
|
41
|
+
pass
|
|
42
|
+
except ValenceError:
|
|
43
|
+
log.append('structure has errors, stereo data skipped')
|
|
44
|
+
molecule.flush_cache()
|
|
45
|
+
break
|
|
46
|
+
else:
|
|
47
|
+
stereo = fail_stereo
|
|
48
|
+
if len(stereo) == old_stereo:
|
|
49
|
+
break
|
|
50
|
+
molecule.flush_stereo_cache()
|
|
51
|
+
if calc_cis_trans:
|
|
52
|
+
molecule.calculate_cis_trans_from_2d(clean_cache=False)
|
|
53
|
+
continue
|
|
54
|
+
break
|
|
55
|
+
|
|
56
|
+
if log:
|
|
57
|
+
if 'chython_parsing_log' not in molecule.meta:
|
|
58
|
+
molecule.meta['chython_parsing_log'] = log
|
|
59
|
+
else:
|
|
60
|
+
molecule.meta['chython_parsing_log'].extend(log)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
__all__ = ['postprocess_molecule']
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
#
|
|
3
|
+
# Copyright 2021-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 collections import defaultdict
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
from ...containers import MoleculeContainer
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
charge_map = {-4: ' 0', -3: ' 7', -2: ' 6', -1: ' 5', 0: ' 0', 1: ' 3', 2: ' 2', 3: ' 1', 4: ' 0'}
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class IO:
|
|
28
|
+
def __init__(self, file, *, mapping: bool = True, append: bool = False):
|
|
29
|
+
"""
|
|
30
|
+
:param mapping: write atom mapping.
|
|
31
|
+
:param append: open file path in append mode.
|
|
32
|
+
"""
|
|
33
|
+
self._mapping = mapping
|
|
34
|
+
|
|
35
|
+
if isinstance(file, str):
|
|
36
|
+
self._file = open(file, 'a' if append else 'w')
|
|
37
|
+
self._is_buffer = False
|
|
38
|
+
elif isinstance(file, Path):
|
|
39
|
+
self._file = file.open('a' if append else 'w')
|
|
40
|
+
self._is_buffer = False
|
|
41
|
+
elif hasattr(file, 'write'):
|
|
42
|
+
self._file = file
|
|
43
|
+
self._is_buffer = True
|
|
44
|
+
else:
|
|
45
|
+
raise TypeError('invalid file. file-like object or path to file expected')
|
|
46
|
+
|
|
47
|
+
def close(self, force=False):
|
|
48
|
+
"""
|
|
49
|
+
Close opened file
|
|
50
|
+
|
|
51
|
+
:param force: force closing of externally opened file or buffer
|
|
52
|
+
"""
|
|
53
|
+
self.write = self.__write_closed
|
|
54
|
+
|
|
55
|
+
if not self._is_buffer or force:
|
|
56
|
+
self._file.close()
|
|
57
|
+
|
|
58
|
+
def __enter__(self):
|
|
59
|
+
return self
|
|
60
|
+
|
|
61
|
+
def __exit__(self, _type, value, traceback):
|
|
62
|
+
self.close()
|
|
63
|
+
|
|
64
|
+
@staticmethod
|
|
65
|
+
def __write_closed(_):
|
|
66
|
+
raise ValueError('I/O operation on closed writer')
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class EMOLWrite(IO):
|
|
70
|
+
def __init__(self, file, *, mapping: bool = True, append: bool = False, absolute: bool = False):
|
|
71
|
+
"""
|
|
72
|
+
:param mapping: write atom mapping.
|
|
73
|
+
:param append: open file path in append mode.
|
|
74
|
+
:param absolute: explicitly write MDLV30/STEABS collection for stereocenters without extended stereo groups.
|
|
75
|
+
By default chython treats any set chirality flag as absolute if not in an AND/OR group,
|
|
76
|
+
but some tools require the ABS collection to be explicitly present in the file.
|
|
77
|
+
"""
|
|
78
|
+
self._absolute = absolute
|
|
79
|
+
super().__init__(file, mapping=mapping, append=append)
|
|
80
|
+
|
|
81
|
+
def _write_molecule(self, g, write3d=None):
|
|
82
|
+
if not isinstance(g, MoleculeContainer):
|
|
83
|
+
raise TypeError('MoleculeContainer expected')
|
|
84
|
+
|
|
85
|
+
if write3d is not None:
|
|
86
|
+
xyz = g._conformers[write3d]
|
|
87
|
+
else:
|
|
88
|
+
z = 0
|
|
89
|
+
|
|
90
|
+
bonds = g._bonds
|
|
91
|
+
|
|
92
|
+
file = self._file
|
|
93
|
+
file.write(f'M V30 BEGIN CTAB\nM V30 COUNTS {g.atoms_count} {g.bonds_count} 0 0 0\nM V30 BEGIN ATOM\n')
|
|
94
|
+
|
|
95
|
+
for n, (m, a) in enumerate(g.atoms(), start=1):
|
|
96
|
+
if write3d is not None:
|
|
97
|
+
x, y, z = xyz[m]
|
|
98
|
+
z = f'{z:.4f}'
|
|
99
|
+
else:
|
|
100
|
+
x, y = a.x, a.y
|
|
101
|
+
|
|
102
|
+
c = f' CHG={a.charge}' if a.charge else ''
|
|
103
|
+
r = ' RAD=2' if a.is_radical else ''
|
|
104
|
+
i = f' MASS={a.isotope}' if a.isotope else ''
|
|
105
|
+
|
|
106
|
+
if not self._mapping:
|
|
107
|
+
m = 0
|
|
108
|
+
file.write(f'M V30 {n} {a.atomic_symbol} {x:.4f} {y:.4f} {z} {m}{c}{r}{i}\n')
|
|
109
|
+
|
|
110
|
+
file.write('M V30 END ATOM\nM V30 BEGIN BOND\n')
|
|
111
|
+
|
|
112
|
+
mapping = {m: n for n, m in enumerate(g, start=1)}
|
|
113
|
+
wedge = defaultdict(set)
|
|
114
|
+
i = 0 # trick for empty wedge_map
|
|
115
|
+
for i, (n, m, s) in enumerate(g._wedge_map, start=1):
|
|
116
|
+
file.write(f'M V30 {i} {bonds[n][m].order} {mapping[n]} {mapping[m]} CFG={s == 1 and "1" or "3"}\n')
|
|
117
|
+
wedge[n].add(m)
|
|
118
|
+
wedge[m].add(n)
|
|
119
|
+
|
|
120
|
+
for n, m, b in g.bonds():
|
|
121
|
+
if m not in wedge[n]:
|
|
122
|
+
i += 1
|
|
123
|
+
file.write(f'M V30 {i} {b.order} {mapping[n]} {mapping[m]}\n')
|
|
124
|
+
file.write('M V30 END BOND\n')
|
|
125
|
+
|
|
126
|
+
# enhanced stereo collection
|
|
127
|
+
rac = defaultdict(list) # AND groups: positive extended_stereo
|
|
128
|
+
rel = defaultdict(list) # OR groups: negative extended_stereo
|
|
129
|
+
ast = [] # absolute stereo
|
|
130
|
+
for m, a in g.atoms():
|
|
131
|
+
if (es := a.extended_stereo) is not None:
|
|
132
|
+
if es > 0:
|
|
133
|
+
rac[es].append(mapping[m])
|
|
134
|
+
elif es < 0:
|
|
135
|
+
rel[-es].append(mapping[m])
|
|
136
|
+
elif a.stereo is not None:
|
|
137
|
+
ast.append(mapping[m])
|
|
138
|
+
if rac or rel or (self._absolute and ast):
|
|
139
|
+
file.write('M V30 BEGIN COLLECTION\n')
|
|
140
|
+
if self._absolute and ast:
|
|
141
|
+
file.write(f'M V30 MDLV30/STEABS ATOMS=({len(ast)} {" ".join(str(x) for x in ast)})\n')
|
|
142
|
+
for gid in sorted(rac):
|
|
143
|
+
al = rac[gid]
|
|
144
|
+
file.write(f'M V30 MDLV30/STERAC{gid} ATOMS=({len(al)} {" ".join(str(x) for x in al)})\n')
|
|
145
|
+
for gid in sorted(rel):
|
|
146
|
+
al = rel[gid]
|
|
147
|
+
file.write(f'M V30 MDLV30/STEREL{gid} ATOMS=({len(al)} {" ".join(str(x) for x in al)})\n')
|
|
148
|
+
file.write('M V30 END COLLECTION\n')
|
|
149
|
+
|
|
150
|
+
file.write('M V30 END CTAB\n')
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
class MOLWrite(IO):
|
|
154
|
+
def _write_molecule(self, g, write3d=None):
|
|
155
|
+
if not isinstance(g, MoleculeContainer):
|
|
156
|
+
raise TypeError('MoleculeContainer expected')
|
|
157
|
+
elif max(g) > 999:
|
|
158
|
+
raise ValueError('MOL file support only small molecules')
|
|
159
|
+
|
|
160
|
+
if write3d is not None:
|
|
161
|
+
xyz = g._conformers[write3d]
|
|
162
|
+
else:
|
|
163
|
+
z = 0.
|
|
164
|
+
|
|
165
|
+
bonds = g._bonds
|
|
166
|
+
|
|
167
|
+
file = self._file
|
|
168
|
+
file.write(f'{g.name}\n\n\n{g.atoms_count:3d}{g.bonds_count:3d} 0 0 0 0 999 V2000\n')
|
|
169
|
+
|
|
170
|
+
for n, (m, a) in enumerate(g.atoms(), start=1):
|
|
171
|
+
if write3d is not None:
|
|
172
|
+
x, y, z = xyz[m]
|
|
173
|
+
else:
|
|
174
|
+
x, y = a.x, a.y
|
|
175
|
+
|
|
176
|
+
c = charge_map[a.charge]
|
|
177
|
+
if not self._mapping:
|
|
178
|
+
m = 0
|
|
179
|
+
file.write(f'{x:10.4f}{y:10.4f}{z:10.4f} {a.atomic_symbol:3s} 0{c} 0 0 0 0 0 0 0{m:3d} 0 0\n')
|
|
180
|
+
|
|
181
|
+
atoms = {m: n for n, m in enumerate(g, start=1)}
|
|
182
|
+
wedge = defaultdict(set)
|
|
183
|
+
for n, m, s in g._wedge_map:
|
|
184
|
+
file.write(f'{atoms[n]:3d}{atoms[m]:3d} {bonds[n][m].order} {s == 1 and "1" or "6"} 0 0 0\n')
|
|
185
|
+
wedge[n].add(m)
|
|
186
|
+
wedge[m].add(n)
|
|
187
|
+
for n, m, b in g.bonds():
|
|
188
|
+
if m not in wedge[n]:
|
|
189
|
+
file.write(f'{atoms[n]:3d}{atoms[m]:3d} {b.order} 0 0 0 0\n')
|
|
190
|
+
|
|
191
|
+
for n, (_, a) in enumerate(g.atoms(), start=1):
|
|
192
|
+
if a.isotope:
|
|
193
|
+
file.write(f'M ISO 1 {n:3d} {a.isotope:3d}\n')
|
|
194
|
+
if a.is_radical:
|
|
195
|
+
file.write(f'M RAD 1 {n:3d} 2\n') # invalid for carbenes
|
|
196
|
+
if a.charge in (-4, 4):
|
|
197
|
+
file.write(f'M CHG 1 {n:3d} {a.charge:3d}\n')
|
|
198
|
+
file.write('M END\n')
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
__all__ = ['MOLWrite', 'EMOLWrite']
|
chython/files/opsin.py
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
#
|
|
3
|
+
# Copyright 2025, 2026 Ramil Nugmanov <nougmanoff@protonmail.com>
|
|
4
|
+
# This file is part of chython.
|
|
5
|
+
#
|
|
6
|
+
# chython is free software; you can redistribute it and/or modify
|
|
7
|
+
# it under the terms of the GNU Lesser General Public License as published by
|
|
8
|
+
# the Free Software Foundation; either version 3 of the License, or
|
|
9
|
+
# (at your option) any later version.
|
|
10
|
+
#
|
|
11
|
+
# This program is distributed in the hope that it will be useful,
|
|
12
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
13
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
14
|
+
# GNU Lesser General Public License for more details.
|
|
15
|
+
#
|
|
16
|
+
# You should have received a copy of the GNU Lesser General Public License
|
|
17
|
+
# along with this program; if not, see <https://www.gnu.org/licenses/>.
|
|
18
|
+
#
|
|
19
|
+
from .daylight import smiles
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def opsin(string):
|
|
23
|
+
"""Parse IUPAC name into MoleculeContainer."""
|
|
24
|
+
from .._java import get_opsin
|
|
25
|
+
|
|
26
|
+
result = get_opsin().parseChemicalName(string)
|
|
27
|
+
if str(result.getStatus()) == 'FAILURE':
|
|
28
|
+
raise ValueError(f'Failed to convert `{string}`: {result.getMessage()}')
|
|
29
|
+
return smiles(str(result.getSmiles()))
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
iupac = opsin
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
__all__ = ['opsin', 'iupac']
|
|
File without changes
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
import pytest
|
|
3
|
+
|
|
4
|
+
try:
|
|
5
|
+
from chython import inchi
|
|
6
|
+
HAS_INCHI = True
|
|
7
|
+
except ImportError:
|
|
8
|
+
HAS_INCHI = False
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@pytest.mark.skipif(not HAS_INCHI, reason='libinchi not available')
|
|
12
|
+
def test_inchi_parse_ethanol():
|
|
13
|
+
mol = inchi('InChI=1S/C2H6O/c1-2-3/h3H,2H2,1H3')
|
|
14
|
+
assert len(mol) == 3
|
|
15
|
+
assert 'O' in {a.atomic_symbol for _, a in mol.atoms()}
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@pytest.mark.skipif(not HAS_INCHI, reason='libinchi not available')
|
|
19
|
+
def test_inchi_parse_benzene():
|
|
20
|
+
mol = inchi('InChI=1S/C6H6/c1-2-4-6-5-3-1/h1-6H')
|
|
21
|
+
assert len(mol) == 6
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@pytest.mark.skipif(not HAS_INCHI, reason='libinchi not available')
|
|
25
|
+
def test_inchi_parse_charged():
|
|
26
|
+
mol = inchi('InChI=1S/Na.H2O/h;1H2/q+1;/p-1')
|
|
27
|
+
assert mol is not None
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
from io import BytesIO, StringIO
|
|
3
|
+
from chython import smiles
|
|
4
|
+
from chython.files import MRVRead, MRVWrite
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def test_mrv_read_molecule():
|
|
8
|
+
with MRVRead('test/implicit.mrv') as f:
|
|
9
|
+
mol = next(f)
|
|
10
|
+
assert mol is not None
|
|
11
|
+
assert len(mol) > 0
|
|
12
|
+
assert 'N' in {a.atomic_symbol for _, a in mol.atoms()}
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def test_mrv_read_all():
|
|
16
|
+
with MRVRead('test/implicit.mrv') as f:
|
|
17
|
+
mols = f.read()
|
|
18
|
+
assert len(mols) == 1
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def test_mrv_write_read_roundtrip():
|
|
22
|
+
mol = smiles('c1ccccc1')
|
|
23
|
+
mol.clean2d()
|
|
24
|
+
|
|
25
|
+
buf = StringIO()
|
|
26
|
+
with MRVWrite(buf) as w:
|
|
27
|
+
w.write(mol)
|
|
28
|
+
|
|
29
|
+
xml_bytes = buf.getvalue().encode()
|
|
30
|
+
with MRVRead(BytesIO(xml_bytes)) as r:
|
|
31
|
+
mol2 = next(r)
|
|
32
|
+
|
|
33
|
+
assert str(mol) == str(mol2)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def test_mrv_write_reaction():
|
|
37
|
+
rxn = smiles('[CH3:1][OH:2]>>[CH3:1][NH2:3]')
|
|
38
|
+
for m in rxn.molecules():
|
|
39
|
+
m.clean2d()
|
|
40
|
+
|
|
41
|
+
buf = StringIO()
|
|
42
|
+
with MRVWrite(buf) as w:
|
|
43
|
+
w.write(rxn)
|
|
44
|
+
|
|
45
|
+
assert '<reaction>' in buf.getvalue()
|
|
46
|
+
assert 'reactantList' in buf.getvalue()
|
|
47
|
+
assert 'productList' in buf.getvalue()
|