"""The QCSchema molecule writer.
Writes a ``qcschema_molecule`` (schema version 2): symbols, geometry in bohr,
charge and multiplicity. It emits only the fields vibe-qc's QCSchema adapter
accepts (``vibeqc.qcschema.molecule_from_qcschema``), which itself rejects
any molecule feature it cannot represent; this writer holds itself to the
same rule from the other side.
A molecule carries no calculation, so every task but ``geometry`` is
refused. :class:`QcschemaInputWriter` writes the calculation: a
``qcschema_input`` (schema version 1) with the molecule, a driver and a
model, exactly as far as vibe-qc's adapter (``vibeqc.qcschema``) reads one:
the ``energy`` driver for a single point, ``hessian`` for frequencies (the
adapter computes Hessians for HF only), ``model`` with method and basis,
and empty ``keywords``. Whatever the adapter would reject is refused here
first: optimisations (an AtomicInput has no such driver), TDDFT, NMR, and
every keyword (dispersion, solvation, auxiliary basis, approximation,
resources, constraints).
"""
from __future__ import annotations
import json
from collections.abc import Sequence
from typing import Any, ClassVar
from vibesketch.document.model import JobSpec, Species, Task
from vibesketch.inputs.base import InputFile, InputWriter, first_conformer, needs_conformer
from vibesketch.units import ANGSTROM_TO_BOHR
[docs]
class QcschemaMoleculeWriter(InputWriter):
name: ClassVar[str] = "qcschema-molecule"
description: ClassVar[str] = "QCSchema molecule (v2): geometry in bohr, charge, multiplicity"
tasks: ClassVar[frozenset[Task]] = frozenset({"geometry"})
def _refusals(self, species: Species, spec: JobSpec) -> list[str]:
reasons = needs_conformer(self.name, species)
if spec.task != "geometry":
reasons.append(
"a QCSchema molecule carries no calculation; write the calculation with the "
"'qcschema' writer (an input with model and driver) or the 'vibeqc' writer"
)
return reasons
[docs]
def molecule(self, species: Species) -> dict[str, Any]:
"""The ``qcschema_molecule`` object for ``species``."""
from vibesketch import __version__
conformer = first_conformer(species)
geometry = [
coordinate * ANGSTROM_TO_BOHR for row in conformer.xyz_angstrom for coordinate in row
]
return {
"schema_name": "qcschema_molecule",
"schema_version": 2,
"symbols": list(conformer.symbols),
"geometry": geometry,
"molecular_charge": species.charge,
"molecular_multiplicity": species.multiplicity,
"provenance": {
"creator": "vibe-sketch",
"version": __version__,
"routine": "vibesketch.inputs.qcschema",
},
}
def _render(self, species: Species, spec: JobSpec) -> Sequence[InputFile]:
text = json.dumps(self.molecule(species), indent=2) + "\n"
return [InputFile(f"{species.id}.qcschema.json", text, "application/json")]
#: QCSchema drivers for the tasks the input writer can express.
_DRIVERS: dict[Task, str] = {"sp": "energy", "freq": "hessian"}
[docs]
class QcschemaInputWriter(InputWriter):
name: ClassVar[str] = "qcschema"
description: ClassVar[str] = "QCSchema input (v1) for vibe-qc: energy, or the HF Hessian"
tasks: ClassVar[frozenset[Task]] = frozenset(_DRIVERS)
fields: ClassVar[frozenset[str]] = frozenset({"method", "basis"})
def _refusals(self, species: Species, spec: JobSpec) -> list[str]:
reasons = needs_conformer(self.name, species)
if species.conformers and first_conformer(species).electron_count(species.charge) == 0:
reasons.append(f"species {species.id} has no electrons: there is nothing to compute")
if spec.task in ("opt", "opt+freq"):
reasons.append(
"a QCSchema AtomicInput has no optimisation driver; the 'vibeqc' writer "
"writes optimisations"
)
if spec.basis is None and spec.task != "geometry":
reasons.append("vibe-qc's QCSchema adapter needs a basis set in the model")
if spec.task == "freq" and (spec.method or "").lower() not in ("hf", "scf", "rhf", "uhf"):
reasons.append(
f"vibe-qc's QCSchema adapter computes Hessians for HF only, not {spec.method}; "
"the 'vibeqc' writer covers other methods"
)
return reasons
def _render(self, species: Species, spec: JobSpec) -> Sequence[InputFile]:
from vibesketch import __version__
document = {
"schema_name": "qcschema_input",
"schema_version": 1,
"molecule": QcschemaMoleculeWriter().molecule(species),
"driver": _DRIVERS[spec.task],
"model": {"method": (spec.method or "").lower(), "basis": (spec.basis or "").lower()},
"keywords": {},
"provenance": {
"creator": "vibe-sketch",
"version": __version__,
"routine": "vibesketch.inputs.qcschema",
},
}
text = json.dumps(document, indent=2) + "\n"
return [InputFile(f"{species.id}.qcschema_input.json", text, "application/json")]