Source code for vibesketch.document.relax
"""Relaxed 3D structures for a document's species, kept in the document.
Each named species' first conformer goes through
:func:`vibesketch.chem.relax.relax_3d` and is replaced by the result; its
other conformers stay. Constraints name atoms of one species, so a call
with constraints names exactly one species.
"""
from __future__ import annotations
from collections.abc import Sequence
from dataclasses import dataclass, field
from vibesketch.chem.relax import DEFAULT_FMAX, DEFAULT_MAX_STEPS, ForceField, Relaxed
from vibesketch.document.embed import UnknownSpecies
from vibesketch.document.model import Constraint, Document
__all__ = ["RelaxReport", "relax"]
[docs]
@dataclass
class RelaxReport:
"""What relaxing did, per species id."""
relaxed: list[str] = field(default_factory=list)
#: The relaxation of each relaxed species: energies, steps, constraint checks.
results: dict[str, Relaxed] = field(default_factory=dict)
#: Species that could not be relaxed, with the reason.
failed: dict[str, str] = field(default_factory=dict)
warnings: list[str] = field(default_factory=list)
[docs]
def relax(
document: Document,
species: Sequence[str] | None = None,
*,
constraints: Sequence[Constraint] | None = None,
force_field: ForceField = "mmff94",
fmax: float | None = None,
max_steps: int | None = None,
) -> tuple[Document, RelaxReport]:
"""``document`` with the 3D structures of ``species`` (default: all) relaxed.
``fmax`` and ``max_steps`` default to the relaxation's own. Raises
:class:`UnknownSpecies` for an id the document does not have and
:class:`ValueError` when constraints come with more than one species.
"""
from vibesketch.chem.relax import RelaxFailed, relax_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)
if constraints and len(wanted) != 1:
raise ValueError("constraints name atoms of one species, so name exactly one species")
report = RelaxReport()
updated = []
for entry in document.species:
if entry.id not in wanted:
updated.append(entry)
continue
try:
result = relax_3d(
entry,
constraints=constraints,
force_field=force_field,
fmax=DEFAULT_FMAX if fmax is None else fmax,
max_steps=DEFAULT_MAX_STEPS if max_steps is None else max_steps,
)
except RelaxFailed as error:
report.failed[entry.id] = str(error)
updated.append(entry)
continue
report.relaxed.append(entry.id)
report.results[entry.id] = result
report.warnings.extend(f"{entry.id}: {warning}" for warning in result.warnings)
conformers = [result.conformer, *entry.conformers[1:]]
updated.append(entry.model_copy(update={"conformers": conformers}))
rebuilt = document.model_copy(update={"species": updated})
return Document.model_validate(rebuilt.to_json_data()), report