"""CIP stereodescriptors of a structure, and its stereo left undefined (#92).
R/S (and the pseudo-asymmetric r/s) at stereocentres, E/Z at double bonds
and M/P at atropisomeric axes come from RDKit's new CIP labeler, an
implementation of the rules as revised by R. M. Hanson, S. Musacchio,
J. W. Mayfield, M. J. Vainio, A. Yerin and D. Redkin, "Algorithmic analysis
of Cahn-Ingold-Prelog rules of stereochemistry: proposals for revised rules
and a guide for machine implementation", J. Chem. Inf. Model. 58 (2018)
1755-1765, doi:10.1021/acs.jcim.8b00324 (``rdCIPLabeler.AssignCIPLabels``).
It is used, not RDKit's legacy approximation, so the drawing, figures and
every interface give the same labels.
Stereocentres and double bonds that could be stereogenic but are drawn
without a configuration, or as "either" on purpose, are reported
(``Chem.FindPotentialStereo``), as a check rather than an error: a racemate
or an unknown geometry is drawn so.
Enhanced-stereo groups (ABS, AND, OR) are listed with their atoms.
"""
from __future__ import annotations
from dataclasses import asdict, dataclass, field
from typing import Any
__all__ = ["StereoLabels", "labelled", "stereo_labels"]
[docs]
@dataclass(frozen=True)
class StereoLabels:
"""Descriptors by atom index and by bond (as its two atom indices), and what is undefined."""
#: ``[atom, label]``: R, S, r, s, or M and P at an axis atom.
atoms: list[tuple[int, str]] = field(default_factory=list)
#: ``[[begin, end], label]``: E, Z, or M and P at an atropisomeric bond.
bonds: list[tuple[tuple[int, int], str]] = field(default_factory=list)
#: Stereocentres drawn without a configuration, by atom index.
undefined_centres: list[int] = field(default_factory=list)
#: Double bonds drawn without a geometry, as their two atom indices.
unspecified_double_bonds: list[tuple[int, int]] = field(default_factory=list)
#: Enhanced stereo: ``{"kind": "abs" | "and" | "or", "atoms": [...]}``.
groups: list[dict[str, Any]] = field(default_factory=list)
[docs]
def to_dict(self) -> dict[str, Any]:
return asdict(self)
[docs]
def labelled(mol: Any) -> Any:
"""A copy of ``mol`` whose atoms and bonds carry the new labeler's ``_CIPCode``."""
from rdkit import Chem
from rdkit.Chem import rdCIPLabeler
copy = Chem.Mol(mol)
for atom in copy.GetAtoms():
atom.ClearProp("_CIPCode")
for bond in copy.GetBonds():
bond.ClearProp("_CIPCode")
rdCIPLabeler.AssignCIPLabels(copy)
return copy
[docs]
def stereo_labels(mol: Any) -> StereoLabels:
"""The CIP descriptors, undefined stereo and enhanced-stereo groups of ``mol``."""
from rdkit import Chem
found = labelled(mol)
atoms = [
(atom.GetIdx(), str(atom.GetProp("_CIPCode")))
for atom in found.GetAtoms()
if atom.HasProp("_CIPCode")
]
bonds = [
((bond.GetBeginAtomIdx(), bond.GetEndAtomIdx()), str(bond.GetProp("_CIPCode")))
for bond in found.GetBonds()
if bond.HasProp("_CIPCode")
]
undefined: list[int] = []
unspecified: list[tuple[int, int]] = []
# Unspecified: drawn without a configuration; Unknown: drawn as "either" on purpose.
undecided = (Chem.StereoSpecified.Unspecified, Chem.StereoSpecified.Unknown)
for info in Chem.FindPotentialStereo(Chem.Mol(mol)):
if info.specified not in undecided:
continue
if info.type == Chem.StereoType.Atom_Tetrahedral:
undefined.append(int(info.centeredOn))
elif info.type == Chem.StereoType.Bond_Double:
bond = mol.GetBondWithIdx(int(info.centeredOn))
unspecified.append((bond.GetBeginAtomIdx(), bond.GetEndAtomIdx()))
kinds = {
Chem.StereoGroupType.STEREO_ABSOLUTE: "abs",
Chem.StereoGroupType.STEREO_AND: "and",
Chem.StereoGroupType.STEREO_OR: "or",
}
groups = [
{"kind": kinds[group.GetGroupType()], "atoms": sorted(a.GetIdx() for a in group.GetAtoms())}
for group in mol.GetStereoGroups()
]
return StereoLabels(atoms, bonds, sorted(undefined), sorted(unspecified), groups)