diff --git a/chython/__init__.py b/chython/__init__.py index 2b4fc171..93d5cd04 100644 --- a/chython/__init__.py +++ b/chython/__init__.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # -# Copyright 2014-2026 Ramil Nugmanov +# Copyright 2014-2025 Ramil Nugmanov # Copyright 2014-2019 Timur Madzhidov tmadzhidov@gmail.com features and API discussion # Copyright 2014-2019 Alexandre Varnek base idea of CGR approach # This file is part of chython. @@ -18,7 +18,6 @@ # 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,9 +26,9 @@ from .utils import * -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')] +torch_device = 'cpu' # AAM model device. Change before first `reset_mapping` call! +clean2d_engine: Literal['smilesdrawer', 'rdkit'] = 'smilesdrawer' +conformer_engine: Literal['rdkit'] = 'rdkit' __all__ = [] diff --git a/chython/algorithms/conformers.py b/chython/algorithms/conformers.py index 849ad116..45db16fd 100644 --- a/chython/algorithms/conformers.py +++ b/chython/algorithms/conformers.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # -# Copyright 2025, 2026 Ramil Nugmanov +# Copyright 2025 Ramil Nugmanov # This file is part of chython. # # chython is free software; you can redistribute it and/or modify @@ -16,43 +16,36 @@ # You should have received a copy of the GNU Lesser General Public License # along with this program; if not, see . # -from io import StringIO -from typing import Literal +from typing import Literal, TYPE_CHECKING + + +if TYPE_CHECKING: + from chython import MoleculeContainer class Conformers: __slots__ = () - def generate_conformers(self, limit: int = 10, *, optimize: bool = False, - engine: Literal['rdkit', 'cdpkit'] = None, **kwargs) -> int: + def generate_conformers(self: 'MoleculeContainer', limit: int = 10, *, optimize: bool = False, + engine: Literal['rdkit'] = None, **kwargs) -> int: """ Generate conformers for the molecule ignoring implicit hydrogens. Set them manually to have a full 3D structure. By default, the RDKit engine is used. Can be changed globally with the `chython.conformer_engine` parameter. - Two conformer generation engines are supported: - - 'rdkit': Uses RDKit's ETKDG algorithm for conformer generation - - 'cdpkit': Uses CDPKit's ConformerGenerator for conformer generation (https://pubs.acs.org/doi/10.1021/acs.jcim.3c00563) - :param limit: maximum number of conformers to generate :param optimize: optimize conformers using MMFF94 force field (only for RDKit engine) - :param engine: override globally set engine ('rdkit' or 'cdpkit') - :param kwargs: additional arguments for the engine: - - timeout: timeout for the engine in seconds (only for CDPKit engine, default: 60) - - min_rmsd: minimum RMSD between generated conformers (only for CDPKit engine, default: .5) - - energy_window: energy window for the engine (only for CDPKit engine, default: 20) + :param engine: override globally set engine + :param kwargs: additional arguments for the engine - Check EmbedMultipleConfs API for RDKit engine :return: number of generated conformers """ if engine is None: from chython import conformer_engine as engine - - copy = self.copy() - copy.explicify_hydrogens() - if engine == 'rdkit': from rdkit.Chem.AllChem import EmbedMultipleConfs, MMFFOptimizeMolecule + copy = self.copy() + copy.explicify_hydrogens() rmol = copy.to_rdkit(keep_mapping=False, keep_hydrogens=False) ids = EmbedMultipleConfs(rmol, numConfs=limit, **kwargs) if optimize: @@ -63,40 +56,6 @@ def generate_conformers(self, limit: int = 10, *, optimize: bool = False, {n: tuple(v) for n, v in zip(self, conf.GetPositions())} for conf in rmol.GetConformers() if conf.Is3D() ] - elif engine == 'cdpkit': - from CDPL import Base, Chem, ConfGen - from chython import SDFWrite, SDFRead # to prevent circular imports - from chython.files.mdl import parse_mol_v2000 - - # the easiest way is just to provide intermediate SDF - f = StringIO() - SDFWrite(f, mapping=False).write(copy) - cmol = Chem.BasicMolecule() - if not Chem.SDFMoleculeReader(Base.StringIOStream(f.getvalue())).read(cmol): - return 0 - - ConfGen.prepareForConformerGeneration(cmol) - gen = ConfGen.ConformerGenerator() - gen.settings.timeout = kwargs.get('timeout', 60) * 1000 - gen.settings.minRMSD = kwargs.get('min_rmsd', .5) - gen.settings.energyWindow = kwargs.get('energy_window', 20.) - gen.settings.maxNumOutputConformers = limit - if gen.generate(cmol) != ConfGen.ReturnCode.SUCCESS: - return 0 - - gen.setConformers(cmol) - c = gen.getNumConformers() - f = Base.StringIOStream(mode='w') - Chem.SDFMolecularGraphWriter(f).write(cmol) - s = SDFRead(StringIO(f.getvalue())) - - conformers = [ - { - n: (a['x'], a['y'], a['z']) - for n, a in zip(self, parse_mol_v2000(s._read_mol(current=False))['atoms']) - } - for _ in range(c) - ] else: raise ValueError(f'Invalid conformer generation engine: {engine}') if conformers: self._conformers = conformers diff --git a/chython/reactor/reactions/__init__.py b/chython/reactor/reactions/__init__.py new file mode 100644 index 00000000..78e2f9e9 --- /dev/null +++ b/chython/reactor/reactions/__init__.py @@ -0,0 +1,157 @@ +# -*- coding: utf-8 -*- +# +# Copyright 2022-2024 Ramil Nugmanov +# Copyright 2023 Timur Gimadiev +# 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 collections import deque +from itertools import product +from typing import Iterator, Optional, List +from ._amidation import template as amidation_template +from ._amine_isocyanate import template as amine_isocyanate_template +from ._buchwald_hartwig import template as buchwald_hartwig_template +from ._esterification import template as esterification_template +from ._macmillan import template as macmillan_template +from ._reductive_amination import template as reductive_amination_template +from ._sonogashira import template as songashira_template +from ._sulfonamidation import template as sulfonamidation_template +from ._suzuki_miyaura import template as suzuki_miyaura_template +from ..reactor import Reactor, fix_mapping_overlap +from ... import smarts, ReactionContainer, MoleculeContainer + +""" +Predefined reactors for common reactions. +""" + + +################# +# Magic Factory # +################# + +__all__ = ['PreparedReactor', 'prepare_reactor'] +__all__.extend(k[:-9] for k, v in globals().items() if k.endswith('_template') and isinstance(v, dict) and v) +_cache = {} + + +class PreparedReactor: + """ + Prepared reactors with predefined sets of templates. + """ + def __init__(self, rules, name): + self.name = name + self.rules = rules + + self.rxn_ms = [] + self.rxn_os = [] + self.alerts = [] + + self.global_alerts = [smarts(x) for x in rules['alerts']] + + for c in rules['templates']: + alerts = [smarts(x) for x in c['alerts']] + p = smarts(c['product']) + for rs in product(*([smarts(x) for x in c[x]] for x in 'ABCD' if x in c)): + self.rxn_ms.append(Reactor(rs, [p], one_shot=False, automorphism_filter=False)) # noqa + self.rxn_os.append(Reactor(rs, [p], one_shot=True, automorphism_filter=False)) # noqa + self.alerts.append(alerts) + + def __repr__(self): + return f'{__name__}.{self.name}' + + def __str__(self): + return f'Reactor<{self.rules["name"]}>' + + def __call__(self, *molecules: MoleculeContainer, one_shot=True, check_alerts: bool = True, + excess: Optional[List[int]] = None) -> Iterator[ReactionContainer]: + """ + :param molecules: Reactants molecules. + :param one_shot: Generate only single stage products. Otherwise, all possible combinations, including products. + :param check_alerts: Check structural alerts of reactants. + :param excess: Molecules indices which can be involved in multistep synthesis. All by default. + """ + if not molecules: + raise ValueError('empty molecule list') + if check_alerts and any(a < m for a, m in product(self.global_alerts, molecules)): + return + + molecules = fix_mapping_overlap(molecules) + seen = set() + if one_shot: + for rx, al in zip(self.rxn_os, self.alerts): + if check_alerts and any(a < m for a, m in product(al, molecules)): + continue + for r in rx(*molecules): + if str(r) in seen: + continue + seen.add(str(r)) + yield r + return + + excess = molecules if excess is None else [molecules[x] for x in excess] + stack = deque([]) + for i, (rx, al) in enumerate(zip(self.rxn_ms, self.alerts)): + if check_alerts and any(a < m for a, m in product(al, molecules)): + continue + x = self.rxn_ms.copy() + del x[i] + stack.appendleft((rx, molecules, x)) + + while stack: + rx, rct, nxt_rxn = stack.pop() + for r in rx(*rct): + if str(r) in seen: + continue + seen.add(str(r)) + + r = ReactionContainer([x.copy() for x in molecules], r.products) + yield r + + x = excess.copy() + for p in reversed(r.products): + x.insert(0, p.copy()) + x = fix_mapping_overlap(x) + if excess is not molecules: + # expected that product can react with all excess molecules simultaneously. + # e.g. multicomponent reaction (Ugi) + for m, nrx in enumerate(nxt_rxn): + z = nxt_rxn.copy() + del z[m] + stack.append((nrx, x.copy(), z)) + else: # drop one of the reactants + for n in range(len(r.products), len(x)): + y = x.copy() + del y[n] + for m, nrx in enumerate(nxt_rxn): + z = nxt_rxn.copy() + del z[m] + stack.append((nrx, y, z)) + + +prepare_reactor = PreparedReactor # backward compatibility + + +def __getattr__(name): + try: + return _cache[name] + except KeyError: + if name in __all__: + _cache[name] = t = PreparedReactor(globals()[f'{name}_template'], name) + return t + raise AttributeError + + +def __dir__(): + return __all__