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
|
@@ -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
|
+
#
|