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
chython/files/SDFrw.py
ADDED
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
#
|
|
3
|
+
# Copyright 2014-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 io import BytesIO
|
|
21
|
+
from pickle import dump
|
|
22
|
+
from re import compile, match
|
|
23
|
+
from subprocess import check_output
|
|
24
|
+
from sys import platform
|
|
25
|
+
from typing import Optional, List
|
|
26
|
+
from .mdl import MDLRead, MOLWrite, EMOLWrite, parse_mol_v2000, parse_mol_v3000, postprocess_molecule
|
|
27
|
+
from ._convert import create_molecule
|
|
28
|
+
from ._mapping import postprocess_parsed_molecule
|
|
29
|
+
from ..containers import MoleculeContainer
|
|
30
|
+
from ..exceptions import BufferOverflow, InvalidMolBlock
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
meta_pattern = compile(r'^>([^<]+)<([^>]+)>([^><]*)$')
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class SDFRead(MDLRead):
|
|
37
|
+
"""
|
|
38
|
+
MDL SDF files reader. works similar to opened file object. support `with` context manager.
|
|
39
|
+
on initialization accept opened in text mode file, string path to file,
|
|
40
|
+
pathlib.Path object or another buffered reader object
|
|
41
|
+
"""
|
|
42
|
+
escape_map = {'>': '>', '<': '<'}
|
|
43
|
+
molecule_cls = MoleculeContainer
|
|
44
|
+
|
|
45
|
+
def __init__(self, file, *, buffer_size=10000, indexable: bool = False, ignore: bool = True, remap: bool = False,
|
|
46
|
+
calc_cis_trans: bool = False, ignore_stereo: bool = False, ignore_bad_isotopes: bool = False):
|
|
47
|
+
"""
|
|
48
|
+
:param buffer_size: readahead size. increase if you have big molecules or metadata records.
|
|
49
|
+
:param indexable: if True: supported methods seek, tell, object size and subscription, it only works when
|
|
50
|
+
dealing with a real file (the path to the file is specified) because the external grep utility is used,
|
|
51
|
+
supporting in unix-like OS the object behaves like a normal open file.
|
|
52
|
+
|
|
53
|
+
if False: works like generator converting a record into MoleculeContainer and returning each object in
|
|
54
|
+
order, records with errors are skipped
|
|
55
|
+
:param ignore: Skip some checks of data or try to fix some errors.
|
|
56
|
+
:param remap: Remap atom numbers started from one.
|
|
57
|
+
:param calc_cis_trans: Calculate cis/trans marks from 2d coordinates.
|
|
58
|
+
:param ignore_stereo: Ignore stereo data.
|
|
59
|
+
:param ignore_bad_isotopes: reset invalid isotope mark to non-isotopic.
|
|
60
|
+
"""
|
|
61
|
+
super().__init__(file, indexable=indexable, ignore=ignore, remap=remap, ignore_bad_isotopes=ignore_bad_isotopes,
|
|
62
|
+
ignore_stereo=ignore_stereo, calc_cis_trans=calc_cis_trans, buffer_size=buffer_size)
|
|
63
|
+
self.__m_end = None
|
|
64
|
+
|
|
65
|
+
def read_structure(self, *, current=True) -> MoleculeContainer:
|
|
66
|
+
data = self._read_mol(current=current)
|
|
67
|
+
if data[4].startswith('M V30 BEGIN CTAB'):
|
|
68
|
+
tmp = parse_mol_v3000(data)
|
|
69
|
+
else:
|
|
70
|
+
tmp = parse_mol_v2000(data)
|
|
71
|
+
|
|
72
|
+
postprocess_parsed_molecule(tmp, remap=self._remap, ignore=self._ignore)
|
|
73
|
+
mol = create_molecule(tmp, ignore_bad_isotopes=self._ignore_bad_isotopes, _cls=self.molecule_cls)
|
|
74
|
+
if not self._ignore_stereo:
|
|
75
|
+
postprocess_molecule(mol, tmp, calc_cis_trans=self._calc_cis_trans)
|
|
76
|
+
meta = self.read_metadata()
|
|
77
|
+
if meta:
|
|
78
|
+
mol.meta.update(meta)
|
|
79
|
+
return mol
|
|
80
|
+
|
|
81
|
+
def read_metadata(self, *, current=True):
|
|
82
|
+
mkey = None
|
|
83
|
+
meta = defaultdict(list)
|
|
84
|
+
for line in self._read_metadata(current=current):
|
|
85
|
+
if matched := match(meta_pattern, line):
|
|
86
|
+
mkey = ' '.join(y for x in matched.groups() if (y := x.strip()))
|
|
87
|
+
for e, s in self.escape_map.items():
|
|
88
|
+
mkey = mkey.replace(e, s)
|
|
89
|
+
elif mkey:
|
|
90
|
+
if line := line.strip():
|
|
91
|
+
meta[mkey].append(line)
|
|
92
|
+
else:
|
|
93
|
+
meta['chython_unparsed_metadata'].append(line.strip())
|
|
94
|
+
return {k: '\n'.join(v) for k, v in meta.items()}
|
|
95
|
+
|
|
96
|
+
def read_mol(self, *, current: bool = True) -> str:
|
|
97
|
+
"""
|
|
98
|
+
Read MOL block without metadata
|
|
99
|
+
"""
|
|
100
|
+
return ''.join(self._read_mol(current=current))
|
|
101
|
+
|
|
102
|
+
def seek(self, offset):
|
|
103
|
+
super().seek(offset)
|
|
104
|
+
self.__m_end = None
|
|
105
|
+
|
|
106
|
+
def reset_index(self):
|
|
107
|
+
if platform != 'win32' and not self._is_buffer:
|
|
108
|
+
shifts = [0]
|
|
109
|
+
for x in BytesIO(check_output(['grep', '-bE', r'\$\$\$\$', self._file.name])):
|
|
110
|
+
pos, line = x.split(b':', 1)
|
|
111
|
+
shifts.append(int(pos) + len(line))
|
|
112
|
+
shifts.pop(-1)
|
|
113
|
+
with open(self._cache_path, 'wb') as f:
|
|
114
|
+
dump(shifts, f)
|
|
115
|
+
self._shifts = shifts
|
|
116
|
+
else:
|
|
117
|
+
raise NotImplementedError('Indexable supported in unix-like o.s. and for files stored on disk')
|
|
118
|
+
|
|
119
|
+
def _read_block(self, *, current=True) -> List[str]:
|
|
120
|
+
if current and self._buffer:
|
|
121
|
+
return self._buffer
|
|
122
|
+
self.__m_end = m_end = None
|
|
123
|
+
self._buffer = buffer = []
|
|
124
|
+
buffer_size = self._buffer_size
|
|
125
|
+
|
|
126
|
+
for n, line in enumerate(self._file):
|
|
127
|
+
if line.startswith('$$$$'):
|
|
128
|
+
break
|
|
129
|
+
elif n == buffer_size:
|
|
130
|
+
raise BufferOverflow
|
|
131
|
+
buffer.append(line)
|
|
132
|
+
if not m_end and line.startswith('M END'):
|
|
133
|
+
self.__m_end = m_end = len(buffer)
|
|
134
|
+
if buffer:
|
|
135
|
+
self._tell += 1
|
|
136
|
+
else:
|
|
137
|
+
raise EOFError
|
|
138
|
+
return buffer
|
|
139
|
+
|
|
140
|
+
def _read_mol(self, *, current: bool = True) -> List[str]:
|
|
141
|
+
data = self._read_block(current=current)
|
|
142
|
+
if not self.__m_end:
|
|
143
|
+
raise InvalidMolBlock
|
|
144
|
+
return data[:self.__m_end]
|
|
145
|
+
|
|
146
|
+
def _read_metadata(self, *, current: bool = True):
|
|
147
|
+
data = self._read_block(current=current)
|
|
148
|
+
if not self.__m_end:
|
|
149
|
+
raise InvalidMolBlock
|
|
150
|
+
return data[self.__m_end:]
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
class SDFWrite(MOLWrite):
|
|
154
|
+
"""
|
|
155
|
+
MDL SDF files writer. works similar to opened for writing file object. support `with` context manager.
|
|
156
|
+
on initialization accept opened for writing in text mode file, string path to file,
|
|
157
|
+
pathlib.Path object or another buffered writer object
|
|
158
|
+
"""
|
|
159
|
+
escape_map = {'>': '>', '<': '<'}
|
|
160
|
+
|
|
161
|
+
def write(self, data: MoleculeContainer, write3d: Optional[int] = None):
|
|
162
|
+
"""
|
|
163
|
+
write single molecule into file
|
|
164
|
+
|
|
165
|
+
:param write3d: write conformer coordinates with given index
|
|
166
|
+
"""
|
|
167
|
+
self._write_molecule(data, write3d=write3d)
|
|
168
|
+
|
|
169
|
+
file = self._file
|
|
170
|
+
for k, v in data.meta.items():
|
|
171
|
+
for e, s in self.escape_map.items():
|
|
172
|
+
k = k.replace(e, s)
|
|
173
|
+
file.write(f'> <{k}>\n{v}\n\n')
|
|
174
|
+
file.write('$$$$\n')
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
class ESDFWrite(EMOLWrite):
|
|
178
|
+
"""
|
|
179
|
+
MDL V3000 SDF files writer. works similar to opened for writing file object. support `with` context manager.
|
|
180
|
+
on initialization accept opened for writing in text mode file, string path to file,
|
|
181
|
+
pathlib.Path object or another buffered writer object
|
|
182
|
+
"""
|
|
183
|
+
escape_map = {'>': '>', '<': '<'}
|
|
184
|
+
|
|
185
|
+
def write(self, data: MoleculeContainer, write3d: Optional[int] = None):
|
|
186
|
+
"""
|
|
187
|
+
write single molecule into file
|
|
188
|
+
|
|
189
|
+
:param write3d: write conformer coordinates with given index
|
|
190
|
+
"""
|
|
191
|
+
file = self._file
|
|
192
|
+
file.write(f'{data.name}\n\n\n 0 0 0 0 0 999 V3000\n')
|
|
193
|
+
self._write_molecule(data, write3d)
|
|
194
|
+
file.write('M END\n')
|
|
195
|
+
|
|
196
|
+
for k, v in data.meta.items():
|
|
197
|
+
for e, s in self.escape_map.items():
|
|
198
|
+
k = k.replace(e, s)
|
|
199
|
+
file.write(f'> <{k}>\n{v}\n\n')
|
|
200
|
+
file.write('$$$$\n')
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def mdl_mol(data: str, /, *, ignore=True, calc_cis_trans=False, ignore_stereo=False, remap=False,
|
|
204
|
+
ignore_bad_isotopes=False, _cls=MoleculeContainer) -> MoleculeContainer:
|
|
205
|
+
"""
|
|
206
|
+
Parse string with mol file.
|
|
207
|
+
"""
|
|
208
|
+
data = data.splitlines()
|
|
209
|
+
if data[4].startswith('M V30 BEGIN CTAB'):
|
|
210
|
+
tmp = parse_mol_v3000(data)
|
|
211
|
+
else:
|
|
212
|
+
tmp = parse_mol_v2000(data)
|
|
213
|
+
|
|
214
|
+
postprocess_parsed_molecule(tmp, remap=remap, ignore=ignore)
|
|
215
|
+
mol = create_molecule(tmp, ignore_bad_isotopes=ignore_bad_isotopes, _cls=_cls)
|
|
216
|
+
if not ignore_stereo:
|
|
217
|
+
postprocess_molecule(mol, tmp, calc_cis_trans=calc_cis_trans)
|
|
218
|
+
return mol
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
__all__ = ['SDFRead', 'SDFWrite', 'ESDFWrite', 'mdl_mol']
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
#
|
|
3
|
+
# Copyright 2014-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 *
|
|
20
|
+
from .libinchi import *
|
|
21
|
+
from .MRVrw import *
|
|
22
|
+
from .opsin import *
|
|
23
|
+
from .PDBrw import *
|
|
24
|
+
from .RDFrw import *
|
|
25
|
+
from .SDFrw import *
|
|
26
|
+
from .xyz import *
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
__all__ = ['smiles', 'smarts', 'mdl_mol', 'mdl_rxn', 'xyz', 'xyz_file', 'inchi', 'opsin', 'iupac']
|
|
30
|
+
__all__.extend(x for x in locals() if x.endswith(('Read', 'Write')))
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
#
|
|
3
|
+
# Copyright 2023-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 ..containers import MoleculeContainer, ReactionContainer
|
|
20
|
+
from ..containers.bonds import Bond
|
|
21
|
+
from ..exceptions import AtomNotFound
|
|
22
|
+
from ..periodictable import Element
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
# atomic number constants
|
|
26
|
+
B = 5
|
|
27
|
+
C = 6
|
|
28
|
+
N = 7
|
|
29
|
+
P = 15
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def create_molecule(data, *, ignore_bad_isotopes=False, skip_calc_implicit=False,
|
|
33
|
+
keep_implicit=False, keep_radicals=True, ignore_aromatic_radicals=True, ignore=True,
|
|
34
|
+
ignore_carbon_radicals=False, _cls=MoleculeContainer):
|
|
35
|
+
g = _cls()
|
|
36
|
+
g._name = data.get('title')
|
|
37
|
+
atoms = g._atoms
|
|
38
|
+
bonds = g._bonds
|
|
39
|
+
mapping = data['mapping']
|
|
40
|
+
|
|
41
|
+
if any(a.get('z') for a in data['atoms']):
|
|
42
|
+
# store conformer
|
|
43
|
+
g._conformers = [{n: (a['x'], a['y'], a['z']) for n, a in zip(mapping, data['atoms'])}]
|
|
44
|
+
|
|
45
|
+
for n, atom in zip(mapping, data['atoms']):
|
|
46
|
+
e = Element.from_symbol(atom.pop('element'))
|
|
47
|
+
atom.pop('z', None) # clean up MDL
|
|
48
|
+
try:
|
|
49
|
+
atoms[n] = e(**atom)
|
|
50
|
+
except (ValueError, TypeError):
|
|
51
|
+
if not ignore_bad_isotopes:
|
|
52
|
+
raise
|
|
53
|
+
del atom['isotope'] # reset isotope mark on errors and try again.
|
|
54
|
+
atoms[n] = e(**atom)
|
|
55
|
+
bonds[n] = {}
|
|
56
|
+
|
|
57
|
+
for n, m, b in data['bonds']:
|
|
58
|
+
n, m = mapping[n], mapping[m]
|
|
59
|
+
if n == m:
|
|
60
|
+
raise ValueError('atom loops impossible')
|
|
61
|
+
if n not in bonds or m not in bonds:
|
|
62
|
+
raise AtomNotFound('atoms not found')
|
|
63
|
+
if n in bonds[m]:
|
|
64
|
+
raise ValueError('atoms already bonded')
|
|
65
|
+
bonds[n][m] = bonds[m][n] = Bond(b)
|
|
66
|
+
|
|
67
|
+
g.calc_labels() # set all labels except rings
|
|
68
|
+
|
|
69
|
+
if data.get('log'): # store log to the meta
|
|
70
|
+
if data.get('meta') is None:
|
|
71
|
+
data['meta'] = {}
|
|
72
|
+
data['meta']['chython_parsing_log'] = data['log']
|
|
73
|
+
g._meta = data.get('meta') or None
|
|
74
|
+
|
|
75
|
+
if skip_calc_implicit: # don't calc Hs. e.g. INCHI
|
|
76
|
+
return g
|
|
77
|
+
|
|
78
|
+
implicit_mismatch = {}
|
|
79
|
+
radicalized = []
|
|
80
|
+
# precalculate Hs
|
|
81
|
+
for n, a in atoms.items():
|
|
82
|
+
if a.implicit_hydrogens is None:
|
|
83
|
+
# let's try to calculate. in case of errors just keep as is. radicals in smiles should be in [brackets],
|
|
84
|
+
# thus has implicit Hs value
|
|
85
|
+
g.calc_implicit(n)
|
|
86
|
+
elif keep_implicit:
|
|
87
|
+
# keep given Hs count as is
|
|
88
|
+
continue
|
|
89
|
+
else: # recheck given Hs count
|
|
90
|
+
h = a.implicit_hydrogens # parsed Hs
|
|
91
|
+
g.calc_implicit(n) # recalculate
|
|
92
|
+
if a.implicit_hydrogens is None: # atom has invalid valence or aromatic ring.
|
|
93
|
+
if a.hybridization == 4:
|
|
94
|
+
# this is aromatic ring. just restore given H count.
|
|
95
|
+
a._implicit_hydrogens = h
|
|
96
|
+
# rare H0 case
|
|
97
|
+
if (not keep_radicals and not ignore_aromatic_radicals
|
|
98
|
+
and not h and not a.charge and not a.is_radical and a in (B, C, N, P)
|
|
99
|
+
and sum(b != 8 for b in bonds[n].values()) == 2):
|
|
100
|
+
# c[c]c - aromatic B,C,N,P radical
|
|
101
|
+
a._is_radical = True
|
|
102
|
+
radicalized.append(n)
|
|
103
|
+
elif not keep_radicals and not a.is_radical: # CXSMILES radical not set.
|
|
104
|
+
# SMILES doesn't code radicals. so, let's try to guess.
|
|
105
|
+
a._is_radical = True
|
|
106
|
+
if g.check_implicit(n, h): # radical form is valid
|
|
107
|
+
radicalized.append(n)
|
|
108
|
+
a._implicit_hydrogens = h
|
|
109
|
+
elif ignore: # radical state also has errors.
|
|
110
|
+
a._is_radical = False # reset radical state
|
|
111
|
+
implicit_mismatch[n] = h
|
|
112
|
+
if data.get('log') is None:
|
|
113
|
+
data['log'] = []
|
|
114
|
+
data['log'].append(f'implicit hydrogen count ({h}) mismatch with calculated on atom {n}')
|
|
115
|
+
else:
|
|
116
|
+
raise ValueError(f'implicit hydrogen count ({h}) mismatch with calculated on atom {n}')
|
|
117
|
+
elif h != a.implicit_hydrogens: # H count mismatch.
|
|
118
|
+
if a.hybridization == 4:
|
|
119
|
+
if (not keep_radicals
|
|
120
|
+
and not h and not a.charge and not a.is_radical and a in (B, C, N, P)
|
|
121
|
+
and sum(b != 8 for b in bonds[n].values()) == 2):
|
|
122
|
+
# c[c]c - aromatic B,C,N,P radical
|
|
123
|
+
a._implicit_hydrogens = 0
|
|
124
|
+
a._is_radical = True
|
|
125
|
+
radicalized.append(n)
|
|
126
|
+
elif ignore:
|
|
127
|
+
implicit_mismatch[n] = h
|
|
128
|
+
if data.get('log') is None:
|
|
129
|
+
data['log'] = []
|
|
130
|
+
data['log'].append(f'implicit hydrogen count ({h}) mismatch with calculated on atom {n}')
|
|
131
|
+
else:
|
|
132
|
+
raise ValueError(f'implicit hydrogen count ({h}) mismatch with calculated on atom {n}')
|
|
133
|
+
elif g.check_implicit(n, h): # set another possible implicit state. probably Al, P
|
|
134
|
+
a._implicit_hydrogens = h
|
|
135
|
+
elif not keep_radicals and not a.is_radical: # CXSMILES radical is not set. try radical form
|
|
136
|
+
a._is_radical = True
|
|
137
|
+
if g.check_implicit(n, h):
|
|
138
|
+
a._implicit_hydrogens = h
|
|
139
|
+
radicalized.append(n)
|
|
140
|
+
# radical state also has errors.
|
|
141
|
+
elif ignore:
|
|
142
|
+
a._is_radical = False # reset radical state
|
|
143
|
+
implicit_mismatch[n] = h
|
|
144
|
+
if data.get('log') is None:
|
|
145
|
+
data['log'] = []
|
|
146
|
+
data['log'].append(f'implicit hydrogen count ({h}) mismatch with calculated on atom {n}')
|
|
147
|
+
else:
|
|
148
|
+
raise ValueError(f'implicit hydrogen count ({h}) mismatch with calculated on atom {n}')
|
|
149
|
+
elif ignore: # just ignore it
|
|
150
|
+
implicit_mismatch[n] = h
|
|
151
|
+
if data.get('log') is None:
|
|
152
|
+
data['log'] = []
|
|
153
|
+
data['log'].append(f'implicit hydrogen count ({h}) mismatch with calculated on atom {n}')
|
|
154
|
+
else:
|
|
155
|
+
raise ValueError(f'implicit hydrogen count ({h}) mismatch with calculated on atom {n}')
|
|
156
|
+
|
|
157
|
+
if ignore_carbon_radicals:
|
|
158
|
+
for n in radicalized:
|
|
159
|
+
if (a := atoms[n]) == C:
|
|
160
|
+
a._is_radical = False
|
|
161
|
+
a._implicit_hydrogens += 1
|
|
162
|
+
if data.get('log') is None:
|
|
163
|
+
data['log'] = []
|
|
164
|
+
data['log'].append(f'carbon radical {n} replaced with implicit hydrogen')
|
|
165
|
+
elif radicalized:
|
|
166
|
+
g.meta['chython_radicalized_atoms'] = radicalized
|
|
167
|
+
if data.get('log') and 'chython_parsing_log' not in g.meta:
|
|
168
|
+
g.meta['chython_parsing_log'] = data['log']
|
|
169
|
+
if implicit_mismatch:
|
|
170
|
+
g.meta['chython_implicit_mismatch'] = implicit_mismatch
|
|
171
|
+
return g
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def create_reaction(data, *, ignore=True, skip_calc_implicit=False, ignore_bad_isotopes=False,
|
|
175
|
+
keep_implicit=False, keep_radicals=True, ignore_aromatic_radicals=True,
|
|
176
|
+
ignore_carbon_radicals=False, _r_cls=ReactionContainer, _m_cls=MoleculeContainer):
|
|
177
|
+
rc, pr, rg = [], [], []
|
|
178
|
+
for ms, pms, gr in ((rc, data['reactants'], 'reactant'),
|
|
179
|
+
(pr, data['products'], 'products'),
|
|
180
|
+
(rg, data['reagents'], 'reagent')):
|
|
181
|
+
tdl = []
|
|
182
|
+
for n, m in enumerate(pms):
|
|
183
|
+
try:
|
|
184
|
+
ms.append(create_molecule(m, skip_calc_implicit=skip_calc_implicit,
|
|
185
|
+
ignore_bad_isotopes=ignore_bad_isotopes, keep_implicit=keep_implicit,
|
|
186
|
+
keep_radicals=keep_radicals,
|
|
187
|
+
ignore_aromatic_radicals=ignore_aromatic_radicals, ignore=ignore,
|
|
188
|
+
ignore_carbon_radicals=ignore_carbon_radicals, _cls=_m_cls))
|
|
189
|
+
except ValueError as e:
|
|
190
|
+
if not ignore:
|
|
191
|
+
raise
|
|
192
|
+
if data.get('log') is None:
|
|
193
|
+
data['log'] = []
|
|
194
|
+
data['log'].append(f'ignored {gr} molecule {n} with {e}')
|
|
195
|
+
tdl.append(n)
|
|
196
|
+
if tdl: # ad-hoc for later postprocessing
|
|
197
|
+
for n in reversed(tdl):
|
|
198
|
+
del pms[n]
|
|
199
|
+
|
|
200
|
+
if data.get('log'): # store log to the meta
|
|
201
|
+
if data.get('meta') is None:
|
|
202
|
+
data['meta'] = {}
|
|
203
|
+
data['meta']['chython_parsing_log'] = data['log']
|
|
204
|
+
return _r_cls(rc, pr, rg, meta=data.get('meta') or None, name=data.get('title'))
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
__all__ = ['create_molecule']
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
#
|
|
3
|
+
# Copyright 2014-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 itertools import count
|
|
20
|
+
from ..exceptions import MappingError
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def postprocess_parsed_molecule(data, *, remap=False, ignore=True):
|
|
24
|
+
if remap:
|
|
25
|
+
remapped = list(range(1, len(data['atoms']) + 1))
|
|
26
|
+
else:
|
|
27
|
+
length = count(max(x.get('parsed_mapping') or 0 for x in data['atoms']) + 1)
|
|
28
|
+
remapped, used = [], set()
|
|
29
|
+
for n, atom in enumerate(data['atoms']):
|
|
30
|
+
m = atom.get('parsed_mapping')
|
|
31
|
+
if not m:
|
|
32
|
+
remapped.append(next(length))
|
|
33
|
+
elif m in used:
|
|
34
|
+
if not ignore:
|
|
35
|
+
raise MappingError('mapping in molecules should be unique')
|
|
36
|
+
remapped.append(next(length))
|
|
37
|
+
if data.get('log') is None:
|
|
38
|
+
data['log'] = []
|
|
39
|
+
data['log'].append(f'mapping in molecule changed from {m} to {remapped[n]}')
|
|
40
|
+
else:
|
|
41
|
+
remapped.append(m)
|
|
42
|
+
used.add(m)
|
|
43
|
+
data['mapping'] = remapped
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def postprocess_parsed_reaction(data, *, remap=False, ignore=True):
|
|
47
|
+
maps = {'reactants': [], 'products': [], 'reagents': []}
|
|
48
|
+
for i, tmp in maps.items():
|
|
49
|
+
for molecule in data[i]:
|
|
50
|
+
used = set()
|
|
51
|
+
for atom in molecule['atoms']:
|
|
52
|
+
m = atom.get('parsed_mapping')
|
|
53
|
+
if m:
|
|
54
|
+
if m in used:
|
|
55
|
+
if not ignore:
|
|
56
|
+
raise MappingError('mapping in molecules should be unique')
|
|
57
|
+
molecule['log'].append(f'non-unique mapping in molecule: {m}')
|
|
58
|
+
else:
|
|
59
|
+
used.add(m)
|
|
60
|
+
tmp.append(m)
|
|
61
|
+
else:
|
|
62
|
+
tmp.append(0)
|
|
63
|
+
|
|
64
|
+
length = count(max(max(maps['products'], default=0), max(maps['reactants'], default=0),
|
|
65
|
+
max(maps['reagents'], default=0)) + 1)
|
|
66
|
+
|
|
67
|
+
# map unmapped atoms.
|
|
68
|
+
for i, tmp in maps.items():
|
|
69
|
+
used = set()
|
|
70
|
+
maps[i] = _remap = []
|
|
71
|
+
for m in tmp:
|
|
72
|
+
if not m:
|
|
73
|
+
_remap.append(next(length))
|
|
74
|
+
elif m in used:
|
|
75
|
+
if not ignore:
|
|
76
|
+
raise MappingError('mapping in reagents or products or reactants should be unique')
|
|
77
|
+
# force remap non unique atoms in molecules.
|
|
78
|
+
_remap.append(next(length))
|
|
79
|
+
if data.get('log') is None:
|
|
80
|
+
data['log'] = []
|
|
81
|
+
data['log'].append(f'mapping in {i} changed from {m} to {_remap[-1]}')
|
|
82
|
+
else:
|
|
83
|
+
_remap.append(m)
|
|
84
|
+
used.add(m)
|
|
85
|
+
|
|
86
|
+
if maps['reagents']:
|
|
87
|
+
tmp = (set(maps['reactants']) | set(maps['products'])) & set(maps['reagents'])
|
|
88
|
+
if tmp:
|
|
89
|
+
e = f'reagents has map intersection with reactants or products: {tmp}'
|
|
90
|
+
if not ignore:
|
|
91
|
+
raise MappingError(e)
|
|
92
|
+
if data.get('log') is None:
|
|
93
|
+
data['log'] = []
|
|
94
|
+
data['log'].append(e)
|
|
95
|
+
maps['reagents'] = [x if x not in tmp else next(length) for x in maps['reagents']]
|
|
96
|
+
|
|
97
|
+
# find breaks in map. e.g. 1,2,5,6. 3,4 - skipped
|
|
98
|
+
if remap:
|
|
99
|
+
lose = sorted(
|
|
100
|
+
set(range(1, next(length))) - set(maps['reactants']) - set(maps['products']) - set(maps['reagents']),
|
|
101
|
+
reverse=True)
|
|
102
|
+
if lose:
|
|
103
|
+
for i, tmp in maps.items():
|
|
104
|
+
if not tmp:
|
|
105
|
+
continue
|
|
106
|
+
for j in lose:
|
|
107
|
+
maps[i] = tmp = [x if x < j else x - 1 for x in tmp]
|
|
108
|
+
|
|
109
|
+
for i, tmp in maps.items():
|
|
110
|
+
shift = 0
|
|
111
|
+
for j in data[i]:
|
|
112
|
+
atom_len = len(j['atoms'])
|
|
113
|
+
j['mapping'] = tmp[shift: atom_len + shift]
|
|
114
|
+
shift += atom_len
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
__all__ = ['postprocess_parsed_molecule', 'postprocess_parsed_reaction']
|
|
Binary file
|
chython/files/_xyz.pyx
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# cython: freethreading_compatible=True
|
|
2
|
+
#
|
|
3
|
+
# Copyright 2022-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
|
+
cimport cython
|
|
20
|
+
from cpython.mem cimport PyMem_Malloc, PyMem_Free
|
|
21
|
+
from libc.math cimport sqrt
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
cdef extern from "Python.h":
|
|
25
|
+
dict _PyDict_NewPresized(Py_ssize_t minused)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@cython.boundscheck(False)
|
|
29
|
+
@cython.wraparound(False)
|
|
30
|
+
def possible_bonds(double[:, ::1] xyz not None, double[::1] radii not None, double multiplier):
|
|
31
|
+
cdef unsigned int size, max_bonds, c = 0, b = 0, n, m, k
|
|
32
|
+
cdef double d, nx, ny, nz, rn, mx, my, mz
|
|
33
|
+
cdef dict py_bonds, tmp
|
|
34
|
+
|
|
35
|
+
size = xyz.shape[0]
|
|
36
|
+
max_bonds = size * 10 # each atom has less then 10 neighbors approximately
|
|
37
|
+
|
|
38
|
+
cdef unsigned int *ns = <unsigned int *> PyMem_Malloc(size * sizeof(unsigned int))
|
|
39
|
+
cdef unsigned int *mi = <unsigned int *> PyMem_Malloc(max_bonds * sizeof(unsigned int))
|
|
40
|
+
cdef double *ds = <double *> PyMem_Malloc(max_bonds * sizeof(double))
|
|
41
|
+
|
|
42
|
+
if not ns or not mi or not ds:
|
|
43
|
+
raise MemoryError()
|
|
44
|
+
|
|
45
|
+
for n in range(size - 1):
|
|
46
|
+
nx, ny, nz = xyz[n, 0], xyz[n, 1], xyz[n, 2]
|
|
47
|
+
rn = radii[n]
|
|
48
|
+
for m in range(n + 1, size):
|
|
49
|
+
mx, my, mz = nx - xyz[m, 0], ny - xyz[m, 1], nz - xyz[m, 2]
|
|
50
|
+
d = sqrt(mx * mx + my * my + mz * mz)
|
|
51
|
+
if d <= (rn + radii[m]) * multiplier:
|
|
52
|
+
mi[c] = m
|
|
53
|
+
ds[c] = d
|
|
54
|
+
c += 1
|
|
55
|
+
ns[n] = c
|
|
56
|
+
|
|
57
|
+
# prepare dict of dicts
|
|
58
|
+
py_bonds = _PyDict_NewPresized(size)
|
|
59
|
+
for n in range(size):
|
|
60
|
+
py_bonds[n + 1] = {}
|
|
61
|
+
|
|
62
|
+
for n in range(size - 1):
|
|
63
|
+
c = ns[n]
|
|
64
|
+
n += 1
|
|
65
|
+
tmp = py_bonds[n]
|
|
66
|
+
for m in range(b, c):
|
|
67
|
+
k = mi[m] + 1
|
|
68
|
+
tmp[k] = py_bonds[k][n] = ds[m]
|
|
69
|
+
b = c
|
|
70
|
+
|
|
71
|
+
PyMem_Free(ns)
|
|
72
|
+
PyMem_Free(mi)
|
|
73
|
+
PyMem_Free(ds)
|
|
74
|
+
return py_bonds
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
#
|
|
3
|
+
# Copyright 2018-2026 Ramil Nugmanov <nougmanoff@protonmail.com>
|
|
4
|
+
# Copyright 2019 Artem Mukanov <nostro32@mail.ru>
|
|
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 .smarts import *
|
|
21
|
+
from .smiles import *
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
__all__ = ['smiles', 'smarts']
|