Adopting QVF: how to implement a writer in your own code¶
This tutorial is for developers of other quantum-chemistry codes who want to
emit QVF (.qvf) files: the format vibe-qc and vibe-view use to carry a
calculation’s structure, wavefunction, spectra, and properties in one
self-describing container. By the end you will understand the container model,
know how to write a conforming archive with the standalone toolkit, and know how
to extend the format for data it doesn’t yet cover.
Nothing here depends on vibe-qc. The toolkit is a self-contained, Apache-2.0 distribution you can vendor into any code, including a proprietary one.
Download the QVF writer toolkit
Everything in this tutorial lives in the qvf-writer toolkit: the normative spec, the JSON Schema, reference writers in Python and C++, a validator, and worked examples.
⬇ Download qvf-writer-0.1.0.tar.gz
See also the format specification, the library guide, and the ORCA integration guide.
1. Why a new format, and what QVF is¶
A single calculation today scatters its results across Cube files, XYZ, Molden, XSF, a log file, and program-specific sidecars. QVF replaces that with one ZIP archive whose members are typed, checksummed, and described by a manifest.
The design rests on six ideas: one shareable artifact; random access (a consumer reads only the members it needs); typed payloads (declared dtype/shape + sha256); a stable vocabulary of section kinds; partial support (a consumer can understand a subset and still open the file); and producer neutrality (the format is not vibe-qc-specific).
2. The container model¶
A .qvf file is a ZIP archive. The only member read by name is
manifest.json at the root; every other member is located through a path
recorded in the manifest. A minimal manifest:
{
"qvf_version": 1,
"source": {"program": "my-code", "version": "1.0", "calculation": "h2o"},
"sections": [
{
"id": "structure",
"kind": "structure",
"members": {
"structure": {
"path": "structure/structure.json",
"format": "json",
"sha256": "…64 hex…"
}
}
}
]
}
The pieces:
source(required) names your program, its version, and the calculation.sectionsis the list of data blocks. Each has a uniqueid, akindfrom the registry (or anx_<vendor>.*extension), andmembersmapping a role to a member spec.A member spec is either JSON (
{path, format:"json", sha256}) or binary ({path, format:"binary", dtype, shape, sha256}). Binary members are raw, C-contiguous, little-endian arrays; the byte length must equalitemsize × ∏(shape).
Two rules make QVF robust: the sha256 of every member is stored in the
manifest, so a consumer verifies integrity before use; and unknown x_<vendor>.*
sections are reported, never misinterpreted, so producers can extend the format
without breaking existing readers.
3. Writing your first archive¶
Grab the toolkit (link above) and write an archive with the Python reference writer: this is the “executable spec”:
from qvf_writer import QvfWriter
w = QvfWriter(program="my-code", version="1.0", calculation="h2o/rhf/sto-3g")
w.add_structure([
{"symbol": "O", "position": [0.0, 0.0, 0.1173], "atomic_number": 8},
{"symbol": "H", "position": [0.0, 0.7572, -0.4692], "atomic_number": 1},
{"symbol": "H", "position": [0.0, -0.7572, -0.4692], "atomic_number": 1},
])
w.add_spectrum("spectra.ir", frequencies=[1595, 3657, 3756],
intensities=[67, 5, 42])
w.set_provenance(method="RHF", basis="STO-3G", scf_energy_eh=-74.963,
scf_converged=True)
w.write("h2o.qvf")
The C++ library mirrors this method-for-method (qvf::QvfWriter), builds with
cmake -S cpp -B build, and has zero external dependencies: it carries its
own ZIP, CRC-32, SHA-256, and JSON emitter, so it drops into an existing build
without new link requirements.
4. Publishing a wavefunction (the one thing to get right)¶
The wavefunction.gto kind carries an atom-centered Gaussian basis plus MO
coefficients, the same content as a Molden file or an ORCA GBW. It is the most
error-prone section to implement, for one reason: primitive normalization.
QVF requires contraction coefficients on unit-normalized primitives. But
integral libraries (libint, libcint, and the storage conventions many codes
inherit) keep coefficients pre-multiplied by the primitive norm
N_i = (2α_i/π)^{3/4}·(4α_i)^{l/2}/√((2l−1)!!). If you write those numbers
verbatim, every orbital renders quantitatively wrong.
The reference writers divide it out for you: just declare which convention your coefficients follow:
w.add_wavefunction_gto(
shells, # {center, l, exponents, coefficients}
mo_coefficients=C, # [n_mo, n_ao], rows are MOs
energies=eps, occupations=occ,
coeffs_are_libint_normalized=True, # divide by N_i on the way out
)
The in-shell AO order is also fixed (spherical m = −l … +l; Cartesian in
libint order). If your code orders AOs differently for a given l, permute the
coefficient rows before writing. Both points are spelled out in Appendix A of
the specification.
5. Extending QVF for data it doesn’t cover¶
Sooner or later you have data with no canonical kind: a bespoke fragment
analysis, an experimental property your code computes and nobody else does. Don’t
force it into an ill-fitting slot; use the vendor namespace. A kind
x_<vendor>.<name> is yours to define, and a declaration in the root
extensions block records its contract:
w.set_extensions({"x_myorg": {"version": "1.0", "critical": False}})
w.add_vendor_section(
"x_myorg.fragment_charges",
json_members={"fragments": [{"atoms": [0, 1, 2], "charge": -0.31}]},
critical=False, # viewers that don't understand it still open the file
)
Keep critical: false unless the section changes how the rest of the file must
be interpreted. When two independent codes emit the same vendor kind with a
stable shape, it becomes a candidate for promotion to a canonical kind through
the lightweight registry process (spec § 7.5); that is exactly how EPR became
the canonical spectra.epr kind after starting life as a vendor extension.
6. Validate everything you write¶
The toolkit ships a validator. Run it on every archive, and wire it into your CI:
python python/qvf_reader.py h2o.qvf # non-zero exit on any problem
It checks sha256 integrity, binary byte sizing, id uniqueness, cross-reference
resolution, and extension governance, and, when jsonschema is installed, full
conformance against the bundled qvf_manifest.schema.json. A clean report means
your file opens in vibe-view and any other QVF consumer.
7. Where to go next¶
The full specification: every section kind, units, and the conformance checklists.
The library guide: build/link/API reference for both writers.
The ORCA integration guide: a complete worked mapping (GBW, spectra, EPR, properties) for a production code.
Open your archive in vibe-view to see it rendered, or read the consumer reference to write your own reader.
That is the whole loop: model your data onto section kinds, write with the toolkit (mind the normalization flag), extend via the vendor namespace when you must, and validate. A conforming producer is a few dozen lines on top of the library.