diff --git a/chython/__init__.py b/chython/__init__.py
index 97868a7a..803ae753 100644
--- a/chython/__init__.py
+++ b/chython/__init__.py
@@ -18,6 +18,7 @@
# You should have received a copy of the GNU Lesser General Public License
# along with this program; if not, see .
#
+from os import getenv
from typing import Literal
from .algorithms.depict import depict_settings
from .containers import *
@@ -27,8 +28,9 @@
torch_device = 'cpu' # AAM model device. Change before first `reset_mapping` call!
-clean2d_engine: Literal['smilesdrawer', 'rdkit'] = 'smilesdrawer'
+clean2d_engine: Literal['rdkit', 'smilesdrawer', 'cdk', 'obabel', 'indigo'] = 'smilesdrawer'
conformer_engine: Literal['rdkit', 'cdpkit'] = 'rdkit'
+class_paths = [getenv('CDK_PATH', 'cdk.jar'), getenv('OPSIN_PATH', 'opsin.jar')]
__all__ = []
diff --git a/chython/algorithms/calculate2d/molecule.py b/chython/algorithms/calculate2d/molecule.py
index 6536b1f1..282451e1 100644
--- a/chython/algorithms/calculate2d/molecule.py
+++ b/chython/algorithms/calculate2d/molecule.py
@@ -47,7 +47,7 @@ class Calculate2DMolecule:
_bonds: Dict[int, Dict[int, 'Bond']]
def clean2d(self: Union['MoleculeContainer', 'Calculate2DMolecule'],
- *, engine: Literal['rdkit', 'smilesdrawer'] = None):
+ *, engine: Literal['rdkit', 'smilesdrawer', 'cdk', 'obabel', 'indigo'] = None):
"""
Calculate 2d layout of graph.
@@ -85,6 +85,31 @@ def clean2d(self: Union['MoleculeContainer', 'Calculate2DMolecule'],
shift_x, shift_y = xy[0]
for n, (x, y) in zip(order, xy):
plane[n] = (x - shift_x, shift_y - y)
+ elif engine == 'cdk':
+ sdg = self._cdk_engine.layout.StructureDiagramGenerator()
+ sdg.setUseTemplates(False)
+ sdg.setMolecule(self.to_cdk())
+ sdg.generateCoordinates()
+ mol = sdg.getMolecule()
+
+ for i, n in enumerate(self.smiles_atoms_order):
+ xy = mol.getAtom(i).getPoint2d()
+ plane[n] = (xy.x, xy.y)
+ elif engine == 'obabel':
+ mol = self.to_openbabel()
+ assert self._obgen2d(mol), 'OpenBabel failed to generate 2d layout'
+ assert mol.NumAtoms() == len(self), 'OpenBabel modified molecule'
+
+ for i, n in enumerate(self.smiles_atoms_order, 1):
+ xy = mol.GetAtom(i).GetVector()
+ plane[n] = (xy.GetX(), xy.GetY())
+ elif engine == 'indigo':
+ mol = self.to_indigo()
+ assert not mol.layout(), 'Indigo failed to generate 2d layout'
+
+ for n, a in zip(self.smiles_atoms_order, mol.iterateAtoms()):
+ x, y, _ = a.xyz()
+ plane[n] = (x, y)
else: raise ValueError(f'Invalid clean2d engine: {engine}')
bonds = []
diff --git a/chython/algorithms/depict.py b/chython/algorithms/depict.py
index 46e8b20e..6e45fce0 100644
--- a/chython/algorithms/depict.py
+++ b/chython/algorithms/depict.py
@@ -55,7 +55,7 @@
'span_size': .35, 'other_size': 0.3, 'monochrome': False, 'bond_color': 'black', 'bond_width': .04,
'other_color': 'black', 'bond_radius': .02, 'atom_radius': -.2, 'mapping_size': .25,
'atoms_colors': cpk, 'triple_space': .13, 'double_space': .06, 'mapping_color': '#0305A7',
- 'aromatic_space': .14, 'aromatic_dashes': (.15, .05), 'dx_m': .05, 'dy_m': .2,
+ 'aromatic_space': .14, 'aromatic_dashes': (.15, .05), 'dx_m': .05, 'dy_m': .2, 'dx_s': .05, 'dy_s': .1,
'other_font_style': 'monospace', 'dx_ci': .05, 'dy_ci': 0.2, 'symbols_font_style': 'sans-serif',
'mapping_font_style': 'monospace', 'wedge_space': .08, 'arrow_color': 'black'}
@@ -164,11 +164,12 @@ def depict_settings(*, carbon: bool = False, aam: bool = True, monochrome: bool
bond_color: str = 'black', aam_color: str = '#0305A7', atoms_colors: tuple = cpk,
bond_width: float = .04, wedge_space: float = .08, dashes: Tuple[float, float] = (.2, .1),
aromatic_dashes: Tuple[float, float] = (.15, .05), dx_ci: float = .05, dy_ci: float = .2,
- dx_m: float = .05, dy_m: float = .2, span_dy: float = .15, double_space: float = .06,
- triple_space: float = .13, aromatic_space: float = .14, atom_radius: float = .2, bond_radius=.02,
- font_size: float = .5, other_size: float = .3, span_size: float = .35, aam_size: float = .25,
- symbols_font_style: str = 'sans-serif', other_font_style: str = 'monospace',
- other_color: str = 'black', arrow_color: str = 'black', mapping_font_style: str = 'monospace'):
+ dx_m: float = .05, dy_m: float = .2, dx_s: float = .05, dy_s: float = .1, span_dy: float = .15,
+ double_space: float = .06, triple_space: float = .13, aromatic_space: float = .14,
+ atom_radius: float = .2, bond_radius=.02, font_size: float = .5, other_size: float = .3,
+ span_size: float = .35, aam_size: float = .25, symbols_font_style: str = 'sans-serif',
+ other_font_style: str = 'monospace', other_color: str = 'black', arrow_color: str = 'black',
+ mapping_font_style: str = 'monospace'):
"""
Settings for depict of chemical structures
@@ -198,6 +199,8 @@ def depict_settings(*, carbon: bool = False, aam: bool = True, monochrome: bool
:param dy_ci: y-axis offset relative to the center of the atom symbol for radical, charges, isotope
:param dx_m: x-axis offset relative to the center of the atom symbol for atom-to-atom mapping
:param dy_m: y-axis offset relative to the center of the atom symbol for atom-to-atom mapping
+ :param dx_s: x-axis offset relative to the center of the atom symbol for extended stereo label
+ :param dy_s: y-axis offset relative to the center of the atom symbol for extended stereo label
:param span_dy: y-axis offset relative to the center of the atom symbol for hydrogen count
:param mapping_font_style: font style for mapping
:param wedge_space: wedge bond width
@@ -226,6 +229,7 @@ def depict_settings(*, carbon: bool = False, aam: bool = True, monochrome: bool
_render_config['dx_m'], _render_config['dy_m'] = dx_m, dy_m
_render_config['other_font_style'] = other_font_style
_render_config['dx_ci'], _render_config['dy_ci'] = dx_ci, dy_ci
+ _render_config['dx_s'], _render_config['dy_s'] = dx_s, dy_s
_render_config['symbols_font_style'] = symbols_font_style
_render_config['mapping_font_style'] = mapping_font_style
_render_config['wedge_space'] = wedge_space
@@ -311,7 +315,7 @@ def __render_bonds(self: Union['MoleculeContainer', 'DepictMolecule']):
nx, ny = atoms[n].xy
mx, my = atoms[m].xy
ny, my = -ny, -my
- dx, dy = _rotate_vector(0, wedge_space, mx - nx, ny - my)
+ dx, dy = _rotate_vector(0, wedge_space, mx - nx, my - ny)
svg.append(f' ')
@@ -368,6 +372,7 @@ def __render_atoms(self: 'MoleculeContainer', uid):
mapping_size = _render_config['mapping_size']
dx_m, dy_m = _render_config['dx_m'], _render_config['dy_m']
dx_ci, dy_ci = _render_config['dx_ci'], _render_config['dy_ci']
+ dx_s, dy_s = _render_config['dx_s'], _render_config['dy_s']
symbols_font_style = _render_config['symbols_font_style']
span_dy = _render_config['span_dy']
other_font_style = _render_config['other_font_style']
@@ -412,6 +417,10 @@ def __render_atoms(self: 'MoleculeContainer', uid):
if atom.isotope:
others.append(f' {atom.isotope}')
+ if atom.stereo is not None and atom.extended_stereo:
+ label = f'&{atom.extended_stereo}' if atom.extended_stereo > 0 else f'o{-atom.extended_stereo}'
+ others.append(f' '
+ f'{label}')
if len(symbol) > 1:
dx = font7
@@ -448,8 +457,13 @@ def __render_atoms(self: 'MoleculeContainer', uid):
if mapping:
maps.append(f' {n}')
- elif mapping:
- maps.append(f' {n}')
+ else:
+ if mapping:
+ maps.append(f' {n}')
+ if atom.stereo is not None and atom.extended_stereo:
+ label = f'&{atom.extended_stereo}' if atom.extended_stereo > 0 else f'o{-atom.extended_stereo}'
+ others.append(f' '
+ f'{label}')
if svg: # group atoms symbols
if fill_zone:
diff --git a/chython/algorithms/standardize/_groups.py b/chython/algorithms/standardize/_groups.py
index 58a5d52d..828a3c67 100644
--- a/chython/algorithms/standardize/_groups.py
+++ b/chython/algorithms/standardize/_groups.py
@@ -553,6 +553,11 @@ def _rules_single():
bonds_fix = ((1, 2, 1), (2, 3, 2))
rules.append((q, atom_fix, bonds_fix, True))
+ q = smarts('[O,S;D1;z2;x0]=[C;D3;r5]1[N;D2;z1][A;z2]=[N][N;z1]1')
+ atom_fix = {}
+ bonds_fix = ((1, 2, 1), (2, 3, 2))
+ rules.append((q, atom_fix, bonds_fix, True))
+
#
# fix pyraz-imin
#
diff --git a/chython/algorithms/standardize/test/test_groups.py b/chython/algorithms/standardize/test/test_groups.py
index b44e3af5..c34cc505 100644
--- a/chython/algorithms/standardize/test/test_groups.py
+++ b/chython/algorithms/standardize/test/test_groups.py
@@ -76,6 +76,7 @@
('C=C(O)O', 'CC(=O)O'), ('C=C(O)N', 'CC(=O)N'),
('OC=C', 'O=CC'), ('OC(C)=C', 'O=C(C)C'),
('O=C1N=CC=CC1', 'OC=1N=CC=CC=1'), ('OC=1N=CC=CC=1', 'OC=1N=CC=CC=1'), ('N=C1N=CC=CC1', 'NC=1N=CC=CC=1'),
+ ('CN1N=CNC1=O', 'CN1N=CN=C1O'), ('CN1N=CN=C1O', 'CN1N=CN=C1O'),
('S=C1NC=CN1', 'S=C1NC=CN1'), ('SC1=NC=CN1', 'S=C1NC=CN1'), ('S=C1NCCN1', 'S=C1NCCN1'), ('SC1=NCCN1', 'S=C1NCCN1'),
('CN=C1NC=CN1', 'CNC1=NC=CN1'), ('CNC1=NC=CN1', 'CNC1=NC=CN1'), ('CN=C1NCCN1', 'CNC1=NCCN1'), ('CNC1=NCCN1', 'CNC1=NCCN1'),
('S=C1NNC=C1', 'SC1=NNC=C1'), ('SC1=NNC=C1', 'SC1=NNC=C1'), ('S=C1NNCC1', 'S=C1NNCC1'), ('SC1=NNCC1', 'S=C1NNCC1'),
diff --git a/chython/containers/chimera.py b/chython/containers/chimera.py
new file mode 100644
index 00000000..0200a82c
--- /dev/null
+++ b/chython/containers/chimera.py
@@ -0,0 +1,92 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright 2025 Ramil Nugmanov
+# This file is part of chython.
+#
+# chython is free software; you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License as published by
+# the Free Software Foundation; either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this program; if not, see .
+#
+from CachedMethods import class_cached_property
+
+
+class Chimera:
+ __slots__ = ()
+
+ def to_cdk(self):
+ """
+ Convert molecule to CDK Molecule object.
+
+ Due to translation through SMILES string, atom order is not preserved.
+ Use `self.smiles_atoms_order` to map atoms back.
+ """
+ parser = self._cdk_engine.smiles.SmilesParser(self._cdk_engine.DefaultChemObjectBuilder.getInstance())
+ return parser.parseSmiles(str(self))
+
+ def to_openbabel(self):
+ """
+ Convert molecule to OpenBabel OBMol object.
+
+ Due to translation through SMILES string, atom order is not preserved.
+ Use `self.smiles_atoms_order` to map atoms back.
+ """
+ from openbabel import openbabel
+
+ mol = openbabel.OBMol()
+ assert self._obparser(mol, str(self)), 'OpenBabel failed to parse smiles'
+ return mol
+
+ def to_indigo(self):
+ """
+ Convert molecule to Indigo molecule object.
+
+ Due to translation through SMILES string, atom order is not preserved.
+ Use `self.smiles_atoms_order` to map atoms back.
+ """
+ return self._indigo_engine.loadMolecule(str(self))
+
+ @class_cached_property
+ def _cdk_engine(self):
+ try:
+ from jpype import isJVMStarted, startJVM, JPackage
+
+ if not isJVMStarted():
+ from chython import class_paths
+
+ startJVM('--enable-native-access=ALL-UNNAMED', classpath=class_paths)
+
+ return JPackage('org').openscience.cdk
+ except (ImportError, AttributeError):
+ raise ImportError('Java/JPype/CDK.jar is not installed or broken. make sure CDK_PATH env variable is set')
+
+ @class_cached_property
+ def _indigo_engine(self):
+ from indigo import Indigo
+
+ return Indigo()
+
+ @class_cached_property
+ def _obparser(self):
+ from openbabel import openbabel
+
+ obparser = openbabel.OBConversion()
+ obparser.SetInFormat('smi')
+ return obparser.ReadString
+
+ @class_cached_property
+ def _obgen2d(self):
+ from openbabel import openbabel
+
+ return openbabel.OBOp.FindType('gen2D').Do
+
+
+__all__ = ['Chimera']
diff --git a/chython/containers/molecule.py b/chython/containers/molecule.py
index f695436d..291327df 100644
--- a/chython/containers/molecule.py
+++ b/chython/containers/molecule.py
@@ -25,6 +25,7 @@
from zlib import compress, decompress
from .bonds import Bond, DynamicBond
from .cgr import CGRContainer
+from .chimera import Chimera
from .graph import Graph
from .rdkit import RDkit
from ..algorithms.aromatics import Aromatize
@@ -65,7 +66,7 @@ def _rotable_rules():
class MoleculeContainer(MoleculeStereo, Graph[Element, Bond], Morgan, Rings, MoleculeIsomorphism,
Aromatize, StandardizeMolecule, MoleculeSmiles, DepictMolecule, Calculate2DMolecule,
- Conformers, Fingerprints, Tautomers, RDkit, MCS, X3domMolecule):
+ Conformers, Fingerprints, Tautomers, RDkit, Chimera, MCS, X3domMolecule):
__slots__ = ('_meta', '_name', '_conformers', '_changed', '_backup')
def __init__(self):
diff --git a/chython/files/__init__.py b/chython/files/__init__.py
index e5b0778a..1265b583 100644
--- a/chython/files/__init__.py
+++ b/chython/files/__init__.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
#
-# Copyright 2014-2023 Ramil Nugmanov
+# Copyright 2014-2025 Ramil Nugmanov
# This file is part of chython.
#
# chython is free software; you can redistribute it and/or modify
@@ -19,11 +19,12 @@
from .daylight import *
from .libinchi import *
from .MRVrw import *
+from .opsin import *
from .PDBrw import *
from .RDFrw import *
from .SDFrw import *
from .xyz import *
-__all__ = ['smiles', 'smarts', 'mdl_mol', 'mdl_rxn', 'xyz', 'xyz_file', 'inchi']
+__all__ = ['smiles', 'smarts', 'mdl_mol', 'mdl_rxn', 'xyz', 'xyz_file', 'inchi', 'opsin', 'iupac']
__all__.extend(x for x in locals() if x.endswith(('Read', 'Write')))
diff --git a/chython/files/opsin.py b/chython/files/opsin.py
new file mode 100644
index 00000000..78e12e05
--- /dev/null
+++ b/chython/files/opsin.py
@@ -0,0 +1,49 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright 2025 Ramil Nugmanov
+# This file is part of chython.
+#
+# chython is free software; you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License as published by
+# the Free Software Foundation; either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this program; if not, see .
+#
+from jpype import JPackage, isJVMStarted, startJVM
+from .daylight import smiles
+
+
+_nametostruct = _opsin = None
+
+
+def opsin(string):
+ """Parse IUPAC name into MoleculeContainer.
+ """
+ global _opsin, _nametostruct
+
+ if _opsin is None:
+ if not isJVMStarted():
+ from chython import class_paths
+
+ startJVM('--enable-native-access=ALL-UNNAMED', classpath=class_paths)
+
+ _opsin = JPackage('uk').ac.cam.ch.wwmm.opsin
+ _nametostruct = _opsin.NameToStructure.getInstance()
+
+ result = _nametostruct.parseChemicalName(string)
+ if str(result.getStatus()) == 'FAILURE':
+ raise ValueError(f'Failed to convert `{string}`: {result.getMessage()}')
+ return smiles(str(result.getSmiles()))
+
+
+iupac = opsin
+
+
+__all__ = ['opsin', 'iupac']
diff --git a/chython/reactor/deprotection.py b/chython/reactor/deprotection.py
index b293702b..22962538 100644
--- a/chython/reactor/deprotection.py
+++ b/chython/reactor/deprotection.py
@@ -325,6 +325,11 @@
'c1ccccc1NC(=O)OCC[Si](C)(C)C', 'c1ccccc1N'),
)
+_amine_sem = (
+ ('[N;D2,D3:1]-;!@[C;D2;x2;z1][O;D2;x0]-[C;D2;z1;x1][C;D2;x1;z1][Si;D4;z1;x0]([C;D1])([C;D1])[C;D1]', '[A:1]',
+ 'CN(C)COCC[Si](C)(C)C', 'CNC'),
+)
+
_amine_troc = ( # [Zn]
('[N;D2,D3:1]-;!@[C;z2;x3](=O)[O;D2;x0]-[C;D2][C;D4;x3]([Cl;D1])([Cl;D1])[Cl;D1]', '[A:1]',
'c1ccccc1NC(=O)OCC(Cl)(Cl)Cl', 'c1ccccc1N', 'c1ccccc1NC(=O)OC(C)C(Cl)(Cl)Cl'),
diff --git a/chython/reactor/reactions/__init__.py b/chython/reactor/reactions/__init__.py
index 78e2f9e9..3ee46ec3 100644
--- a/chython/reactor/reactions/__init__.py
+++ b/chython/reactor/reactions/__init__.py
@@ -29,6 +29,7 @@
from ._sonogashira import template as songashira_template
from ._sulfonamidation import template as sulfonamidation_template
from ._suzuki_miyaura import template as suzuki_miyaura_template
+from ._xec_sp2_sp3 import template as xec_template
from ..reactor import Reactor, fix_mapping_overlap
from ... import smarts, ReactionContainer, MoleculeContainer
diff --git a/chython/reactor/reactions/_xec_sp2_sp3.py b/chython/reactor/reactions/_xec_sp2_sp3.py
new file mode 100644
index 00000000..3c225fae
--- /dev/null
+++ b/chython/reactor/reactions/_xec_sp2_sp3.py
@@ -0,0 +1,47 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright 2025 Kostia Chernichenko
+# This file is part of chython.
+#
+# chython is free software; you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License as published by
+# the Free Software Foundation; either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this program; if not, see .
+#
+
+
+template = {
+ 'name': 'XEC',
+ 'description': 'Cross-electrophile C-sp2-X C-sp3-X coupling reaction',
+ 'templates': [
+ {
+ 'A': [
+ # Hal-Ar
+ '[Cl,Br,I;D1:1]-[C;a:2]',
+ # Hal-pseudoaromatic, more specifically C5, C6 vinylic
+ '[Cl,Br,I;D1:1]-[C;z2;r5,r6:2]',
+ # Ar triflate
+ '[C;a:2]-[O;D2;x1:1]-[S;x3;D4:10](=[O:11])(=[O:12])-[C;D4:13](-[F;D1:14])(-[F;D1:15])-[F;D1:16]'
+ ],
+ 'B': [
+ # sp3-C-X
+ '[Cl,Br,I;D1:3]-[C;z1:4]'
+ ],
+ 'product': '[A:2]-[A:4]',
+ 'alerts': [],
+ 'ufe': {
+ 'A': '[A:2][At;M]',
+ 'B': 3
+ }
+ }
+ ],
+ 'alerts': []
+}
diff --git a/pyproject.toml b/pyproject.toml
index bd206d03..fc6ebe76 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[tool.poetry]
name = 'chython'
-version = '2.13'
+version = '2.15'
description = 'Library for processing molecules and reactions in python way'
authors = ['Ramil Nugmanov ']
license = 'LGPLv3'
@@ -39,6 +39,9 @@ py-mini-racer = {version = '>=0.6.0', optional = true}
chytorch-rxnmap = {version = '>=1.4', optional = true}
rdkit = {version = '>=2023.9', optional = true}
pyppeteer = {version = '>=2.0.0', optional = true}
+jpype1 = {version = '>=1.6.0', optional = true}
+openbabel-wheel = {version = '>=3.1.1.22', optional = true}
+cdpkit = {version = '>=1.2.3', optional = true}
numpy = ">=1.21.0"
[tool.poetry.extras]
@@ -47,6 +50,8 @@ rdkit = ['rdkit']
png = ['pyppeteer']
racer-default = ['mini-racer']
racer-deprecated = ['py-mini-racer']
+extra-clean2d = ['jpype1', 'openbabel-wheel']
+extra-clean3d = ['cdpkit']
[tool.poetry.group.dev]
optional = true