Source code for vibesketch.chem.cip

"""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)