Source code for vibesketch.document.embed
"""3D structures for a document's species, cached in the document.
Each species gets its conformers from :func:`vibesketch.chem.embed.embed_3d`
and keeps them while its structure, charge and multiplicity stay the same, so
a redraw elsewhere on the canvas does not throw them away (see
:mod:`vibesketch.document.derive`). The input writers read the first
conformer.
"""
from __future__ import annotations
from collections.abc import Sequence
from dataclasses import dataclass, field
from vibesketch.document.model import Document
__all__ = ["EmbedReport", "UnknownSpecies", "embed"]
[docs]
class UnknownSpecies(KeyError):
"""The document has no species with that id."""
def __str__(self) -> str:
return f"the document has no species {self.args[0]!r}"
[docs]
@dataclass
class EmbedReport:
"""What embedding did, per species id."""
embedded: list[str] = field(default_factory=list)
#: Species that had conformers already and were left alone.
kept: list[str] = field(default_factory=list)
#: Species that could not be embedded, with the reason.
failed: dict[str, str] = field(default_factory=dict)
warnings: list[str] = field(default_factory=list)
[docs]
def embed(
document: Document,
species: Sequence[str] | None = None,
*,
count: int = 1,
replace: bool = False,
) -> tuple[Document, EmbedReport]:
"""``document`` with 3D conformers for ``species`` (default: all of them).
A species that has conformers keeps them unless ``replace`` is set.
Raises :class:`UnknownSpecies` for an id the document does not have.
"""
from vibesketch.chem.embed import EmbedFailed, embed_3d
known = {s.id for s in document.species}
wanted = list(species) if species is not None else [s.id for s in document.species]
for identifier in wanted:
if identifier not in known:
raise UnknownSpecies(identifier)
report = EmbedReport()
updated = []
for entry in document.species:
if entry.id not in wanted:
updated.append(entry)
continue
if entry.conformers and not replace:
report.kept.append(entry.id)
updated.append(entry)
continue
if entry.solid is not None:
report.failed[entry.id] = (
f"the solid {entry.solid.text} is given by its formula: its structure comes with "
"crystal and slab models (roadmap Phase 5)"
)
updated.append(entry)
continue
if entry.nanostructure is not None:
report.failed[entry.id] = (
"a generated nanostructure keeps its generated atoms; generate it anew to "
"change them"
)
updated.append(entry)
continue
if entry.smiles is None:
report.failed[entry.id] = "it has no structure to embed"
updated.append(entry)
continue
try:
result = embed_3d(entry.smiles, count=count, attempts=max(10, 3 * count))
except EmbedFailed as error:
report.failed[entry.id] = str(error)
updated.append(entry)
continue
report.embedded.append(entry.id)
report.warnings.extend(f"{entry.id}: {warning}" for warning in result.warnings)
updated.append(entry.model_copy(update={"conformers": result.conformers}))
rebuilt = document.model_copy(update={"species": updated})
return Document.model_validate(rebuilt.to_json_data()), report