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,71 @@
1
+ # -*- coding: utf-8 -*-
2
+ #
3
+ # Copyright 2018-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 typing import Tuple, Union
20
+ from .bonds import Bond, QueryBond
21
+ from .graph import Graph
22
+ from ..algorithms.isomorphism import QueryIsomorphism
23
+ from ..periodictable import Element, QueryElement
24
+ from ..periodictable.base import Query
25
+
26
+
27
+ class QueryContainer(Graph[Query, QueryBond], QueryIsomorphism):
28
+ __slots__ = ('_smarts',)
29
+
30
+ def __init__(self, smarts: str):
31
+ super().__init__()
32
+ self._smarts = smarts
33
+
34
+ def __str__(self):
35
+ return self._smarts
36
+
37
+ def __repr__(self):
38
+ return f'smarts({self._smarts})'
39
+
40
+ def add_atom(self, atom: Union[Query, Element, int, str], *args, **kwargs):
41
+ if not isinstance(atom, Query):
42
+ # set only basic labels: charge, radical, isotope. use Query object directly for the full control.
43
+ if isinstance(atom, Element):
44
+ atom = QueryElement.from_atom(atom)
45
+ elif isinstance(atom, str):
46
+ atom = QueryElement.from_symbol(atom)()
47
+ elif isinstance(atom, int):
48
+ atom = QueryElement.from_atomic_number(atom)()
49
+ else:
50
+ raise TypeError('QueryElement object expected')
51
+ return super().add_atom(atom, *args, **kwargs)
52
+
53
+ def add_bond(self, n, m, bond: Union[QueryBond, Bond, int, Tuple[int, ...]]):
54
+ if isinstance(bond, Bond):
55
+ bond = QueryBond.from_bond(bond)
56
+ elif not isinstance(bond, QueryBond):
57
+ bond = QueryBond(bond)
58
+ super().add_bond(n, m, bond)
59
+
60
+ def union(self, other: 'QueryContainer', *, remap: bool = False, copy: bool = True) -> 'QueryContainer':
61
+ if not isinstance(other, QueryContainer):
62
+ raise TypeError('QueryContainer expected')
63
+ return super().union(other, remap=remap, copy=copy)
64
+
65
+ def copy(self):
66
+ copy = super().copy()
67
+ copy._smarts = self._smarts
68
+ return copy
69
+
70
+
71
+ __all__ = ['QueryContainer']
@@ -0,0 +1,183 @@
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 typing import TYPE_CHECKING, Type
20
+ from ..periodictable import Element
21
+
22
+
23
+ if TYPE_CHECKING:
24
+ from chython import MoleculeContainer
25
+
26
+
27
+ class RDkit:
28
+ __slots__ = ()
29
+
30
+ @classmethod
31
+ def from_rdkit(cls: Type['MoleculeContainer'], data):
32
+ """
33
+ RDKit molecule object to MoleculeContainer converter
34
+ """
35
+ from rdkit.Chem import BondStereo, BondType, ChiralType
36
+
37
+ _rdkit_bond_map = {BondType.SINGLE: 1, BondType.DOUBLE: 2, BondType.TRIPLE: 3, BondType.AROMATIC: 4,
38
+ BondType.ZERO: 8, BondType.UNSPECIFIED: 8, BondType.DATIVE: 8}
39
+ _chiral_cw = ChiralType.CHI_TETRAHEDRAL_CW
40
+ _chiral_ccw = ChiralType.CHI_TETRAHEDRAL_CCW
41
+ _trans = BondStereo.STEREOE
42
+ _cis = BondStereo.STEREOZ
43
+
44
+ mol = cls()
45
+
46
+ mapping = {}
47
+ tetrahedron_stereo = []
48
+ for ra in data.GetAtoms():
49
+ e = Element.from_symbol(ra.GetSymbol())
50
+ a = e(ra.GetIsotope() or None, charge=ra.GetFormalCharge(), is_radical=bool(ra.GetNumRadicalElectrons()),
51
+ parsed_mapping=ra.GetAtomMapNum(), implicit_hydrogens=ra.GetNumExplicitHs() + ra.GetNumImplicitHs())
52
+ mapping[ra.GetIdx()] = mol.add_atom(a, _skip_calculation=True)
53
+ s = ra.GetChiralTag()
54
+ if s in (_chiral_cw, _chiral_ccw):
55
+ tetrahedron_stereo.append((ra.GetIdx(), [x.GetIdx() for x in ra.GetNeighbors()], s == _chiral_ccw))
56
+
57
+ cis_trans_stereo = []
58
+ for b in data.GetBonds():
59
+ n, m = mapping[b.GetBeginAtomIdx()], mapping[b.GetEndAtomIdx()]
60
+ mol.add_bond(n, m, _rdkit_bond_map[b.GetBondType()], _skip_calculation=True)
61
+ s = b.GetStereo()
62
+ if s in (_cis, _trans):
63
+ nn, nm = b.GetStereoAtoms()
64
+ cis_trans_stereo.append((n, m, mapping[nn], mapping[nm], s == _cis))
65
+
66
+ if cs := data.GetConformers():
67
+ # set coordinates from the first rdkit conformer. usually it's 2d layout
68
+ for (_, atom), (x, y, _) in zip(mol.atoms(), cs[0].GetPositions()):
69
+ atom.xy = (x, y)
70
+
71
+ conformers = []
72
+ for c in cs:
73
+ if c.Is3D():
74
+ conformers.append({n: tuple(v) for n, v in enumerate(c.GetPositions(), 1)})
75
+ if conformers:
76
+ mol._conformers = conformers
77
+
78
+ # move stereo labels as is
79
+ for n, env, s in tetrahedron_stereo:
80
+ n = mapping[n]
81
+ try:
82
+ mol.atom(n)._stereo = mol._translate_tetrahedron_sign(n, [mapping[x] for x in env], s)
83
+ except KeyError:
84
+ pass
85
+ for n, m, nn, nm, s in cis_trans_stereo:
86
+ try:
87
+ mol.bond(n, m)._stereo = mol._translate_cis_trans_sign(n, m, nn, nm, s)
88
+ except KeyError:
89
+ pass
90
+
91
+ mol.fix_structure(recalculate_hydrogens=False)
92
+ if tetrahedron_stereo or cis_trans_stereo:
93
+ mol.fix_stereo()
94
+ return mol
95
+
96
+ def to_rdkit(self: 'MoleculeContainer', *, keep_mapping=True, keep_hydrogens=True):
97
+ """
98
+ Convert into RDKit molecule object
99
+
100
+ :param keep_mapping: set atom numbers
101
+ :param keep_hydrogens: set implicit hydrogens
102
+ """
103
+ from rdkit.Chem import (AssignStereochemistry, Atom, BondStereo, BondType, ChiralType,
104
+ Conformer, RWMol, SanitizeMol)
105
+
106
+ _bond_map = {1: BondType.SINGLE, 2: BondType.DOUBLE, 3: BondType.TRIPLE,
107
+ 4: BondType.AROMATIC, 8: BondType.DATIVE}
108
+
109
+ _chiral_cw = ChiralType.CHI_TETRAHEDRAL_CW
110
+ _chiral_ccw = ChiralType.CHI_TETRAHEDRAL_CCW
111
+ _trans = BondStereo.STEREOE
112
+ _cis = BondStereo.STEREOZ
113
+
114
+ mol = RWMol()
115
+ mapping = {}
116
+
117
+ for n, a in self.atoms():
118
+ ra = Atom(a.atomic_number)
119
+ if keep_hydrogens and a.implicit_hydrogens is not None:
120
+ ra.SetNumExplicitHs(a.implicit_hydrogens)
121
+ if keep_mapping:
122
+ ra.SetAtomMapNum(n)
123
+ if a.charge:
124
+ ra.SetFormalCharge(a.charge)
125
+ if a.isotope:
126
+ ra.SetIsotope(a.isotope)
127
+ if a.is_radical:
128
+ ra.SetNumRadicalElectrons(1)
129
+ mapping[n] = mol.AddAtom(ra)
130
+
131
+ inverted = {v: k for k, v in mapping.items()}
132
+
133
+ for n, m, b in self.bonds():
134
+ if self.atom(n).atomic_symbol not in _inorganic:
135
+ n, m = m, n # fix direction of dative bond
136
+ mol.AddBond(mapping[n], mapping[m], _bond_map[b.order])
137
+
138
+ for n, a in self.atoms():
139
+ if a.stereo is None:
140
+ continue
141
+ if n not in self.stereogenic_tetrahedrons:
142
+ continue # allenes are not supported
143
+ ra = mol.GetAtomWithIdx(mapping[n])
144
+ env = [inverted[x.GetIdx()] for x in ra.GetNeighbors()]
145
+ s = self._translate_tetrahedron_sign(n, env)
146
+ ra.SetChiralTag(_chiral_ccw if s else _chiral_cw)
147
+
148
+ for n, m, b in self.bonds():
149
+ if b.stereo is None:
150
+ continue
151
+ # check for simple cis-trans
152
+ nm = self._stereo_cis_trans_centers.get(n)
153
+ if nm is None or n not in nm or m not in nm:
154
+ continue
155
+
156
+ n1, m1, *_ = self.stereogenic_cis_trans[nm]
157
+ rb = mol.GetBondBetweenAtoms(mapping[n], mapping[m])
158
+ rb.SetStereoAtoms(mapping[n1], mapping[m1])
159
+ rb.SetStereo(_cis if b.stereo else _trans)
160
+
161
+ conf = Conformer()
162
+ for n, a in self.atoms():
163
+ conf.SetAtomPosition(mapping[n], (a.x, a.y, 0))
164
+ conf.Set3D(False)
165
+ mol.AddConformer(conf, assignId=True)
166
+
167
+ if hasattr(self, '_conformers'):
168
+ for c in self._conformers:
169
+ conf = Conformer()
170
+ for n, xyz in c.items():
171
+ conf.SetAtomPosition(mapping[n], xyz)
172
+ mol.AddConformer(conf, assignId=True)
173
+
174
+ SanitizeMol(mol)
175
+ AssignStereochemistry(mol, flagPossibleStereoCenters=True, force=True)
176
+ return mol
177
+
178
+
179
+ _inorganic = {'He', 'Ne', 'Ar', 'Kr', 'Xe', 'F', 'Cl', 'Br', 'I', 'C', 'N', 'O',
180
+ 'H', 'Si', 'P', 'S', 'Se', 'Ge', 'As', 'Sb', 'Te'}
181
+
182
+
183
+ __all__ = ['RDkit']
@@ -0,0 +1,324 @@
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 functools import reduce
20
+ from itertools import chain
21
+ from math import ceil
22
+ from operator import itemgetter, or_
23
+ from typing import Dict, Iterator, Optional, Tuple, List, Sequence
24
+ from zlib import compress, decompress
25
+ from .cgr import CGRContainer
26
+ from .molecule import MoleculeContainer
27
+ from ..algorithms.calculate2d import Calculate2DReaction
28
+ from ..algorithms.depict import DepictReaction
29
+ from ..algorithms.mapping import Mapping
30
+ from ..algorithms.standardize import StandardizeReaction
31
+ from .._functions import cached_method
32
+
33
+
34
+ class ReactionContainer(StandardizeReaction, Mapping, Calculate2DReaction, DepictReaction):
35
+ """
36
+ Reaction storage. Contains reactants, products and reagents lists.
37
+
38
+ Reaction storage hashable and comparable. based on reaction unique signature (SMILES).
39
+ """
40
+ __slots__ = ('_reactants', '_products', '_reagents', '_meta', '_name', '_arrow', '_signs', '__dict__')
41
+
42
+ def __init__(self, reactants: Sequence[MoleculeContainer] = (), products: Sequence[MoleculeContainer] = (),
43
+ reagents: Sequence[MoleculeContainer] = (), meta: Optional[Dict] = None, name: Optional[str] = None):
44
+ """
45
+ New reaction object creation
46
+
47
+ :param reactants: list of MoleculeContainers in left side of reaction
48
+ :param products: right side of reaction. see reactants
49
+ :param reagents: middle side of reaction: solvents, catalysts, etc. see reactants
50
+ :param meta: dictionary of metadata. like DTYPE-DATUM in RDF
51
+
52
+ """
53
+ reactants = tuple(reactants)
54
+ products = tuple(products)
55
+ reagents = tuple(reagents)
56
+ if not reactants and not products and not reagents:
57
+ raise ValueError('At least one graph object required')
58
+ elif not all(isinstance(x, MoleculeContainer) for x in chain(reactants, products, reagents)):
59
+ raise TypeError('MoleculeContainers expected')
60
+
61
+ self._reactants = reactants
62
+ self._products = products
63
+ self._reagents = reagents
64
+ if meta is None:
65
+ self._meta = None
66
+ else:
67
+ self._meta = dict(meta)
68
+ if name is None:
69
+ self._name = None
70
+ else:
71
+ self._name = name
72
+ self._arrow = None
73
+ self._signs = None
74
+
75
+ @property
76
+ def reactants(self) -> Tuple[MoleculeContainer, ...]:
77
+ return self._reactants
78
+
79
+ @property
80
+ def reagents(self) -> Tuple[MoleculeContainer, ...]:
81
+ return self._reagents
82
+
83
+ @property
84
+ def products(self) -> Tuple[MoleculeContainer, ...]:
85
+ return self._products
86
+
87
+ def molecules(self) -> Iterator[MoleculeContainer]:
88
+ """
89
+ Iterator of all reaction molecules
90
+ """
91
+ return chain(self.reactants, self.reagents, self.products)
92
+
93
+ @property
94
+ def meta(self) -> Dict:
95
+ """
96
+ Dictionary of metadata.
97
+ Like DTYPE-DATUM in RDF
98
+ """
99
+ if self._meta is None:
100
+ self._meta = {} # lazy
101
+ return self._meta
102
+
103
+ @property
104
+ def name(self) -> str:
105
+ return self._name or ''
106
+
107
+ @name.setter
108
+ def name(self, name: str):
109
+ if not isinstance(name, str):
110
+ raise TypeError('name should be string up to 80 symbols')
111
+ self._name = name
112
+
113
+ def copy(self) -> 'ReactionContainer':
114
+ """
115
+ Get copy of object
116
+ """
117
+ copy = object.__new__(self.__class__)
118
+ copy._reactants = tuple(x.copy() for x in self.reactants)
119
+ copy._products = tuple(x.copy() for x in self.products)
120
+ copy._reagents = tuple(x.copy() for x in self.reagents)
121
+ copy._name = self._name
122
+ if self._meta is None:
123
+ copy._meta = None
124
+ else:
125
+ copy._meta = self._meta.copy()
126
+ copy._arrow = self._arrow
127
+ copy._signs = self._signs
128
+ return copy
129
+
130
+ def compose(self, *, dynamic=True) -> CGRContainer:
131
+ """
132
+ Get CGR of reaction
133
+
134
+ Reagents will be presented as unchanged molecules
135
+ :return: CGRContainer
136
+ """
137
+ rr = self.reagents + self.reactants
138
+ if rr:
139
+ r = reduce(or_, rr)
140
+ else:
141
+ r = MoleculeContainer()
142
+ if self.products:
143
+ p = reduce(or_, self.products)
144
+ else:
145
+ p = MoleculeContainer()
146
+ return r.compose(p, dynamic)
147
+
148
+ def flush_cache(self, keep_molecule_cache=False, **kwargs):
149
+ self.__dict__.clear()
150
+ if not keep_molecule_cache:
151
+ for m in self.molecules():
152
+ m.flush_cache(**kwargs)
153
+
154
+ def pack(self, *, compressed=True, check=True) -> bytes:
155
+ """
156
+ Pack into compressed bytes.
157
+
158
+ Note:
159
+ * Same restrictions as in molecules pack.
160
+ * reactants, reagents nad products should contain less than 256 molecules.
161
+
162
+ Format specification:
163
+ Big endian bytes order
164
+ 8 bit - header byte = 0x01 (current format specification)
165
+ 8 bit - reactants count
166
+ 8 bit - reagents count
167
+ 8 bit - products count
168
+ x bit - concatenated molecules packs
169
+
170
+ :param compressed: return zlib-compressed pack.
171
+ :param check: check molecules for format restrictions.
172
+ """
173
+ data = b''.join((bytearray((1, len(self.reactants), len(self.reagents), len(self.products))),
174
+ *(m.pack(compressed=False, check=check) for m in self.molecules())))
175
+ if compressed:
176
+ return compress(data, 9)
177
+ return data
178
+
179
+ def pach(self, *, compressed=True, check=True) -> bytes:
180
+ """
181
+ Pack into compressed bytes.
182
+ """
183
+ return self.pack(compressed=compressed, check=check)
184
+
185
+ @classmethod
186
+ def pack_len(cls, data: bytes, /, *, compressed=True) -> Tuple[List[int], List[int], List[int]]:
187
+ """
188
+ Returns reactants, reagents, products molecules atoms count in reaction pack.
189
+ """
190
+ if compressed:
191
+ data = decompress(data)
192
+ data = memoryview(data)
193
+ if data[0] != 1:
194
+ raise ValueError('invalid pack header')
195
+ reactants, reagents, products = data[1], data[2], data[3]
196
+
197
+ v = data[4] # mol pack version
198
+ shift = 5 # RH+RC+RC+PC+MH
199
+ molecules = []
200
+ for _ in range(reactants + reagents + products - 1):
201
+ acs = int.from_bytes(data[shift: shift + 3], 'big')
202
+ neighbors = 0
203
+ ac = acs >> 12
204
+ shift += 4 # AC+CC+AN
205
+ for _ in range(ac):
206
+ neighbors += data[shift] & 0x0f
207
+ shift += 9
208
+ neighbors //= 2
209
+ if v == 2:
210
+ shift += 3 * neighbors + ceil(neighbors * 3 / 8) + (acs & 0x0fff) * 4
211
+ elif v == 0:
212
+ shift += 3 * neighbors + ceil(neighbors / 5) * 2 + (acs & 0x0fff) * 4
213
+ molecules.append(ac)
214
+ if reactants or reagents or products:
215
+ molecules.append(int.from_bytes(data[shift: shift + 3], 'big') >> 12)
216
+ return molecules[:reactants], molecules[reactants: -products], molecules[-products:]
217
+
218
+ @classmethod
219
+ def unpack(cls, data: bytes, /, *, compressed=True) -> 'ReactionContainer':
220
+ """
221
+ Unpack from compressed bytes.
222
+
223
+ :param compressed: decompress data before processing.
224
+ """
225
+ if compressed:
226
+ data = decompress(data)
227
+ data = memoryview(data)
228
+ if data[0] != 1:
229
+ raise ValueError('invalid pack header')
230
+
231
+ reactants, reagents, products = data[1], data[2], data[3]
232
+ molecules: List[MoleculeContainer] = []
233
+ shift = 4
234
+ for _ in range(reactants + reagents + products):
235
+ m, pl = MoleculeContainer.unpack(data[shift:], compressed=False, _return_pack_length=True)
236
+ molecules.append(m)
237
+ shift += pl
238
+ return cls(molecules[:reactants], molecules[-products:], molecules[reactants: -products])
239
+
240
+ @classmethod
241
+ def unpach(cls, data: bytes, /, *, compressed=True) -> 'ReactionContainer':
242
+ """
243
+ Unpack from compressed bytes.
244
+ """
245
+ return cls.unpack(data, compressed=compressed)
246
+
247
+ def __bytes__(self):
248
+ return self.pack()
249
+
250
+ @cached_method
251
+ def __invert__(self) -> CGRContainer:
252
+ """
253
+ Get CGR of reaction
254
+ """
255
+ return self.compose()
256
+
257
+ def __eq__(self, other):
258
+ return isinstance(other, ReactionContainer) and str(self) == str(other)
259
+
260
+ @cached_method
261
+ def __hash__(self):
262
+ return hash(str(self))
263
+
264
+ def __bool__(self):
265
+ """
266
+ Exists both reactants and products
267
+ """
268
+ return bool(self.reactants and self.products)
269
+
270
+ @cached_method
271
+ def __str__(self):
272
+ return format(self)
273
+
274
+ def __format__(self, format_spec):
275
+ """
276
+ :param format_spec:
277
+ !c - Keep nested containers order.
278
+ a - Generate asymmetric closures.
279
+ !s - Disable stereo marks.
280
+ A - Use aromatic bonds instead aromatic atoms.
281
+ m - Set atom mapping.
282
+ r - Generate random-ordered smiles.
283
+ h - Show implicit hydrogens.
284
+ !b - Disable bonds tokens.
285
+ !x - Disable CXSMILES extension.
286
+ !z - Disable charge representation.
287
+ """
288
+ sig = []
289
+ count = 0
290
+ contract = []
291
+ radicals = []
292
+
293
+ for ml in (self.reactants, self.reagents, self.products):
294
+ mso = [(m, *m.__format__(format_spec, _return_order=True)) for m in ml]
295
+ if not format_spec or '!c' not in format_spec:
296
+ mso.sort(key=itemgetter(1))
297
+
298
+ ss = []
299
+ for m, s, o in mso:
300
+ if m.connected_components_count > 1:
301
+ contract.append([str(x + count) for x in range(m.connected_components_count)])
302
+ count += m.connected_components_count
303
+ else:
304
+ count += 1
305
+
306
+ radicals.extend(m.atom(n).is_radical for n in o)
307
+ ss.append(s)
308
+ sig.append('.'.join(ss))
309
+
310
+ if not format_spec or '!x' not in format_spec:
311
+ cx = []
312
+ if r := ','.join(str(n) for n, r in enumerate(radicals) if r):
313
+ cx.append(f'^1:{r}')
314
+ if contract:
315
+ cx.append(f"f:{','.join('.'.join(x) for x in contract)}")
316
+ if cx:
317
+ return f"{'>'.join(sig)} |{','.join(cx)}|"
318
+ return '>'.join(sig)
319
+
320
+ def __len__(self):
321
+ return len(self.reactants) + len(self.products) + len(self.reagents)
322
+
323
+
324
+ __all__ = ['ReactionContainer']
@@ -0,0 +1,18 @@
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
+ #