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/MRVrw.py
ADDED
|
@@ -0,0 +1,532 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
#
|
|
3
|
+
# Copyright 2017-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 itertools import count, islice, chain
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
from typing import Union, List, Iterator, Dict, Optional
|
|
23
|
+
from xml.etree.ElementTree import iterparse, tostring
|
|
24
|
+
from ._convert import create_molecule, create_reaction
|
|
25
|
+
from ._mapping import postprocess_parsed_molecule, postprocess_parsed_reaction
|
|
26
|
+
from .mdl import postprocess_molecule
|
|
27
|
+
from ..containers import MoleculeContainer, ReactionContainer
|
|
28
|
+
from ..exceptions import EmptyMolecule, EmptyReaction
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
bond_map = {8: '1" queryType="Any', 4: 'A', 1: '1', 2: '2', 3: '3',
|
|
32
|
+
'Any': 8, 'any': 8, 'A': 4, 'a': 4, '1': 1, '2': 2, '3': 3}
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _local_name(tag):
|
|
36
|
+
if tag[0] == '{':
|
|
37
|
+
return tag.split('}', 1)[1]
|
|
38
|
+
return tag
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def xml_dict(parent_element, stop_list=None):
|
|
42
|
+
stop_list = set() if stop_list is None else set(stop_list)
|
|
43
|
+
out = {}
|
|
44
|
+
for x, y in parent_element.items():
|
|
45
|
+
y = y.strip()
|
|
46
|
+
if y:
|
|
47
|
+
x = '@%s' % x.strip()
|
|
48
|
+
out[x] = y
|
|
49
|
+
|
|
50
|
+
text = []
|
|
51
|
+
if len(parent_element):
|
|
52
|
+
elements_grouped = defaultdict(list)
|
|
53
|
+
for element in parent_element:
|
|
54
|
+
name = _local_name(element.tag)
|
|
55
|
+
if name in stop_list:
|
|
56
|
+
tail = element.tail
|
|
57
|
+
element.tail = None
|
|
58
|
+
text.append(tostring(element, encoding='unicode'))
|
|
59
|
+
element.tail = tail
|
|
60
|
+
else:
|
|
61
|
+
elements_grouped[name].append(element)
|
|
62
|
+
|
|
63
|
+
if element.tail:
|
|
64
|
+
t = element.tail.strip()
|
|
65
|
+
if t:
|
|
66
|
+
text.append(t)
|
|
67
|
+
|
|
68
|
+
for element_tag, element_group in elements_grouped.items():
|
|
69
|
+
if len(element_group) == 1:
|
|
70
|
+
out[element_tag] = xml_dict(element_group[0], stop_list)
|
|
71
|
+
else:
|
|
72
|
+
out[element_tag] = [xml_dict(x, stop_list) for x in element_group]
|
|
73
|
+
|
|
74
|
+
if parent_element.text:
|
|
75
|
+
t = parent_element.text.strip()
|
|
76
|
+
if t:
|
|
77
|
+
text.insert(0, t)
|
|
78
|
+
if text:
|
|
79
|
+
out['$'] = ''.join(text)
|
|
80
|
+
|
|
81
|
+
return out
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class MRVRead:
|
|
85
|
+
"""
|
|
86
|
+
ChemAxon MRV files reader. works similar to opened file object. support `with` context manager.
|
|
87
|
+
on initialization accept opened in binary mode file, string path to file,
|
|
88
|
+
pathlib.Path object or another binary buffered reader object
|
|
89
|
+
"""
|
|
90
|
+
molecule_cls = MoleculeContainer
|
|
91
|
+
reaction_cls = ReactionContainer
|
|
92
|
+
|
|
93
|
+
def __init__(self, file, *, ignore: bool = True, remap: bool = False,
|
|
94
|
+
calc_cis_trans: bool = False, ignore_stereo: bool = False, ignore_bad_isotopes: bool = False):
|
|
95
|
+
"""
|
|
96
|
+
:param ignore: Skip some checks of data or try to fix some errors.
|
|
97
|
+
:param remap: Remap atom numbers started from one.
|
|
98
|
+
:param calc_cis_trans: Calculate cis/trans marks from 2d coordinates.
|
|
99
|
+
:param ignore_stereo: Ignore stereo data.
|
|
100
|
+
:param ignore_bad_isotopes: reset invalid isotope mark to non-isotopic.
|
|
101
|
+
"""
|
|
102
|
+
if isinstance(file, str):
|
|
103
|
+
self.__file = open(file, 'rb')
|
|
104
|
+
self.__is_buffer = False
|
|
105
|
+
elif isinstance(file, Path):
|
|
106
|
+
self.__file = file.open('rb')
|
|
107
|
+
self.__is_buffer = False
|
|
108
|
+
elif hasattr(file, 'read'):
|
|
109
|
+
self.__file = file
|
|
110
|
+
self.__is_buffer = True
|
|
111
|
+
else:
|
|
112
|
+
raise TypeError('invalid file. file-like object or path to file expected')
|
|
113
|
+
self.__ignore = ignore
|
|
114
|
+
self.__remap = remap
|
|
115
|
+
self.__calc_cis_trans = calc_cis_trans
|
|
116
|
+
self.__ignore_stereo = ignore_stereo
|
|
117
|
+
self.__ignore_bad_isotopes = ignore_bad_isotopes
|
|
118
|
+
self.__tell = 0
|
|
119
|
+
self.__xml = iterparse(self.__file, events=('end',))
|
|
120
|
+
self.__buffer = None
|
|
121
|
+
|
|
122
|
+
def read(self, amount: Optional[int] = None) -> List[Union[ReactionContainer, MoleculeContainer]]:
|
|
123
|
+
"""
|
|
124
|
+
Parse whole file
|
|
125
|
+
|
|
126
|
+
:param amount: number of records to read
|
|
127
|
+
"""
|
|
128
|
+
if amount:
|
|
129
|
+
return list(islice(iter(self), amount))
|
|
130
|
+
return list(iter(self))
|
|
131
|
+
|
|
132
|
+
def read_structure(self, *, current: bool = True):
|
|
133
|
+
"""
|
|
134
|
+
Read Reaction or Molecule container.
|
|
135
|
+
|
|
136
|
+
:param current: return current structure if already parsed, otherwise read next
|
|
137
|
+
"""
|
|
138
|
+
data = self._read_block(current=current)
|
|
139
|
+
meta = self.read_metadata()
|
|
140
|
+
log = []
|
|
141
|
+
|
|
142
|
+
if 'molecule' in data and isinstance(data['molecule'], dict):
|
|
143
|
+
data = data['molecule']
|
|
144
|
+
tmp = parse_molecule(data)
|
|
145
|
+
postprocess_parsed_molecule(tmp, remap=self.__remap, ignore=self.__ignore)
|
|
146
|
+
parse_sgroup(data, tmp)
|
|
147
|
+
mol = create_molecule(tmp, ignore_bad_isotopes=self.__ignore_bad_isotopes, _cls=self.molecule_cls)
|
|
148
|
+
if not self.__ignore_stereo:
|
|
149
|
+
postprocess_molecule(mol, tmp, calc_cis_trans=self.__calc_cis_trans)
|
|
150
|
+
if meta:
|
|
151
|
+
mol.meta.update(meta)
|
|
152
|
+
return mol
|
|
153
|
+
elif 'reaction' in data and isinstance(data['reaction'], dict):
|
|
154
|
+
data = data['reaction']
|
|
155
|
+
tmp = {'reactants': [], 'products': [], 'reagents': [], 'log': log, 'title': data.get('@title')}
|
|
156
|
+
|
|
157
|
+
n = 0
|
|
158
|
+
for tag, group in (('reactantList', 'reactants'), ('productList', 'products'), ('agentList', 'reagents')):
|
|
159
|
+
if tag in data and 'molecule' in data[tag]:
|
|
160
|
+
molecule = data[tag]['molecule']
|
|
161
|
+
if isinstance(molecule, dict):
|
|
162
|
+
molecule = (molecule,)
|
|
163
|
+
for m in molecule:
|
|
164
|
+
n += 1
|
|
165
|
+
try:
|
|
166
|
+
tmp[group].append(parse_molecule(m))
|
|
167
|
+
except ValueError as e:
|
|
168
|
+
if isinstance(e, EmptyMolecule):
|
|
169
|
+
log.append(f'ignored empty molecule {n}')
|
|
170
|
+
elif self.__ignore:
|
|
171
|
+
log.append(f'ignored molecule {n} with {e}')
|
|
172
|
+
else:
|
|
173
|
+
raise
|
|
174
|
+
|
|
175
|
+
if not tmp['reactants'] and not tmp['products'] and not tmp['reagents']:
|
|
176
|
+
raise EmptyReaction
|
|
177
|
+
|
|
178
|
+
postprocess_parsed_reaction(tmp, remap=self.__remap, ignore=self.__ignore)
|
|
179
|
+
rxn = create_reaction(tmp, ignore_bad_isotopes=self.__ignore_bad_isotopes, _m_cls=self.molecule_cls,
|
|
180
|
+
_r_cls=self.reaction_cls)
|
|
181
|
+
if not self.__ignore_stereo:
|
|
182
|
+
for mol, parsed in zip(rxn.molecules(), chain(tmp['reactants'], tmp['reagents'], tmp['products'])):
|
|
183
|
+
postprocess_molecule(mol, parsed, calc_cis_trans=self.__calc_cis_trans)
|
|
184
|
+
if meta:
|
|
185
|
+
rxn.meta.update(meta)
|
|
186
|
+
return rxn
|
|
187
|
+
else:
|
|
188
|
+
raise ValueError('reaction or molecule expected')
|
|
189
|
+
|
|
190
|
+
def read_metadata(self, *, current: bool = True) -> Dict[str, str]:
|
|
191
|
+
"""
|
|
192
|
+
Read metadata block
|
|
193
|
+
"""
|
|
194
|
+
data = self._read_block(current=current)
|
|
195
|
+
if 'molecule' in data and isinstance(data['molecule'], dict):
|
|
196
|
+
data = data['molecule']
|
|
197
|
+
elif 'reaction' in data and isinstance(data['reaction'], dict):
|
|
198
|
+
data = data['reaction']
|
|
199
|
+
else:
|
|
200
|
+
raise ValueError('reaction or molecule expected')
|
|
201
|
+
|
|
202
|
+
if 'propertyList' in data and 'property' in data['propertyList']:
|
|
203
|
+
data = data['propertyList']['property']
|
|
204
|
+
meta = {}
|
|
205
|
+
if isinstance(data, dict):
|
|
206
|
+
key = data['@title']
|
|
207
|
+
val = data['scalar']['$'].strip()
|
|
208
|
+
if key and val:
|
|
209
|
+
meta[key] = val
|
|
210
|
+
else:
|
|
211
|
+
meta['chython_unparsed_metadata'] = [data]
|
|
212
|
+
else:
|
|
213
|
+
for x in data:
|
|
214
|
+
key = x['@title']
|
|
215
|
+
val = x['scalar']['$'].strip()
|
|
216
|
+
if key and val:
|
|
217
|
+
meta[key] = val
|
|
218
|
+
else:
|
|
219
|
+
if 'chython_unparsed_metadata' not in meta:
|
|
220
|
+
meta['chython_unparsed_metadata'] = []
|
|
221
|
+
meta['chython_unparsed_metadata'].append(x)
|
|
222
|
+
return meta
|
|
223
|
+
else:
|
|
224
|
+
return {}
|
|
225
|
+
|
|
226
|
+
def close(self, force: bool = False):
|
|
227
|
+
"""
|
|
228
|
+
Close opened file
|
|
229
|
+
|
|
230
|
+
:param force: force closing of externally opened file or buffer
|
|
231
|
+
"""
|
|
232
|
+
if not self.__is_buffer or force:
|
|
233
|
+
self.__file.close()
|
|
234
|
+
|
|
235
|
+
def tell(self):
|
|
236
|
+
"""
|
|
237
|
+
Number of records processed from the original file
|
|
238
|
+
"""
|
|
239
|
+
return self.__tell
|
|
240
|
+
|
|
241
|
+
def __enter__(self):
|
|
242
|
+
return self
|
|
243
|
+
|
|
244
|
+
def __exit__(self, _type, value, traceback):
|
|
245
|
+
self.close()
|
|
246
|
+
|
|
247
|
+
def __iter__(self) -> Iterator[Union[ReactionContainer, MoleculeContainer]]:
|
|
248
|
+
while True:
|
|
249
|
+
try:
|
|
250
|
+
yield self.read_structure(current=False)
|
|
251
|
+
except ValueError:
|
|
252
|
+
pass
|
|
253
|
+
except EOFError:
|
|
254
|
+
return
|
|
255
|
+
|
|
256
|
+
def __next__(self) -> Union[ReactionContainer, MoleculeContainer]:
|
|
257
|
+
return next(iter(self))
|
|
258
|
+
|
|
259
|
+
def _read_block(self, *, current: bool = True) -> dict:
|
|
260
|
+
if not current or not self.__buffer:
|
|
261
|
+
self.__buffer = None
|
|
262
|
+
for event, e in self.__xml:
|
|
263
|
+
if _local_name(e.tag) == 'MChemicalStruct':
|
|
264
|
+
self.__buffer = xml_dict(e)
|
|
265
|
+
self.__tell += 1
|
|
266
|
+
e.clear()
|
|
267
|
+
break
|
|
268
|
+
else:
|
|
269
|
+
raise EOFError
|
|
270
|
+
return self.__buffer
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def parse_molecule(data):
|
|
274
|
+
atoms, bonds, stereo = [], [], []
|
|
275
|
+
log = []
|
|
276
|
+
atom_map = {}
|
|
277
|
+
if 'atom' in data['atomArray']:
|
|
278
|
+
da = data['atomArray']['atom']
|
|
279
|
+
if isinstance(da, dict):
|
|
280
|
+
da = (da,)
|
|
281
|
+
for n, atom in enumerate(da):
|
|
282
|
+
atom_map[atom['@id']] = n
|
|
283
|
+
atoms.append({'element': atom['@elementType'],
|
|
284
|
+
'isotope': int(atom['@isotope']) if '@isotope' in atom else None,
|
|
285
|
+
'charge': int(atom.get('@formalCharge', 0)),
|
|
286
|
+
'is_radical': '@radical' in atom,
|
|
287
|
+
'parsed_mapping': int(atom.get('@mrvMap', 0))})
|
|
288
|
+
if '@z3' in atom:
|
|
289
|
+
atoms[-1].update(x=float(atom['@x3']), y=float(atom['@y3']), z=float(atom['@z3']))
|
|
290
|
+
else:
|
|
291
|
+
atoms[-1].update(x=float(atom['@x2']) / 2, y=float(atom['@y2']) / 2)
|
|
292
|
+
if '@mrvQueryProps' in atom:
|
|
293
|
+
raise ValueError('queries unsupported')
|
|
294
|
+
if '@hydrogenCount' in atom:
|
|
295
|
+
atoms[-1]['implicit_hydrogens'] = int(atom['@hydrogenCount'])
|
|
296
|
+
else:
|
|
297
|
+
atom = data['atomArray']
|
|
298
|
+
for n, (_id, e) in enumerate(zip(atom['@atomID'].split(), atom['@elementType'].split())):
|
|
299
|
+
atom_map[_id] = n
|
|
300
|
+
atoms.append({'element': e})
|
|
301
|
+
if '@z3' in atom:
|
|
302
|
+
for a, x, y, z in zip(atoms, atom['@x3'].split(), atom['@y3'].split(), atom['@z3'].split()):
|
|
303
|
+
a['x'] = float(x)
|
|
304
|
+
a['y'] = float(y)
|
|
305
|
+
a['z'] = float(z)
|
|
306
|
+
else:
|
|
307
|
+
for a, x, y in zip(atoms, atom['@x2'].split(), atom['@y2'].split()):
|
|
308
|
+
a['x'] = float(x) / 2
|
|
309
|
+
a['y'] = float(y) / 2
|
|
310
|
+
if '@isotope' in atom:
|
|
311
|
+
for a, x in zip(atoms, atom['@isotope'].split()):
|
|
312
|
+
if x != '0':
|
|
313
|
+
a['isotope'] = int(x)
|
|
314
|
+
if '@formalCharge' in atom:
|
|
315
|
+
for a, x in zip(atoms, atom['@formalCharge'].split()):
|
|
316
|
+
if x != '0':
|
|
317
|
+
a['charge'] = int(x)
|
|
318
|
+
if '@mrvMap' in atom:
|
|
319
|
+
for a, x in zip(atoms, atom['@mrvMap'].split()):
|
|
320
|
+
if x != '0':
|
|
321
|
+
a['parsed_mapping'] = int(x)
|
|
322
|
+
if '@radical' in atom:
|
|
323
|
+
for a, x in zip(atoms, atom['@radical'].split()):
|
|
324
|
+
if x != '0':
|
|
325
|
+
a['is_radical'] = True
|
|
326
|
+
if '@mrvQueryProps' in atom:
|
|
327
|
+
raise ValueError('queries unsupported')
|
|
328
|
+
if not atoms:
|
|
329
|
+
raise EmptyMolecule
|
|
330
|
+
|
|
331
|
+
if 'bond' in data['bondArray']:
|
|
332
|
+
db = data['bondArray']['bond']
|
|
333
|
+
if isinstance(db, dict):
|
|
334
|
+
db = (db,)
|
|
335
|
+
for bond in db:
|
|
336
|
+
order = bond_map[bond['@queryType' if '@queryType' in bond else '@order']]
|
|
337
|
+
a1, a2 = bond['@atomRefs2'].split()
|
|
338
|
+
if 'bondStereo' in bond:
|
|
339
|
+
if '$' in bond['bondStereo']:
|
|
340
|
+
s = bond['bondStereo']['$']
|
|
341
|
+
if s == 'H':
|
|
342
|
+
stereo.append((atom_map[a1], atom_map[a2], -1))
|
|
343
|
+
elif s == 'W':
|
|
344
|
+
stereo.append((atom_map[a1], atom_map[a2], 1))
|
|
345
|
+
else:
|
|
346
|
+
log.append('invalid or unsupported stereo')
|
|
347
|
+
else:
|
|
348
|
+
log.append('incorrect bondStereo tag')
|
|
349
|
+
bonds.append((atom_map[a1], atom_map[a2], order))
|
|
350
|
+
|
|
351
|
+
return {'atoms': atoms, 'bonds': bonds, 'stereo': stereo,
|
|
352
|
+
'title': data.get('@title'), 'log': log, 'atom_map': atom_map}
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
def parse_sgroup(data, molecule):
|
|
356
|
+
if 'molecule' in data:
|
|
357
|
+
data = data['molecule']
|
|
358
|
+
if isinstance(data, dict):
|
|
359
|
+
data = (data,)
|
|
360
|
+
|
|
361
|
+
sgroups = {}
|
|
362
|
+
atom_map = molecule['mapping']
|
|
363
|
+
atom_map = {k: atom_map[v] for k, v in molecule['atom_map'].items()}
|
|
364
|
+
for x in data:
|
|
365
|
+
if '@atomRefs' in x:
|
|
366
|
+
atoms = [atom_map[x] for x in x['@atomRefs'].split()]
|
|
367
|
+
elif 'AttachmentPointArray' in x:
|
|
368
|
+
atoms = x['AttachmentPointArray']['attachmentPoint']
|
|
369
|
+
if isinstance(atoms, dict):
|
|
370
|
+
atoms = (atoms,)
|
|
371
|
+
atoms = [atom_map[x['@atom']] for x in atoms]
|
|
372
|
+
else:
|
|
373
|
+
continue
|
|
374
|
+
tmp = {k[1:]: v for k, v in x.items() if k not in ('@atomRefs', '@id', '@molID') and k.startswith('@')}
|
|
375
|
+
tmp['atoms'] = atoms
|
|
376
|
+
sgroups[x['@id']] = tmp
|
|
377
|
+
molecule['meta'] = sgroups
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
class MRVWrite:
|
|
381
|
+
"""
|
|
382
|
+
ChemAxon MRV files writer. works similar to opened for writing file object. support `with` context manager.
|
|
383
|
+
on initialization accept opened for writing in text mode file, string path to file,
|
|
384
|
+
pathlib.Path object or another buffered writer object
|
|
385
|
+
"""
|
|
386
|
+
def __init__(self, file, mapping: bool = True):
|
|
387
|
+
"""
|
|
388
|
+
:param mapping: write atom mapping.
|
|
389
|
+
"""
|
|
390
|
+
if isinstance(file, str):
|
|
391
|
+
self.__file = open(file, 'w')
|
|
392
|
+
self.__is_buffer = False
|
|
393
|
+
elif isinstance(file, Path):
|
|
394
|
+
self.__file = file.open('w')
|
|
395
|
+
self.__is_buffer = False
|
|
396
|
+
elif hasattr(file, 'write'):
|
|
397
|
+
self.__file = file
|
|
398
|
+
self.__is_buffer = True
|
|
399
|
+
else:
|
|
400
|
+
raise TypeError('invalid file. file-like object or path to file expected')
|
|
401
|
+
self.__writable = True
|
|
402
|
+
self.__finalized = False
|
|
403
|
+
self.__mapping = mapping
|
|
404
|
+
|
|
405
|
+
def close(self, force=False):
|
|
406
|
+
"""
|
|
407
|
+
Write close tag of MRV file and close opened file
|
|
408
|
+
|
|
409
|
+
:param force: force closing of externally opened file or buffer
|
|
410
|
+
"""
|
|
411
|
+
if not self.__finalized:
|
|
412
|
+
self.__file.write('</cml>\n')
|
|
413
|
+
self.__finalized = True
|
|
414
|
+
if self.__writable:
|
|
415
|
+
self.write = self.__write_closed
|
|
416
|
+
self.__writable = False
|
|
417
|
+
|
|
418
|
+
if not self.__is_buffer or force:
|
|
419
|
+
self.__file.close()
|
|
420
|
+
|
|
421
|
+
def __enter__(self):
|
|
422
|
+
return self
|
|
423
|
+
|
|
424
|
+
def __exit__(self, _type, value, traceback):
|
|
425
|
+
self.close()
|
|
426
|
+
|
|
427
|
+
@staticmethod
|
|
428
|
+
def __write_closed(_):
|
|
429
|
+
raise ValueError('I/O operation on closed writer')
|
|
430
|
+
|
|
431
|
+
def write(self, data: Union[ReactionContainer, MoleculeContainer]):
|
|
432
|
+
"""
|
|
433
|
+
Write single molecule or reaction into file
|
|
434
|
+
"""
|
|
435
|
+
self.__file.write('<cml>\n')
|
|
436
|
+
self.__write(data)
|
|
437
|
+
self.write = self.__write
|
|
438
|
+
|
|
439
|
+
def __write(self, data):
|
|
440
|
+
file = self.__file
|
|
441
|
+
file.write('<MDocument><MChemicalStruct>')
|
|
442
|
+
if isinstance(data, ReactionContainer):
|
|
443
|
+
if not data._arrow:
|
|
444
|
+
data.fix_positions()
|
|
445
|
+
if data.name:
|
|
446
|
+
file.write(f'<reaction title="{data.name}">')
|
|
447
|
+
else:
|
|
448
|
+
file.write('<reaction>')
|
|
449
|
+
|
|
450
|
+
if data.meta:
|
|
451
|
+
file.write('<propertyList>')
|
|
452
|
+
for k, v in data.meta.items():
|
|
453
|
+
if isinstance(v, str):
|
|
454
|
+
v = f'<![CDATA[{v}]]>'
|
|
455
|
+
file.write(f'<property title="{k}"><scalar>{v}</scalar></property>')
|
|
456
|
+
file.write('</propertyList>')
|
|
457
|
+
|
|
458
|
+
c = count(1)
|
|
459
|
+
for i, j in ((data.reactants, 'reactantList'), (data.reagents, 'agentList'),
|
|
460
|
+
(data.products, 'productList')):
|
|
461
|
+
if not i:
|
|
462
|
+
continue
|
|
463
|
+
file.write(f'<{j}>')
|
|
464
|
+
for n, m in zip(c, i):
|
|
465
|
+
if m.name:
|
|
466
|
+
file.write(f'<molecule title="{m.name}" molID="m{n}">')
|
|
467
|
+
else:
|
|
468
|
+
file.write(f'<molecule molID="m{n}">')
|
|
469
|
+
|
|
470
|
+
self.__write_molecule(m)
|
|
471
|
+
file.write('</molecule>')
|
|
472
|
+
file.write(f'</{j}>')
|
|
473
|
+
|
|
474
|
+
file.write(f'<arrow type="DEFAULT" x1="{data._arrow[0] * 2:.4f}" y1="0" '
|
|
475
|
+
f'x2="{data._arrow[1] * 2:.4f}" y2="0"/></reaction>')
|
|
476
|
+
elif not isinstance(data, MoleculeContainer):
|
|
477
|
+
raise TypeError('MoleculeContainer expected')
|
|
478
|
+
else:
|
|
479
|
+
if data.name:
|
|
480
|
+
file.write(f'<molecule title="{data.name}">')
|
|
481
|
+
else:
|
|
482
|
+
file.write('<molecule>')
|
|
483
|
+
if data.meta:
|
|
484
|
+
file.write('<propertyList>')
|
|
485
|
+
for k, v in data.meta.items():
|
|
486
|
+
if isinstance(v, str):
|
|
487
|
+
v = f'<![CDATA[{v}]]>'
|
|
488
|
+
file.write(f'<property title="{k}"><scalar>{v}</scalar></property>')
|
|
489
|
+
file.write('</propertyList>')
|
|
490
|
+
|
|
491
|
+
self.__write_molecule(data)
|
|
492
|
+
file.write('</molecule>')
|
|
493
|
+
file.write('</MChemicalStruct></MDocument>\n')
|
|
494
|
+
|
|
495
|
+
def __write_molecule(self, g):
|
|
496
|
+
bg = g._bonds
|
|
497
|
+
mapping = self.__mapping
|
|
498
|
+
|
|
499
|
+
file = self.__file
|
|
500
|
+
file.write('<atomArray>')
|
|
501
|
+
for n, atom in g.atoms():
|
|
502
|
+
x, y = atom.x, atom.y
|
|
503
|
+
file.write(f'<atom id="a{n}" elementType="{atom.atomic_symbol}" x2="{x * 2:.4f}" y2="{y * 2:.4f}"')
|
|
504
|
+
if mapping:
|
|
505
|
+
file.write(f' mrvMap="{n}"')
|
|
506
|
+
if atom.charge:
|
|
507
|
+
file.write(f' formalCharge="{atom.charge}"')
|
|
508
|
+
if atom.is_radical:
|
|
509
|
+
file.write(' radical="monovalent"')
|
|
510
|
+
if atom.isotope:
|
|
511
|
+
file.write(f' isotope="{atom.isotope}"')
|
|
512
|
+
if atom.implicit_hydrogens is not None:
|
|
513
|
+
file.write(f' hydrogenCount="{atom.implicit_hydrogens}"')
|
|
514
|
+
file.write('/>')
|
|
515
|
+
file.write('</atomArray>')
|
|
516
|
+
|
|
517
|
+
file.write('<bondArray>')
|
|
518
|
+
wedge = defaultdict(set)
|
|
519
|
+
n = 0 # empty wedge trick
|
|
520
|
+
for n, (i, j, s) in enumerate(g._wedge_map, start=1):
|
|
521
|
+
file.write(f'<bond id="b{n}" atomRefs2="a{i} a{j}" order="{bond_map[bg[i][j].order]}">'
|
|
522
|
+
f'<bondStereo>{s == 1 and "W" or "H"}</bondStereo></bond>')
|
|
523
|
+
wedge[i].add(j)
|
|
524
|
+
wedge[j].add(i)
|
|
525
|
+
for i, j, bond in g.bonds():
|
|
526
|
+
if j not in wedge[i]:
|
|
527
|
+
n += 1
|
|
528
|
+
file.write(f'<bond id="b{n}" atomRefs2="a{i} a{j}" order="{bond_map[bond.order]}"/>')
|
|
529
|
+
file.write('</bondArray>')
|
|
530
|
+
|
|
531
|
+
|
|
532
|
+
__all__ = ['MRVRead', 'MRVWrite']
|