"""What the properties panel shows for each species of a document."""
from __future__ import annotations
from dataclasses import asdict, dataclass, field
from typing import Any
from vibesketch.document.model import Document
__all__ = ["SpeciesReport", "report"]
[docs]
@dataclass
class SpeciesReport:
id: str
label: str | None
smiles: str | None
inchikey: str | None
inchi: str | None
formula: str | None
average_mass: float | None
monoisotopic_mass: float | None
charge: int
multiplicity: int
#: The checked name (see :mod:`vibesketch.naming`), or ``None`` if not asked for.
name: dict[str, Any] | None
#: Mass percent per element and the "Anal. Calcd for" line (:mod:`vibesketch.chem.analysis`).
analysis: dict[str, Any] | None = None
#: Calculated estimates: logP, molar refractivity, TPSA, donors, acceptors, counts.
descriptors: dict[str, Any] | None = None
#: CIP descriptors, undefined stereo and enhanced-stereo groups (:mod:`vibesketch.chem.cip`).
stereo: dict[str, Any] | None = None
#: Atoms marked in colour: ``{"colour", "atoms": ["C2", ...]}`` per highlight.
highlights: list[dict[str, Any]] = field(default_factory=list)
#: Abbreviations drawn contracted in it, such as ``["Boc", "Ph"]``.
abbreviations: list[str] = field(default_factory=list)
#: A generated nanostructure's key, description and size (see document.nanostructures).
nanostructure: dict[str, Any] | None = None
warnings: list[str] = field(default_factory=list)
[docs]
def to_dict(self) -> dict[str, Any]:
return asdict(self)
[docs]
def report(document: Document, *, names: bool = True) -> list[SpeciesReport]:
"""One report per species, in document order."""
from vibesketch.chem.analysis import descriptors, elemental_analysis
from vibesketch.chem.species import TRANSITION_METALS, identity, properties
from vibesketch.document.abbreviations import abbreviations_of
from vibesketch.document.formats import species_mols
from vibesketch.document.highlights import marks_for
from vibesketch.document.stereo import described, stale_labels
from vibesketch.naming import name_for
mols = species_mols(document)
stale = set(stale_labels(document))
contracted = abbreviations_of(document)
rows: list[SpeciesReport] = []
for species in document.species:
mol = mols.get(species.id)
warnings: list[str] = []
props = properties(mol) if mol is not None else None
name: dict[str, Any] | None = None
if species.name is not None:
name = {
"status": "set",
"value": species.name.value,
"source": species.name.source,
"confidence": species.name.confidence,
"note": None,
}
elif names and species.smiles:
result = name_for(species.smiles, species.charge, species.multiplicity)
name = asdict(result)
if mol is not None:
numbers = {int(a.GetAtomicNum()) for a in mol.GetAtoms()}
if numbers & TRANSITION_METALS:
warnings.append(
"contains a transition metal: set the multiplicity explicitly, the drawing "
"cannot know its spin state"
)
drawn = identity(mol)
if species.id in stale:
warnings.append(
"the stereo labels drawn on it no longer match its structure: label again"
)
if drawn.charge != species.charge:
warnings.append(
f"the drawing's charge is {drawn.charge}, the document says {species.charge}"
)
formula = props.formula if props else None
average = round(props.average_mass, 4) if props else None
monoisotopic = round(props.monoisotopic_mass, 6) if props else None
made: dict[str, Any] | None = None
if species.solid is not None:
from vibesketch.chem.formula import parse_formula
formula = species.solid.text
average = species.solid.molar_mass_g_mol
mono = parse_formula(species.solid.text).monoisotopic_mass()
monoisotopic = round(mono, 6) if mono is not None else None
if average is None:
warnings.append(
f"{species.solid.text} has a variable composition: it has no molar mass"
)
if species.nanostructure is not None:
from vibesketch.document.nanostructures import masses, summary
made = summary(species)
formula = species.nanostructure.formula
average, monoisotopic = masses(species.conformers[0].symbols)
rows.append(
SpeciesReport(
id=species.id,
label=species.label,
smiles=species.smiles,
inchikey=species.inchikey,
inchi=props.inchi if props else None,
formula=formula,
average_mass=average,
monoisotopic_mass=monoisotopic,
charge=species.charge,
multiplicity=species.multiplicity,
name=name,
analysis=elemental_analysis(mol).to_dict() if mol is not None else None,
descriptors=descriptors(mol).to_dict() if mol is not None else None,
stereo=described(mol) if mol is not None else None,
highlights=[
{
"colour": colour,
"atoms": [f"{mol.GetAtomWithIdx(i).GetSymbol()}{i + 1}" for i in atoms],
}
for atoms, colour in marks_for(document, species.id, mol)
]
if mol is not None
else [],
abbreviations=contracted.get(species.id, []),
nanostructure=made,
warnings=warnings,
)
)
return rows