Source code for qsarkit.chemistry.fragments._core_extractor
"""Scaffold / core-structure extraction."""
from __future__ import annotations
from qsarkit.base.exceptions import RDKIT_MOLECULE_ERRORS
from typing import Any, List, Optional, Sequence
[docs]
class CoreExtractor:
"""Extract a representative structural core from one or more molecules.
Three definitions of "core", answering different questions:
- :meth:`bemis_murcko` gives ring systems plus linkers (the classic
scaffold definition), or the fully generic skeleton with element
and bond-order information stripped.
- :meth:`mcs` gives the maximum common substructure across a set of
molecules, which is the right notion for a congeneric series.
- :meth:`medchem_core` gives a medicinal-chemistry-oriented core: the
Bemis-Murcko scaffold with terminal exocyclic double bonds trimmed
back to rings, which tends to match how chemists describe a
series' "core" more closely than the strict Murcko definition.
Examples
--------
>>> from rdkit import Chem
>>> from qsarkit.chemistry import CoreExtractor
>>> extractor = CoreExtractor()
>>> paracetamol_like = Chem.MolFromSmiles("CC(=O)Nc1ccc(Cl)cc1")
>>> Chem.MolToSmiles(extractor.bemis_murcko(paracetamol_like))
'c1ccccc1'
``generic=True`` discards element identity and bond order, so
pyridine and benzene analogues collapse onto one skeleton -- the
right granularity for asking "how many ring systems are in this
library", the wrong one for asking "which chemotype is this":
>>> Chem.MolToSmiles(extractor.bemis_murcko(paracetamol_like, generic=True))
'C1CCCCC1'
:meth:`mcs` works across a series rather than on one molecule, and
returns the shared substructure as a query mol:
>>> pair = [Chem.MolFromSmiles(s) for s in
... ("CC(=O)Nc1ccc(Cl)cc1", "CC(=O)Nc1ccc(Br)cc1")]
>>> Chem.MolToSmarts(extractor.mcs(pair))
'[#6]-[#6](=[#8])-[#7]-[#6]1:[#6]:[#6]:[#6]:[#6]:[#6]:1'
References
----------
- Bemis, G. W. & Murcko, M. A. (1996). "The Properties of Known
Drugs. 1. Molecular Frameworks." J. Med. Chem., 39(15), 2887-2893.
https://doi.org/10.1021/jm9602928
- Rogers, D. & Hahn, M. (2010) discuss MCS-based series analysis;
canonical algorithm: Cao, Y. et al. (2008). "A Maximum Common
Substructure-Based Algorithm for Searching and Predicting Drug-like
Compounds." Bioinformatics, 24(13), i366-i374.
https://doi.org/10.1093/bioinformatics/btn186
- RDKit ``rdFMCS`` and ``Chem.Scaffolds.MurckoScaffold`` documentation:
https://www.rdkit.org/docs/source/rdkit.Chem.Scaffolds.MurckoScaffold.html
https://www.rdkit.org/docs/source/rdkit.Chem.rdFMCS.html
"""
[docs]
def bemis_murcko(self, mol: Any, generic: bool = False) -> Any:
from rdkit.Chem.Scaffolds import MurckoScaffold
scaffold = MurckoScaffold.GetScaffoldForMol(mol)
if generic:
scaffold = MurckoScaffold.MakeScaffoldGeneric(scaffold)
return scaffold
[docs]
def mcs(self, mols: Sequence[Any], **kwargs: Any) -> Optional[Any]:
from rdkit import Chem
from rdkit.Chem import rdFMCS
result = rdFMCS.FindMCS(list(mols), **kwargs)
if result.canceled or result.numAtoms == 0:
return None
return Chem.MolFromSmarts(result.smartsString)
[docs]
def medchem_core(self, mol: Any) -> Any:
from rdkit import Chem
from rdkit.Chem.Scaffolds import MurckoScaffold
scaffold = MurckoScaffold.GetScaffoldForMol(mol)
rw = Chem.RWMol(scaffold)
# drop terminal atoms that are not part of any ring (residual
# linker stubs left after Murcko decomposition, e.g. an exocyclic
# =O or =N stub with no ring neighbor beyond it)
to_remove = [
atom.GetIdx()
for atom in rw.GetAtoms()
if atom.GetDegree() == 1 and not atom.IsInRing()
]
for idx in sorted(to_remove, reverse=True):
rw.RemoveAtom(idx)
core = rw.GetMol()
try:
Chem.SanitizeMol(core)
except RDKIT_MOLECULE_ERRORS:
# Trimming can leave an unsanitizable fragment; the untrimmed
# Murcko scaffold is the correct fallback.
return scaffold
return core
[docs]
def transform(
self, mols: List[Any], method: str = "bemis_murcko", **kwargs: Any
) -> List[Optional[Any]]:
"""Apply a per-molecule extraction method over an ``Iterable[Mol]``."""
fn = {"bemis_murcko": self.bemis_murcko, "medchem_core": self.medchem_core}[method]
return [fn(m, **kwargs) if m is not None else None for m in mols]