Source code for vibesketch.document.stereo

"""Stereodescriptors of a document's species, and as labels in its drawing (#92).

The descriptors come from :mod:`vibesketch.chem.cip`. In the drawing they
are KET ``cip`` properties on atoms and bonds, which the canvas shows as
"(R)" and "(E)". They are a snapshot: labelling writes them, clearing removes
them, and a label that no longer matches its structure after an edit is
reported as out of date rather than silently trusted.
"""

from __future__ import annotations

import copy
from typing import Any

from vibesketch.document.model import Canvas, Document

__all__ = ["described", "stale_labels", "stereo_report", "with_stereo_labels"]


[docs] def described(mol: Any) -> dict[str, Any]: """The labels of ``mol``, with each atom named as its symbol and number (C2 for index 1).""" from vibesketch.chem.cip import stereo_labels labels = stereo_labels(mol) def name(index: int) -> str: return f"{mol.GetAtomWithIdx(index).GetSymbol()}{index + 1}" return { **labels.to_dict(), "summary": [f"({label}) at {name(atom)}" for atom, label in labels.atoms] + [f"({label}) at {name(a)}={name(b)}" for (a, b), label in labels.bonds], "undefined": [name(atom) for atom in labels.undefined_centres] + [f"{name(a)}={name(b)}" for a, b in labels.unspecified_double_bonds], }
[docs] def stereo_report(document: Document) -> dict[str, dict[str, Any]]: """Each species' descriptors, undefined stereo and enhanced-stereo groups.""" from vibesketch.document.formats import species_mols return { species: described(mol) for species, mol in species_mols(document).items() if mol is not None }
def _expected(fragment: Any) -> tuple[dict[int, str], dict[frozenset[int], str]]: """What a fragment's KET atoms and bonds should carry, by KET index.""" from vibesketch.chem.cip import stereo_labels labels = stereo_labels(fragment.mol) ket = fragment.atoms atoms = {ket[index]: label for index, label in labels.atoms} bonds = {frozenset((ket[a], ket[b])): label for (a, b), label in labels.bonds} return atoms, bonds
[docs] def with_stereo_labels(document: Document, *, clear: bool = False) -> Document: """``document`` with every drawn structure's descriptors written as labels, or removed.""" from vibesketch.document.ket import read_scene data: dict[str, Any] = copy.deepcopy(document.canvas.data) for fragment in read_scene(data).fragments if data else []: node = data.get(fragment.node) if not isinstance(node, dict) or fragment.mol is None: continue atoms, bonds = ({}, {}) if clear else _expected(fragment) mine = set(fragment.atoms) for index, atom in enumerate(node.get("atoms", [])): if index in mine: atom.pop("cip", None) if index in atoms: atom["cip"] = atoms[index] for bond in node.get("bonds", []): ends = frozenset(bond.get("atoms", [])) if ends <= mine: bond.pop("cip", None) if ends in bonds: bond["cip"] = bonds[ends] return document.model_copy(update={"canvas": Canvas(format="ket", data=data)})
[docs] def stale_labels(document: Document) -> list[str]: """The species whose drawn labels no longer match their structure.""" from vibesketch.chem.species import identity from vibesketch.document.ket import read_scene data: dict[str, Any] = document.canvas.data if not data: return [] by_smiles = {s.smiles: s.id for s in document.species} stale: list[str] = [] for fragment in read_scene(data).fragments: node: Any = data.get(fragment.node) if not isinstance(node, dict) or fragment.mol is None: continue mine = set(fragment.atoms) drawn_atoms = { i: a["cip"] for i, a in enumerate(node.get("atoms", [])) if i in mine and isinstance(a, dict) and "cip" in a } drawn_bonds = { frozenset(b.get("atoms", [])): b["cip"] for b in node.get("bonds", []) if isinstance(b, dict) and "cip" in b and frozenset(b.get("atoms", [])) <= mine } if not drawn_atoms and not drawn_bonds: continue if (drawn_atoms, drawn_bonds) != _expected(fragment): species = by_smiles.get(identity(fragment.mol).smiles) if species is not None and species not in stale: stale.append(species) return stale