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.

  • sections is the list of data blocks. Each has a unique id, a kind from the registry (or an x_<vendor>.* extension), and members mapping 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 equal itemsize × ∏(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.