Source code for vibesketch.chem.layout
"""2D coordinates for molecules that have none, with every coordinate finite."""
from __future__ import annotations
import math
from typing import Any
__all__ = ["compute_2d"]
def _positions(mol: Any) -> list[list[float]]:
"""Every atom's coordinates, read in one call: one Point3D per atom is far slower."""
return list(mol.GetConformer().GetPositions().tolist())
def _finite(mol: Any) -> bool:
return all(math.isfinite(value) for point in _positions(mol) for value in point)
def _separate_fragments(mol: Any) -> None:
"""Pack newly drawn components without changing their internal geometry.
Depictor atom extents omit the visible hydrogen/charge labels of isolated
ions. Leave two bond lengths between components to accommodate them.
Existing authored conformers never pass through this layout function.
"""
from rdkit import Chem
from rdkit.Geometry import Point3D
fragments = Chem.GetMolFrags(mol)
if len(fragments) < 2:
return
conformer = mol.GetConformer()
positions = _positions(mol)
lengths = [
math.dist(positions[b.GetBeginAtomIdx()], positions[b.GetEndAtomIdx()])
for b in mol.GetBonds()
]
bond_length = sum(lengths) / len(lengths) if lengths else 1.5
gap = 2.0 * (bond_length if bond_length > 1e-6 else 1.5)
cursor = 0.0
for fragment in fragments:
points = [conformer.GetAtomPosition(i) for i in fragment]
xmin, xmax = min(p.x for p in points), max(p.x for p in points)
middle = (min(p.y for p in points) + max(p.y for p in points)) / 2
for index, point in zip(fragment, points, strict=True):
conformer.SetAtomPosition(index, Point3D(point.x - xmin + cursor, point.y - middle, 0))
cursor += xmax - xmin + gap
[docs]
def compute_2d(mol: Any) -> None:
"""Give ``mol`` a 2D conformer in place.
CoordGen first (it lays out rings and macrocycles better), then RDKit's
own depictor, because CoordGen returns NaN for a molecule made only of
hydrogen atoms (H+, H2). If both fail, the atoms go on a line. The
process-wide CoordGen preference is restored afterwards.
"""
from rdkit.Chem import rdDepictor
from rdkit.Geometry import Point3D
previous = rdDepictor.GetPreferCoordGen()
try:
for prefer in (True, False):
rdDepictor.SetPreferCoordGen(prefer)
mol.RemoveAllConformers()
rdDepictor.Compute2DCoords(mol)
if _finite(mol):
_separate_fragments(mol)
return
finally:
rdDepictor.SetPreferCoordGen(previous)
conformer = mol.GetConformer()
for index in range(mol.GetNumAtoms()):
conformer.SetAtomPosition(index, Point3D(1.5 * index, 0.0, 0.0))
_separate_fragments(mol)