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/reactor/base.py
ADDED
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
#
|
|
3
|
+
# Copyright 2014-2026 Ramil Nugmanov <nougmanoff@protonmail.com>
|
|
4
|
+
# Copyright 2019 Adelia Fatykhova <adelik21979@gmail.com>
|
|
5
|
+
# This file is part of chython.
|
|
6
|
+
#
|
|
7
|
+
# chython is free software; you can redistribute it and/or modify
|
|
8
|
+
# it under the terms of the GNU Lesser General Public License as published by
|
|
9
|
+
# the Free Software Foundation; either version 3 of the License, or
|
|
10
|
+
# (at your option) any later version.
|
|
11
|
+
#
|
|
12
|
+
# This program is distributed in the hope that it will be useful,
|
|
13
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
14
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
15
|
+
# GNU Lesser General Public License for more details.
|
|
16
|
+
#
|
|
17
|
+
# You should have received a copy of the GNU Lesser General Public License
|
|
18
|
+
# along with this program; if not, see <https://www.gnu.org/licenses/>.
|
|
19
|
+
#
|
|
20
|
+
from typing import Union
|
|
21
|
+
from ..containers import MoleculeContainer, QueryContainer
|
|
22
|
+
from ..containers.bonds import Bond
|
|
23
|
+
from ..periodictable import Element, AnyElement, QueryElement
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class BaseReactor:
|
|
27
|
+
def __init__(self, pattern, replacement, delete_atoms, canonicalize, fix_tautomers, ignore_pyrrole_hydrogen):
|
|
28
|
+
if isinstance(replacement, QueryContainer):
|
|
29
|
+
for n, a in replacement.atoms():
|
|
30
|
+
if not isinstance(a, (AnyElement, QueryElement)):
|
|
31
|
+
raise TypeError('Unsupported query atom type')
|
|
32
|
+
elif len(a.implicit_hydrogens) > 1:
|
|
33
|
+
raise ValueError('Query element in patch has more than one implicit hydrogen clause')
|
|
34
|
+
for *_, b in replacement.bonds():
|
|
35
|
+
if len(b.order) > 1:
|
|
36
|
+
raise ValueError('Variable bond in replacement')
|
|
37
|
+
|
|
38
|
+
self._to_delete = {n for n, a in pattern.atoms() if not a.masked} - set(replacement) if delete_atoms else ()
|
|
39
|
+
self._replacement = replacement
|
|
40
|
+
self._canonicalize = canonicalize
|
|
41
|
+
self._fix_tautomers = fix_tautomers
|
|
42
|
+
self._ignore_pyrrole_hydrogen = ignore_pyrrole_hydrogen
|
|
43
|
+
|
|
44
|
+
def _get_deleted(self, structure, mapping):
|
|
45
|
+
if not self._to_delete:
|
|
46
|
+
return set()
|
|
47
|
+
|
|
48
|
+
bonds = structure._bonds
|
|
49
|
+
to_delete = {mapping[x] for x in self._to_delete}
|
|
50
|
+
# if deleted atoms have another path to remain fragment, the path is preserved
|
|
51
|
+
remain = set(mapping.values()).difference(to_delete)
|
|
52
|
+
delete, global_seen = set(), set()
|
|
53
|
+
for x in to_delete:
|
|
54
|
+
for n in bonds[x]:
|
|
55
|
+
if n in global_seen or n in remain:
|
|
56
|
+
continue
|
|
57
|
+
seen = {n}
|
|
58
|
+
global_seen.add(n)
|
|
59
|
+
stack = [x for x in bonds[n] if x not in global_seen]
|
|
60
|
+
while stack:
|
|
61
|
+
current = stack.pop()
|
|
62
|
+
if current in remain:
|
|
63
|
+
break
|
|
64
|
+
if current in to_delete:
|
|
65
|
+
continue
|
|
66
|
+
seen.add(current)
|
|
67
|
+
global_seen.add(current)
|
|
68
|
+
stack.extend([x for x in bonds[current] if x not in global_seen])
|
|
69
|
+
else:
|
|
70
|
+
delete.update(seen)
|
|
71
|
+
|
|
72
|
+
to_delete.update(delete)
|
|
73
|
+
return to_delete
|
|
74
|
+
|
|
75
|
+
def _patcher(self, structure: MoleculeContainer, mapping):
|
|
76
|
+
satoms = structure._atoms
|
|
77
|
+
sbonds = structure._bonds
|
|
78
|
+
|
|
79
|
+
to_delete = self._get_deleted(structure, mapping)
|
|
80
|
+
new = structure.__class__()
|
|
81
|
+
natoms = new._atoms
|
|
82
|
+
nbonds = new._bonds
|
|
83
|
+
max_atom = max(satoms)
|
|
84
|
+
stereo_atoms = []
|
|
85
|
+
stereo_bonds = []
|
|
86
|
+
|
|
87
|
+
# let's preserve connectivity order from replacement to keep stereo signs as is.
|
|
88
|
+
# stereo labels from original structure will be recalculated after full molecule construction.
|
|
89
|
+
for n, ra in self._replacement.atoms():
|
|
90
|
+
if isinstance(ra, AnyElement):
|
|
91
|
+
if m := mapping.get(n):
|
|
92
|
+
# keep matched atom type and isotope
|
|
93
|
+
sa = satoms[m]
|
|
94
|
+
a = sa.copy()
|
|
95
|
+
a.charge = ra.charge
|
|
96
|
+
a.is_radical = ra.is_radical
|
|
97
|
+
if ra.stereo is not None: # override stereo
|
|
98
|
+
a._stereo = ra.stereo
|
|
99
|
+
a._extended_stereo = sa.extended_stereo # stereo swap doesn't resolve uncertainty
|
|
100
|
+
elif sa.stereo is not None: # keep original stereo
|
|
101
|
+
stereo_atoms.append(m) # mark for stereo fix
|
|
102
|
+
a._extended_stereo = sa._extended_stereo # masked by property if retranslation fails
|
|
103
|
+
else:
|
|
104
|
+
raise ValueError("AnyElement doesn't match to pattern")
|
|
105
|
+
else: # QueryElement or Element
|
|
106
|
+
ra: Union[QueryElement, Element] # typehint
|
|
107
|
+
e = Element.from_atomic_number(ra.atomic_number)
|
|
108
|
+
a = e(ra.isotope, charge=ra.charge, is_radical=ra.is_radical)
|
|
109
|
+
if not (m := mapping.get(n)): # new atom
|
|
110
|
+
m = max_atom + 1
|
|
111
|
+
mapping[n] = max_atom = m
|
|
112
|
+
a._stereo = ra.stereo # keep stereo from patch for new atoms
|
|
113
|
+
if isinstance(ra, Element):
|
|
114
|
+
a._implicit_hydrogens = ra.implicit_hydrogens # keep H count from patch
|
|
115
|
+
a.xy = ra.xy # keep coordinates from patch
|
|
116
|
+
a._extended_stereo = ra._extended_stereo # propagate template's extended stereo
|
|
117
|
+
elif ra.implicit_hydrogens: # keep H count from patch
|
|
118
|
+
a._implicit_hydrogens = ra.implicit_hydrogens[0]
|
|
119
|
+
else: # existing atoms
|
|
120
|
+
sa = satoms[m]
|
|
121
|
+
a.xy = sa.xy # preserve existing coordinates
|
|
122
|
+
if ra.stereo is not None:
|
|
123
|
+
a._stereo = ra.stereo
|
|
124
|
+
# template's extended stereo takes priority (e.g. reaction giving racemic product)
|
|
125
|
+
if isinstance(ra, Element) and ra.extended_stereo is not None:
|
|
126
|
+
a._extended_stereo = ra.extended_stereo
|
|
127
|
+
else: # stereo swap doesn't resolve uncertainty
|
|
128
|
+
a._extended_stereo = sa.extended_stereo
|
|
129
|
+
elif sa.stereo is not None: # keep original stereo
|
|
130
|
+
stereo_atoms.append(m)
|
|
131
|
+
a._extended_stereo = sa._extended_stereo
|
|
132
|
+
natoms[m] = a
|
|
133
|
+
nbonds[m] = {}
|
|
134
|
+
|
|
135
|
+
# preserve connectivity order
|
|
136
|
+
for n, bs in self._replacement._bonds.items():
|
|
137
|
+
n = mapping[n]
|
|
138
|
+
for m, rb in bs.items():
|
|
139
|
+
m = mapping[m]
|
|
140
|
+
if n in nbonds[m]: # back-link
|
|
141
|
+
nbonds[n][m] = nbonds[m][n]
|
|
142
|
+
else:
|
|
143
|
+
nbonds[n][m] = b = Bond(int(rb))
|
|
144
|
+
if rb.stereo is not None: # override stereo
|
|
145
|
+
b._stereo = rb.stereo
|
|
146
|
+
# check bond exists in source and has stereo label and the same order
|
|
147
|
+
elif (sbn := sbonds.get(n)) is None or (sb := sbn.get(m)) is None or sb.stereo is None or sb != b:
|
|
148
|
+
continue
|
|
149
|
+
else: # original structure has stereo bond
|
|
150
|
+
stereo_bonds.append((n, m))
|
|
151
|
+
|
|
152
|
+
patched_atoms = set(new)
|
|
153
|
+
for n, sa in satoms.items(): # add unmatched or masked atoms
|
|
154
|
+
if n not in patched_atoms and n not in to_delete:
|
|
155
|
+
natoms[n] = a = sa.copy(hydrogens=True)
|
|
156
|
+
a._extended_stereo = sa._extended_stereo # preserve for atoms outside RC
|
|
157
|
+
nbonds[n] = {}
|
|
158
|
+
if sa.stereo is not None:
|
|
159
|
+
# in case of allenes label can disappear/change, thus, requires recalculation
|
|
160
|
+
# for tetrahedrons label can be stored as is
|
|
161
|
+
if n in structure.stereogenic_tetrahedrons:
|
|
162
|
+
a._stereo = sa.stereo
|
|
163
|
+
else:
|
|
164
|
+
stereo_atoms.append(n)
|
|
165
|
+
|
|
166
|
+
for n, bs in sbonds.items(): # preserve connectivity order for keeping stereo labels as is
|
|
167
|
+
if n in to_delete: # atoms for removing
|
|
168
|
+
continue
|
|
169
|
+
for m, b in bs.items():
|
|
170
|
+
# ignore deleted atoms and patch atoms
|
|
171
|
+
if m in to_delete or n in patched_atoms and m in patched_atoms:
|
|
172
|
+
continue
|
|
173
|
+
elif n in nbonds[m]: # back-link
|
|
174
|
+
nbonds[n][m] = nbonds[m][n]
|
|
175
|
+
else:
|
|
176
|
+
nbonds[n][m] = b.copy()
|
|
177
|
+
if b.stereo is not None:
|
|
178
|
+
# stereo label should be recalculated
|
|
179
|
+
stereo_bonds.append((n, m))
|
|
180
|
+
|
|
181
|
+
for n, a in new.atoms():
|
|
182
|
+
if a.implicit_hydrogens is None:
|
|
183
|
+
new.calc_implicit(n)
|
|
184
|
+
new.calc_labels()
|
|
185
|
+
|
|
186
|
+
# translate stereo sign from old order to new order
|
|
187
|
+
for n in stereo_atoms:
|
|
188
|
+
if n in new.stereogenic_tetrahedrons:
|
|
189
|
+
if sbonds[n].keys() == nbonds[n].keys():
|
|
190
|
+
# flush stereo from reaction center. should be explicitly set in replacement.
|
|
191
|
+
s = new._translate_tetrahedron_sign(n, structure.stereogenic_tetrahedrons[n], satoms[n].stereo)
|
|
192
|
+
natoms[n]._stereo = s
|
|
193
|
+
elif n in new.stereogenic_allenes:
|
|
194
|
+
if set(new.stereogenic_allenes[n]) == set(structure.stereogenic_allenes[n]):
|
|
195
|
+
# flush stereo for changed allene substituents
|
|
196
|
+
s = new._translate_allene_sign(n, *structure.stereogenic_allenes[n][:2], satoms[n].stereo)
|
|
197
|
+
natoms[n]._stereo = s
|
|
198
|
+
# else: ignore label
|
|
199
|
+
|
|
200
|
+
for n, m in stereo_bonds:
|
|
201
|
+
# check if bond is center of cumulene
|
|
202
|
+
if (n12 := new._stereo_cis_trans_terminals.get(n, True)) != new._stereo_cis_trans_terminals.get(m, False):
|
|
203
|
+
continue
|
|
204
|
+
s12 = structure._stereo_cis_trans_terminals[n]
|
|
205
|
+
# check if cumulene terminals are the same
|
|
206
|
+
if set(n12) != set(s12):
|
|
207
|
+
continue
|
|
208
|
+
if set(new.stereogenic_cis_trans[n12]) == set(env := structure.stereogenic_cis_trans[s12]):
|
|
209
|
+
# connected to cumulenes atoms should be the same
|
|
210
|
+
s = new._translate_cis_trans_sign(*n12, *env[:2], sbonds[n][m].stereo)
|
|
211
|
+
nbonds[n][m]._stereo = s
|
|
212
|
+
# else: ignore label
|
|
213
|
+
|
|
214
|
+
if self._canonicalize:
|
|
215
|
+
new.canonicalize(fix_tautomers=self._fix_tautomers,
|
|
216
|
+
ignore_pyrrole_hydrogen=self._ignore_pyrrole_hydrogen)
|
|
217
|
+
else:
|
|
218
|
+
new.fix_stereo()
|
|
219
|
+
return new
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
__all__ = ['BaseReactor']
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
#
|
|
3
|
+
# Copyright 2019-2026 Ramil Nugmanov <nougmanoff@protonmail.com>
|
|
4
|
+
# Copyright 2019 Adelia Fatykhova <adelik21979@gmail.com>
|
|
5
|
+
# This file is part of chython.
|
|
6
|
+
#
|
|
7
|
+
# chython is free software; you can redistribute it and/or modify
|
|
8
|
+
# it under the terms of the GNU Lesser General Public License as published by
|
|
9
|
+
# the Free Software Foundation; either version 3 of the License, or
|
|
10
|
+
# (at your option) any later version.
|
|
11
|
+
#
|
|
12
|
+
# This program is distributed in the hope that it will be useful,
|
|
13
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
14
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
15
|
+
# GNU Lesser General Public License for more details.
|
|
16
|
+
#
|
|
17
|
+
# You should have received a copy of the GNU Lesser General Public License
|
|
18
|
+
# along with this program; if not, see <https://www.gnu.org/licenses/>.
|
|
19
|
+
#
|
|
20
|
+
from collections import deque
|
|
21
|
+
from functools import reduce
|
|
22
|
+
from itertools import count, permutations, combinations
|
|
23
|
+
from logging import getLogger, INFO
|
|
24
|
+
from operator import or_
|
|
25
|
+
from typing import List, Iterator, Tuple, Union
|
|
26
|
+
from .base import BaseReactor
|
|
27
|
+
from .._functions import lazy_product
|
|
28
|
+
from ..containers import QueryContainer, MoleculeContainer, ReactionContainer
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
logger = getLogger('chython.reactor')
|
|
32
|
+
logger.setLevel(INFO)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class Reactor(BaseReactor):
|
|
36
|
+
"""
|
|
37
|
+
Reactor for molecules transformations.
|
|
38
|
+
Generates reaction from input molecules using transformation template.
|
|
39
|
+
|
|
40
|
+
Reactor calling transforms reactants to products and
|
|
41
|
+
returns generator of reaction transformations with all
|
|
42
|
+
possible reactions.
|
|
43
|
+
"""
|
|
44
|
+
def __init__(self, patterns: Tuple[QueryContainer, ...],
|
|
45
|
+
products: Tuple[Union[MoleculeContainer, QueryContainer], ...], *,
|
|
46
|
+
delete_atoms: bool = True, one_shot: bool = True, polymerise_limit: int = 10,
|
|
47
|
+
automorphism_filter: bool = True, canonicalize: bool = True,
|
|
48
|
+
fix_tautomers: bool = True, ignore_pyrrole_hydrogen: bool = False):
|
|
49
|
+
"""
|
|
50
|
+
:param patterns: Search patterns for each reactant.
|
|
51
|
+
:param products: Resulted structures.
|
|
52
|
+
:param delete_atoms: If True atoms exists in reactants but not exists in products will be removed.
|
|
53
|
+
:param one_shot: Do only single reaction center then True, else do all possible combinations of reactions.
|
|
54
|
+
:param polymerise_limit: Limit of self reactions. Make sense than one_shot = False.
|
|
55
|
+
:param automorphism_filter: Skip matches to same atoms.
|
|
56
|
+
:param canonicalize: Run full canonicalization on products (kekule, standardize, thiele, etc.).
|
|
57
|
+
:param fix_tautomers: Canonicalize tautomer forms. Passed to canonicalize().
|
|
58
|
+
:param ignore_pyrrole_hydrogen: Fix invalid rings like Cn1cc[nH]c1. Passed to canonicalize().
|
|
59
|
+
"""
|
|
60
|
+
if not patterns or not products:
|
|
61
|
+
raise ValueError('empty template')
|
|
62
|
+
|
|
63
|
+
if not all(isinstance(x, QueryContainer) for x in patterns):
|
|
64
|
+
raise TypeError('invalid params')
|
|
65
|
+
elif not all(isinstance(x, (QueryContainer, MoleculeContainer)) for x in products):
|
|
66
|
+
raise TypeError('invalid params')
|
|
67
|
+
self._patterns = patterns
|
|
68
|
+
self._products = products
|
|
69
|
+
|
|
70
|
+
self._one_shot = one_shot
|
|
71
|
+
self._polymerise_limit = polymerise_limit
|
|
72
|
+
self._products_atoms = tuple(set(m) for m in products)
|
|
73
|
+
self._automorphism_filter = automorphism_filter
|
|
74
|
+
super().__init__(reduce(or_, patterns), reduce(or_, products), delete_atoms, canonicalize,
|
|
75
|
+
fix_tautomers, ignore_pyrrole_hydrogen)
|
|
76
|
+
|
|
77
|
+
def __call__(self, *structures: MoleculeContainer):
|
|
78
|
+
if any(not isinstance(structure, MoleculeContainer) for structure in structures):
|
|
79
|
+
raise TypeError('only list of Molecules possible')
|
|
80
|
+
|
|
81
|
+
len_patterns = len(self._patterns)
|
|
82
|
+
structures = fix_mapping_overlap(structures)
|
|
83
|
+
s_nums = set(range(len(structures)))
|
|
84
|
+
seen = set()
|
|
85
|
+
if self._one_shot:
|
|
86
|
+
for chosen in permutations(s_nums, len_patterns):
|
|
87
|
+
ignored = [structures[x] for x in s_nums.difference(chosen)]
|
|
88
|
+
chosen = [structures[x] for x in chosen]
|
|
89
|
+
for new in self._single_stage(chosen, {x for x in ignored for x in x}):
|
|
90
|
+
# store reacted molecules in same order as matched pattern
|
|
91
|
+
r = ReactionContainer([x.copy() for x in chosen] + [x.copy() for x in ignored],
|
|
92
|
+
new + [x.copy() for x in ignored])
|
|
93
|
+
if len(new) > 1: # try to keep salts
|
|
94
|
+
r.contract_ions()
|
|
95
|
+
if str(r) in seen:
|
|
96
|
+
continue
|
|
97
|
+
seen.add(str(r))
|
|
98
|
+
yield r
|
|
99
|
+
else:
|
|
100
|
+
queue = deque(([structures[x] for x in chosen], [structures[x] for x in s_nums.difference(chosen)], 0)
|
|
101
|
+
for chosen in permutations(s_nums, len_patterns))
|
|
102
|
+
while queue:
|
|
103
|
+
chosen, ignored, depth = queue.popleft()
|
|
104
|
+
depth += 1
|
|
105
|
+
for new in self._single_stage(chosen, {x for x in ignored for x in x}):
|
|
106
|
+
r = ReactionContainer([x.copy() for x in structures], new + [x.copy() for x in ignored])
|
|
107
|
+
if len(new) > 1:
|
|
108
|
+
r.contract_ions() # try to keep salts
|
|
109
|
+
if str(r) in seen:
|
|
110
|
+
continue
|
|
111
|
+
seen.add(str(r))
|
|
112
|
+
if len(r.products) != len(ignored) + len(self._products_atoms):
|
|
113
|
+
logger.info('ambiguous multicomponent structures. skip multistage processing')
|
|
114
|
+
yield r
|
|
115
|
+
continue
|
|
116
|
+
elif str(r) in seen:
|
|
117
|
+
continue
|
|
118
|
+
else:
|
|
119
|
+
seen.add(str(r))
|
|
120
|
+
|
|
121
|
+
if depth < self._polymerise_limit:
|
|
122
|
+
prod = r.products
|
|
123
|
+
if len_patterns == 1: # simple case. only products or ignored can be transformed.
|
|
124
|
+
for i in range(len(prod)):
|
|
125
|
+
queue.append(([prod[i]], [*prod[:i], *prod[i + 1:]], depth))
|
|
126
|
+
else: # one of products molecule combined with previously chosen
|
|
127
|
+
for chp in combinations(chosen, len_patterns - 1):
|
|
128
|
+
for i in range(len(prod)):
|
|
129
|
+
for ch in permutations(fix_mapping_overlap((prod[i], *chp)), len_patterns):
|
|
130
|
+
queue.append((ch, [*prod[:i], *prod[i + 1:]], depth))
|
|
131
|
+
yield r
|
|
132
|
+
|
|
133
|
+
def _single_stage(self, chosen, ignored) -> Iterator[List[MoleculeContainer]]:
|
|
134
|
+
max_ignored_number = united_chosen = None
|
|
135
|
+
split = len(self._products_atoms) > 1
|
|
136
|
+
for match in lazy_product(*(x.get_mapping(y, automorphism_filter=self._automorphism_filter) for x, y in
|
|
137
|
+
zip(self._patterns, chosen))):
|
|
138
|
+
mapping = match[0].copy()
|
|
139
|
+
for m in match[1:]:
|
|
140
|
+
mapping.update(m)
|
|
141
|
+
if united_chosen is None:
|
|
142
|
+
united_chosen = reduce(or_, chosen)
|
|
143
|
+
max_ignored_number = max(ignored, default=0)
|
|
144
|
+
try:
|
|
145
|
+
new = self._patcher(united_chosen, mapping)
|
|
146
|
+
except Exception:
|
|
147
|
+
logger.info('invalid product structure, skipping')
|
|
148
|
+
continue
|
|
149
|
+
collision = set(new).intersection(ignored)
|
|
150
|
+
if collision:
|
|
151
|
+
new.remap(dict(zip(collision, count(max(max_ignored_number, max(new)) + 1))))
|
|
152
|
+
|
|
153
|
+
if split:
|
|
154
|
+
yield new.split()
|
|
155
|
+
else:
|
|
156
|
+
yield [new]
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def fix_mapping_overlap(structures) -> List[MoleculeContainer]:
|
|
160
|
+
if len(structures) == 1:
|
|
161
|
+
return list(structures)
|
|
162
|
+
checked = []
|
|
163
|
+
checked_atoms = set()
|
|
164
|
+
for structure in structures:
|
|
165
|
+
intersection = set(structure).intersection(checked_atoms)
|
|
166
|
+
if intersection:
|
|
167
|
+
mapping = dict(zip(intersection, count(max(max(checked_atoms), max(structure)) + 1)))
|
|
168
|
+
structure = structure.copy()
|
|
169
|
+
structure.remap(mapping)
|
|
170
|
+
logger.info('some atoms in input structures had the same numbers.\n'
|
|
171
|
+
f'atoms {list(mapping)} were remapped to {list(mapping.values())}')
|
|
172
|
+
checked_atoms.update(structure)
|
|
173
|
+
checked.append(structure)
|
|
174
|
+
return checked
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
__all__ = ['Reactor', 'fix_mapping_overlap']
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
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
|
+
#
|
|
@@ -0,0 +1,40 @@
|
|
|
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 chython import smiles, smarts, Reactor
|
|
20
|
+
from pytest import mark
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
data = [
|
|
24
|
+
(('[B;D3;x2;z1:4]([O:5])([O:6])-[C;@@;h1:3]1([O;M][C;M]1)', '[Cl,Br,I;D1:1]-[C;a:2]'), ('[A;@:3]-[A:2]',),
|
|
25
|
+
('CC1O[C@@H]1B(O)O', 'Brc1ccccc1'), ('CC1O[C@H]1c1ccccc1',)), # inverse stereo check
|
|
26
|
+
(('[B;D3;x2;z1:4]([O:5])([O:6])-[C;@@;h1:3]1([O;M][C;M]1)', '[Cl,Br,I;D1:1]-[C;a:2]'), ('[A;@@:3]-[A:2]',),
|
|
27
|
+
('CC1O[C@@H]1B(O)O', 'Brc1ccccc1'), ('CC1O[C@@H]1c1ccccc1',)), # keep stereo on RC
|
|
28
|
+
(('[B;D3;x2;z1:4]([O:5])([O:6])-[C;@@;h1:3]1([O;M][C;M]1)', '[Cl,Br,I;D1:1]-[C;a:2]'), ('[A:3]-[A:2]',),
|
|
29
|
+
('CC1O[C@@H]1B(O)O', 'Brc1ccccc1'), ('CC1OC1c1ccccc1',)), # drop stereo on RC
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@mark.parametrize('patterns, products, source, result', data)
|
|
34
|
+
def test_transformer(patterns, products, source, result):
|
|
35
|
+
for q, m in zip(patterns, source):
|
|
36
|
+
assert smarts(q) <= smiles(m)
|
|
37
|
+
|
|
38
|
+
reactor = Reactor([smarts(x) for x in patterns], [smarts(x) for x in products])
|
|
39
|
+
out = {format(smiles(x), 'h') for x in result}
|
|
40
|
+
assert {format(x, 'h') for x in next(reactor(*(smiles(x) for x in source))).products} == out
|
|
@@ -0,0 +1,78 @@
|
|
|
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 chython import smiles, smarts, Transformer
|
|
20
|
+
from pytest import mark
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
data = [
|
|
24
|
+
('[C:1]Br', '[A:1][O;M]', 'C[C@H](OC)CBr', 'C[C@H](OC)CO'), # keep stereo out of match
|
|
25
|
+
('[C:2][C:1]Br', '[A:2][A:1][O;M]', 'C[C@H](OC)CBr', 'C[C@H](OC)CO'), # keep stereo inside match
|
|
26
|
+
('[C;M][C;@;h1:1]([O;M])[N;M]', '[A;@@:1]', 'CC[C@H](O)N', 'CC[C@@H](O)N'), # inversion of stereo
|
|
27
|
+
('[C:1]Br', '[A:1][O;M]', 'C/C=C/CBr', 'C/C=C/CO'), # keep stereo out of match
|
|
28
|
+
('[C:1]Br', '[A:1][O;M]', 'CC=[C@]=CCBr', 'CC=[C@]=CCO'), # keep
|
|
29
|
+
('[C:1]Br', '[A:1][O;M]', 'CC=[C@]=CBr', 'CC=C=CO'), # drop stereo on RC
|
|
30
|
+
('[C:1]Br', '[A:1][I;M]', 'C/C=C/Br', 'CC=CI'), # drop stereo on RC
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@mark.parametrize('pattern, replacement, source, result', data)
|
|
35
|
+
def test_transformer(pattern, replacement, source, result):
|
|
36
|
+
transformer = Transformer(smarts(pattern), smarts(replacement))
|
|
37
|
+
|
|
38
|
+
mol = smiles(source)
|
|
39
|
+
out = {format(smiles(x), 'h') for x in ([result] if isinstance(result, str) else result)}
|
|
40
|
+
assert {format(x, 'h') for x in transformer(mol)} == out
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
# Extended stereo test cases.
|
|
44
|
+
#
|
|
45
|
+
# Extended stereo encodes epistemic state (what we know about the mixture), not geometry:
|
|
46
|
+
# AND group (+N): racemic mixture — all centers in the group share relative config
|
|
47
|
+
# OR group (-N): pure compound, unknown absolute config at each center
|
|
48
|
+
#
|
|
49
|
+
# Rules implemented in _patcher:
|
|
50
|
+
# 1. Atoms outside RC: extended stereo always preserved
|
|
51
|
+
# 2. Template overrides stereo (swap/inversion): uncertainty is unchanged, preserve reactant's ext stereo
|
|
52
|
+
# 3. Stereo lost (center destroyed or retranslation fails): ext stereo dropped (masked by property)
|
|
53
|
+
# 4. MoleculeContainer template with extended stereo: template's label takes priority
|
|
54
|
+
|
|
55
|
+
ext_data = [
|
|
56
|
+
# AND group outside RC preserved
|
|
57
|
+
('[C:1]Br', '[A:1][O;M]', 'C[C@H](CC)CBr |&1:1|', '[C@H](CO)(CC)C |&1:0|'),
|
|
58
|
+
# OR group outside RC preserved
|
|
59
|
+
('[C:1]Br', '[A:1][O;M]', 'C[C@H](CC)CBr |o1:1|', '[C@H](CO)(CC)C |o1:0|'),
|
|
60
|
+
# AND group in RC, stereo dropped
|
|
61
|
+
('[C:1]Br', '[A:1][O;M]', 'CC[C@@H](C)Br |&1:2|', 'CCC(C)O'),
|
|
62
|
+
# AND group in RC, stereo inverted — swap doesn't resolve uncertainty
|
|
63
|
+
('[C;M][C;@;h1:1]([O;M])[N;M]', '[A;@@:1]', 'CC[C@H](O)N |&1:2|', 'O[C@@H](N)CC |&1:1|'),
|
|
64
|
+
# OR group in RC, stereo inverted — swap doesn't resolve uncertainty
|
|
65
|
+
('[C;M][C;@;h1:1]([O;M])[N;M]', '[A;@@:1]', 'CC[C@H](O)N |o1:2|', 'O[C@@H](N)CC |o1:1|'),
|
|
66
|
+
# two AND members: one loses stereo in RC, other outside RC keeps label
|
|
67
|
+
('[C:1]Br', '[A:1][N;M]', 'C[C@H](F)[C@H](O)Br |&1:1,3|', 'OC(N)[C@H](C)F |&1:3|'),
|
|
68
|
+
# multiple independent AND/OR groups preserved
|
|
69
|
+
('[C:1]Br', '[A:1][O;M]', '[C@H](F)(O)[C@@H](Cl)CBr |&1:0,o1:3|',
|
|
70
|
+
'O[C@@H]([C@@H](Cl)CO)F |&1:1,o1:2|'),
|
|
71
|
+
]
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@mark.parametrize('pattern, replacement, source, expected', ext_data)
|
|
75
|
+
def test_extended_stereo(pattern, replacement, source, expected):
|
|
76
|
+
t = Transformer(smarts(pattern), smarts(replacement))
|
|
77
|
+
p = next(iter(t(smiles(source))))
|
|
78
|
+
assert p == smiles(expected)
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
#
|
|
3
|
+
# Copyright 2014-2026 Ramil Nugmanov <nougmanoff@protonmail.com>
|
|
4
|
+
# Copyright 2019 Adelia Fatykhova <adelik21979@gmail.com>
|
|
5
|
+
# This file is part of chython.
|
|
6
|
+
#
|
|
7
|
+
# chython is free software; you can redistribute it and/or modify
|
|
8
|
+
# it under the terms of the GNU Lesser General Public License as published by
|
|
9
|
+
# the Free Software Foundation; either version 3 of the License, or
|
|
10
|
+
# (at your option) any later version.
|
|
11
|
+
#
|
|
12
|
+
# This program is distributed in the hope that it will be useful,
|
|
13
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
14
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
15
|
+
# GNU Lesser General Public License for more details.
|
|
16
|
+
#
|
|
17
|
+
# You should have received a copy of the GNU Lesser General Public License
|
|
18
|
+
# along with this program; if not, see <https://www.gnu.org/licenses/>.
|
|
19
|
+
#
|
|
20
|
+
from logging import getLogger
|
|
21
|
+
from typing import Union
|
|
22
|
+
from .base import BaseReactor
|
|
23
|
+
from ..containers import QueryContainer, MoleculeContainer
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
logger = getLogger('chython.reactor')
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class Transformer(BaseReactor):
|
|
30
|
+
"""
|
|
31
|
+
Editor for molecules.
|
|
32
|
+
generates modified molecules from input molecule using template.
|
|
33
|
+
Transformer calling returns generator of all possible replacements.
|
|
34
|
+
"""
|
|
35
|
+
def __init__(self, pattern: QueryContainer, replacement: Union[MoleculeContainer, QueryContainer],
|
|
36
|
+
delete_atoms: bool = True, automorphism_filter: bool = True, canonicalize: bool = True,
|
|
37
|
+
fix_tautomers: bool = True, ignore_pyrrole_hydrogen: bool = False,
|
|
38
|
+
copy_metadata: bool = False):
|
|
39
|
+
"""
|
|
40
|
+
:param pattern: Search pattern.
|
|
41
|
+
:param replacement: Resulted structure.
|
|
42
|
+
:param delete_atoms: If True atoms exists in reactants but not exists in products will be removed.
|
|
43
|
+
:param automorphism_filter: Skip matches to same atoms.
|
|
44
|
+
:param canonicalize: Run full canonicalization on products (kekule, standardize, thiele, etc.).
|
|
45
|
+
:param fix_tautomers: Canonicalize tautomer forms. Passed to canonicalize().
|
|
46
|
+
:param ignore_pyrrole_hydrogen: Fix invalid rings like Cn1cc[nH]c1. Passed to canonicalize().
|
|
47
|
+
:param copy_metadata: Copy metadata from structure to transformed.
|
|
48
|
+
"""
|
|
49
|
+
if not isinstance(pattern, QueryContainer) or not isinstance(replacement, (MoleculeContainer, QueryContainer)):
|
|
50
|
+
raise TypeError('invalid params')
|
|
51
|
+
|
|
52
|
+
self._pattern = pattern
|
|
53
|
+
self._automorphism_filter = automorphism_filter
|
|
54
|
+
self._copy_metadata = copy_metadata
|
|
55
|
+
super().__init__(pattern, replacement, delete_atoms, canonicalize, fix_tautomers, ignore_pyrrole_hydrogen)
|
|
56
|
+
|
|
57
|
+
def __call__(self, structure: MoleculeContainer):
|
|
58
|
+
if not isinstance(structure, MoleculeContainer):
|
|
59
|
+
raise TypeError('only Molecules possible')
|
|
60
|
+
|
|
61
|
+
for mapping in self._pattern.get_mapping(structure, automorphism_filter=self._automorphism_filter):
|
|
62
|
+
try:
|
|
63
|
+
transformed = self._patcher(structure, mapping)
|
|
64
|
+
except Exception:
|
|
65
|
+
logger.info('invalid product structure, skipping')
|
|
66
|
+
continue
|
|
67
|
+
if self._copy_metadata:
|
|
68
|
+
transformed.meta.update(structure.meta)
|
|
69
|
+
yield transformed
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
__all__ = ['Transformer']
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
#
|
|
3
|
+
# Copyright 2019-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 .free_wilson import *
|
|
20
|
+
from .grid import *
|
|
21
|
+
from .retro import *
|
|
22
|
+
from ..containers.graph import Graph
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
_patched = False
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def patch_pandas():
|
|
29
|
+
"""
|
|
30
|
+
Fix pandas molecules representation.
|
|
31
|
+
"""
|
|
32
|
+
# prevent recursive patching
|
|
33
|
+
global _patched
|
|
34
|
+
if _patched:
|
|
35
|
+
return
|
|
36
|
+
_patched = True
|
|
37
|
+
|
|
38
|
+
from pandas.io.formats import printing
|
|
39
|
+
from pandas.io.formats.printing import is_sequence
|
|
40
|
+
|
|
41
|
+
def w(obj):
|
|
42
|
+
if isinstance(obj, Graph):
|
|
43
|
+
return False
|
|
44
|
+
return is_sequence(obj)
|
|
45
|
+
|
|
46
|
+
printing.is_sequence = w
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
__all__ = ['fw_prepare_groups', 'fw_decomposition_tree',
|
|
50
|
+
'grid_depict', 'GridDepict', 'retro_depict', 'RetroDepict', 'patch_pandas']
|