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.
Files changed (175) hide show
  1. chython/__init__.py +35 -0
  2. chython/_functions.py +95 -0
  3. chython/_java.py +57 -0
  4. chython/algorithms/__init__.py +18 -0
  5. chython/algorithms/_isomorphism.cp313-win_amd64.pyd +0 -0
  6. chython/algorithms/_isomorphism.pyx +189 -0
  7. chython/algorithms/_rings.cp313-win_amd64.pyd +0 -0
  8. chython/algorithms/_rings.pyx +478 -0
  9. chython/algorithms/aromatics/__init__.py +27 -0
  10. chython/algorithms/aromatics/_rules.py +124 -0
  11. chython/algorithms/aromatics/kekule.py +532 -0
  12. chython/algorithms/aromatics/test/__init__.py +18 -0
  13. chython/algorithms/aromatics/test/test_kekule.py +160 -0
  14. chython/algorithms/aromatics/test/test_thiele.py +263 -0
  15. chython/algorithms/aromatics/thiele.py +222 -0
  16. chython/algorithms/calculate2d/__init__.py +23 -0
  17. chython/algorithms/calculate2d/clean2d.js +1 -0
  18. chython/algorithms/calculate2d/molecule.py +191 -0
  19. chython/algorithms/calculate2d/reaction.py +75 -0
  20. chython/algorithms/conformers.py +106 -0
  21. chython/algorithms/depict.py +596 -0
  22. chython/algorithms/fingerprints/__init__.py +43 -0
  23. chython/algorithms/fingerprints/linear.py +202 -0
  24. chython/algorithms/fingerprints/morgan.py +132 -0
  25. chython/algorithms/fingerprints/test/__init__.py +18 -0
  26. chython/algorithms/fingerprints/test/test_linear.py +127 -0
  27. chython/algorithms/fingerprints/test/test_morgan.py +79 -0
  28. chython/algorithms/groups/__init__.py +212 -0
  29. chython/algorithms/groups/_functional.py +290 -0
  30. chython/algorithms/groups/_oxidations.py +70 -0
  31. chython/algorithms/groups/_protective.py +964 -0
  32. chython/algorithms/groups/_reactions.py +809 -0
  33. chython/algorithms/groups/_reductions.py +88 -0
  34. chython/algorithms/groups/_transformations.py +278 -0
  35. chython/algorithms/groups/test/__init__.py +18 -0
  36. chython/algorithms/groups/test/test_deprotection.py +108 -0
  37. chython/algorithms/groups/test/test_reactions.py +377 -0
  38. chython/algorithms/isomorphism.py +664 -0
  39. chython/algorithms/mapping/__init__.py +188 -0
  40. chython/algorithms/mapping/_groups.py +94 -0
  41. chython/algorithms/mapping/_reactions.py +72 -0
  42. chython/algorithms/mapping/attention.py +266 -0
  43. chython/algorithms/mapping/reconstruct.py +346 -0
  44. chython/algorithms/mcs.py +201 -0
  45. chython/algorithms/morgan.py +77 -0
  46. chython/algorithms/rings.py +178 -0
  47. chython/algorithms/smiles.py +597 -0
  48. chython/algorithms/standardize/__init__.py +30 -0
  49. chython/algorithms/standardize/_groups.py +958 -0
  50. chython/algorithms/standardize/_isomers.py +330 -0
  51. chython/algorithms/standardize/_metal_organics.py +189 -0
  52. chython/algorithms/standardize/_reagents.py +44 -0
  53. chython/algorithms/standardize/_salts.py +68 -0
  54. chython/algorithms/standardize/molecule.py +475 -0
  55. chython/algorithms/standardize/reaction.py +384 -0
  56. chython/algorithms/standardize/resonance.py +191 -0
  57. chython/algorithms/standardize/salts.py +128 -0
  58. chython/algorithms/standardize/saturation.py +373 -0
  59. chython/algorithms/standardize/test/__init__.py +18 -0
  60. chython/algorithms/standardize/test/test_canonicalize.py +66 -0
  61. chython/algorithms/standardize/test/test_groups.py +146 -0
  62. chython/algorithms/standardize/test/test_isomers.py +198 -0
  63. chython/algorithms/stereo.py +1215 -0
  64. chython/algorithms/tautomers/__init__.py +167 -0
  65. chython/algorithms/tautomers/_acid.py +59 -0
  66. chython/algorithms/tautomers/_base.py +116 -0
  67. chython/algorithms/tautomers/_keto_enol.py +107 -0
  68. chython/algorithms/tautomers/acid_base.py +208 -0
  69. chython/algorithms/tautomers/heteroarenes.py +103 -0
  70. chython/algorithms/tautomers/keto_enol.py +143 -0
  71. chython/algorithms/tautomers/test/__init__.py +18 -0
  72. chython/algorithms/tautomers/test/test_tautomers.py +80 -0
  73. chython/algorithms/test/__init__.py +18 -0
  74. chython/algorithms/test/test_isomorphism.py +58 -0
  75. chython/algorithms/test/test_smiles.py +76 -0
  76. chython/algorithms/x3dom.py +350 -0
  77. chython/containers/__init__.py +48 -0
  78. chython/containers/_pack_v2.cp313-win_amd64.pyd +0 -0
  79. chython/containers/_pack_v2.pyx +271 -0
  80. chython/containers/_unpack_v0v2.cp313-win_amd64.pyd +0 -0
  81. chython/containers/_unpack_v0v2.pyx +302 -0
  82. chython/containers/bonds.py +255 -0
  83. chython/containers/cgr.py +115 -0
  84. chython/containers/chimera.py +63 -0
  85. chython/containers/graph.py +199 -0
  86. chython/containers/molecule.py +817 -0
  87. chython/containers/query.py +71 -0
  88. chython/containers/rdkit.py +183 -0
  89. chython/containers/reaction.py +324 -0
  90. chython/containers/test/__init__.py +18 -0
  91. chython/containers/test/test_molecule.py +117 -0
  92. chython/containers/test/test_pack.py +60 -0
  93. chython/containers/test/test_rdkit.py +70 -0
  94. chython/containers/test/test_reaction.py +40 -0
  95. chython/exceptions.py +114 -0
  96. chython/files/MRVrw.py +532 -0
  97. chython/files/PDBrw.py +255 -0
  98. chython/files/RDFrw.py +309 -0
  99. chython/files/SDFrw.py +221 -0
  100. chython/files/__init__.py +30 -0
  101. chython/files/_convert.py +207 -0
  102. chython/files/_mapping.py +117 -0
  103. chython/files/_xyz.cp313-win_amd64.pyd +0 -0
  104. chython/files/_xyz.pyx +74 -0
  105. chython/files/daylight/__init__.py +24 -0
  106. chython/files/daylight/parser.py +157 -0
  107. chython/files/daylight/smarts.py +107 -0
  108. chython/files/daylight/smiles.py +260 -0
  109. chython/files/daylight/test/__init__.py +18 -0
  110. chython/files/daylight/test/test_daylight_smarts.py +289 -0
  111. chython/files/daylight/test/test_daylight_smiles.py +101 -0
  112. chython/files/daylight/test/test_parser.py +118 -0
  113. chython/files/daylight/test/test_tokenize.py +77 -0
  114. chython/files/daylight/tokenize.py +376 -0
  115. chython/files/libinchi/__init__.py +22 -0
  116. chython/files/libinchi/libinchi.dll +0 -0
  117. chython/files/libinchi/wrapper.py +554 -0
  118. chython/files/mdl/__init__.py +25 -0
  119. chython/files/mdl/emol.py +251 -0
  120. chython/files/mdl/erxn.py +67 -0
  121. chython/files/mdl/mol.py +186 -0
  122. chython/files/mdl/read.py +223 -0
  123. chython/files/mdl/rxn.py +67 -0
  124. chython/files/mdl/stereo.py +63 -0
  125. chython/files/mdl/write.py +201 -0
  126. chython/files/opsin.py +35 -0
  127. chython/files/test/__init__.py +0 -0
  128. chython/files/test/test_inchi.py +27 -0
  129. chython/files/test/test_mrv.py +47 -0
  130. chython/files/test/test_sdf_rdf.py +64 -0
  131. chython/files/test/test_xyz.py +20 -0
  132. chython/files/xyz.py +78 -0
  133. chython/periodictable/__init__.py +69 -0
  134. chython/periodictable/base/__init__.py +25 -0
  135. chython/periodictable/base/dynamic.py +149 -0
  136. chython/periodictable/base/element.py +451 -0
  137. chython/periodictable/base/groups.py +90 -0
  138. chython/periodictable/base/periods.py +46 -0
  139. chython/periodictable/base/query.py +473 -0
  140. chython/periodictable/base/vector.py +119 -0
  141. chython/periodictable/groupI.py +252 -0
  142. chython/periodictable/groupII.py +223 -0
  143. chython/periodictable/groupIII.py +1154 -0
  144. chython/periodictable/groupIV.py +210 -0
  145. chython/periodictable/groupIX.py +196 -0
  146. chython/periodictable/groupV.py +192 -0
  147. chython/periodictable/groupVI.py +183 -0
  148. chython/periodictable/groupVII.py +162 -0
  149. chython/periodictable/groupVIII.py +161 -0
  150. chython/periodictable/groupX.py +166 -0
  151. chython/periodictable/groupXI.py +164 -0
  152. chython/periodictable/groupXII.py +158 -0
  153. chython/periodictable/groupXIII.py +239 -0
  154. chython/periodictable/groupXIV.py +262 -0
  155. chython/periodictable/groupXV.py +276 -0
  156. chython/periodictable/groupXVI.py +457 -0
  157. chython/periodictable/groupXVII.py +324 -0
  158. chython/periodictable/groupXVIII.py +260 -0
  159. chython/periodictable/test/__init__.py +0 -0
  160. chython/periodictable/test/test_element.py +100 -0
  161. chython/reactor/__init__.py +24 -0
  162. chython/reactor/base.py +222 -0
  163. chython/reactor/reactor.py +177 -0
  164. chython/reactor/test/__init__.py +18 -0
  165. chython/reactor/test/test_reactor.py +40 -0
  166. chython/reactor/test/test_transformer.py +78 -0
  167. chython/reactor/transformer.py +72 -0
  168. chython/utils/__init__.py +50 -0
  169. chython/utils/free_wilson.py +114 -0
  170. chython/utils/grid.py +130 -0
  171. chython/utils/retro.py +139 -0
  172. chython-2.19.dist-info/METADATA +105 -0
  173. chython-2.19.dist-info/RECORD +175 -0
  174. chython-2.19.dist-info/WHEEL +4 -0
  175. chython-2.19.dist-info/licenses/LICENSE +165 -0
@@ -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']