Integrating QVF into your quantum-chemistry code¶
QVF is producer-neutral: nothing about the format is vibe-qc-specific, and
any quantum-chemistry code can emit .qvf files. This guide is the
code-agnostic how-to — the patterns, the data mapping, and the one contract you
must get right — so that applications such as vibe-view (the reference
viewer) can read the result sections they support.
For the exact byte-level contract, read the QVF specification
(spec/qvf-format-spec.md). For build/link/API details of the reference writers,
see the library guide. For a complete worked example on a
production code, see the ORCA integration guide.
The whole idea in one paragraph¶
A .qvf file is a ZIP archive with a mandatory manifest.json plus typed
JSON/binary members, one sha256 per member. Each member is a section of a
canonical kind (structure, wavefunction.gto, spectra.ir, bands, …) or a
vendor extension (x_<vendor>.*). A consumer reads only the sections it
understands and reports the rest — so you can emit a subset today and grow it
later without breaking anyone. Producing conforming QVF is a few dozen lines on
top of a reference writer.
Two integration models — pick either¶
A. In-process. Link the zero-dependency C++ library (qvf::QvfWriter) into
your code, or import the Python writer, and emit the archive at the end of a
run straight from your in-memory data structures. Best when you own the code and
want the richest output (grids, wavefunctions) without re-parsing.
B. Post-processor. A small standalone tool reads your code’s existing output (log file, binary wavefunction file, native JSON) and calls a writer. Zero changes to your core; the fastest way to prototype and to support a code you don’t build yourself.
Both can produce archives conforming to the same contract. Consumers support different section subsets; check the intended consumer’s capabilities. Many teams start with a post-processor and later move the hot paths in-process.
Step 1 — decide what to emit¶
Map your code’s outputs onto canonical section kinds. This table is by data category, not by code — find your data on the left, emit the kind on the right:
Your data |
QVF target |
Notes |
|---|---|---|
Geometry (Cartesians) |
|
positions in Å |
Unit cell / PBC |
|
row vectors, Å; |
Basis + MO coefficients |
|
see Step 2 |
Basis + Bloch orbitals at several k-points (periodic) |
|
complex coefficients |
Atomic charges (Mulliken, Löwdin, Hirshfeld, IAO, CM5, CHELPG, RESP, ESP, NBO, MBIS, Bader, DDEC), spin populations, valences, Fukui indices, atomic energies, volumes, NMR shieldings, magnetic moments, per-atom gradients |
|
one member per role: float64 |
Bond orders (Mayer / Wiberg) |
|
quantitative per-pair table |
Normal modes |
|
frequencies cm⁻¹; |
Nuclear gradient / forces, stress |
|
store the gradient dE/dR in hartree/bohr (forces = −gradient); stress hartree/bohr³ |
Cartesian Hessian |
|
|
Excited states (TD-DFT, CIS, ADC, EOM-CC): energies, oscillator / rotatory strengths, transition dipoles, orbital contributions |
|
one JSON table; excitation energies in eV, dipoles in atomic units; |
Natural transition orbitals |
|
|
Transition density on a grid |
|
e/bohr³, bohr grid; |
Dipole (restated), quadrupole, octupole, polarizability, hyperpolarizability |
|
one JSON member |
Elastic constants |
|
symmetric 6 × 6 |
Dielectric tensors, Born charges, piezoelectric tensor |
|
|
IR / Raman / UV-Vis / ECD / VCD |
|
frequency + intensity |
NMR / EPR |
|
object-shaped payload |
SCF convergence trace |
|
per-iteration energy / error |
Method / basis / functional / energy / convergence |
root |
also what produced it: |
Total energy, components, energies by method |
root |
|
Dipole, thermochemistry, constraints |
root metadata |
|
Density / orbital / ESP / RDG grids |
|
bohr grid |
Laplacian of the density |
|
e/bohr⁵ |
Vector fields (density gradient, current density, electric field) |
|
|
Complex orbitals / Bloch functions on a grid |
|
|
Densities per frame, per time step or per state |
|
|
Periodic grids (CHGCAR, periodic cube) |
|
declare |
Optimization / IRC frames |
|
coords in Å; per-frame |
Molecular dynamics (NVE / NVT / NPT / NPH) |
|
coords Å (float32 allowed with |
Bands / DOS / phonons / EOS / Fermi surface |
|
periodic |
Level-of-theory references |
|
BibTeX bytes |
Verbatim input + full log/output |
|
UTF-8 text; |
Declarative job request (pending container) |
|
data, never code; pair with |
Anything with no canonical kind |
|
see Step 4 |
Start small. The highest-value first cut for most codes is structure +
wavefunction.gto + whatever spectra you compute + root provenance. Add kinds
incrementally; partial support is a first-class feature of the format, not a
limitation.
Before an archive is shared, keep machine names out of root provenance
(hostname is deprecated, spec § 3.2, and the reference writers drop it by
default) and remember that a run.record’s verbatim input, log, and
attachments typically carry absolute paths, user names, and host names — see
“Sharing and sanitisation” in spec § 5.8.
A pending container and its provenance. To hand a job to a runner, write the
input structure, a job.spec and provenance.run_status = "pending"; the
spec’s structure_ref names the geometry to run, which matters as soon as the
archive holds more than one structure. A functional or basis of null
says the method takes none (Hartree–Fock has no functional, GFN2-xTB no basis
set), which is not the same as leaving the key out, the engine’s default
(§ 5.9). resources (n_cores, memory_mb,
walltime_s) are hints, not requirements, and engine keywords belong under
options. When the job has run, record in provenance what actually
produced the result: software (your code and the libraries that shaped the
numbers), git_commit (your own revision, 7 to 64 hex characters),
n_threads, n_mpi_ranks, n_gpus and the UTC started_utc /
finished_utc. Keep machine names out of software[].url.
w.add_job_spec(job_type="periodic", method="rks", functional="pbe",
basis="pob-tzvp-rev2", kpoints=[4, 4, 1], tasks=["optimize"],
structure_ref="slab", dispersion="d3bj",
scf={"convergence": 1e-8, "max_iterations": 100},
resources={"n_cores": 16, "walltime_s": 86400})
w.set_provenance(run_status="converged", git_commit="9f2b6c1d4e7a", n_threads=8,
software=[{"name": "my-code", "version": "1.2.3"}],
started_utc="2026-10-07T12:00:00Z",
finished_utc="2026-10-07T12:07:31Z")
Step 2 — the wavefunction contract (the one thing to get right)¶
wavefunction.gto carries an atom-centered Gaussian basis + MO coefficients (the
Molden / GBW equivalent). It is the single most error-prone section, for one
reason: primitive normalization.
QVF requires contraction coefficients that apply to N_i-normalized
primitives — N_i is the axial norm of spec Appendix A.1, one factor per
shell derived from the total l:
χ(r) = Σ_i c_i · φ_i(r), φ_i = N_i · (monomial) · exp(−α_i r²),
N_i = (2 α_i / π)^{3/4} · (4 α_i)^{l/2} / √((2l−1)!!), l = l_x + l_y + l_z
For spherical shells and the axial Cartesian components this coincides with
unit normalization; for mixed Cartesian components (pure: false shells) it
deliberately does not — re-read spec Appendix A.1 before emitting those.
Integral libraries — libint and libcint (the latter is what PySCF uses),
and the storage conventions many codes inherit — keep coefficients
pre-multiplied by N_i. If you write those numbers verbatim, every rendered
orbital is quantitatively wrong (worst for tight and high-l shells) while still
looking plausible.
Both reference writers do the divide for you — you just declare which convention your coefficients follow:
w.add_wavefunction_gto(shells, mo_coefficients=C, energies=eps, occupations=occ,
coeffs_are_libint_normalized=True) # divide by N_i on write
Also fix the AO ordering: spherical shells run m = −l … +l; Cartesian shells
use libint lexicographic 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.
Emit the overlap matrix, and let the validator verify it. Pass the AO overlap
matrix your integral code computed for the basis you wrote (the AOs as written,
after any N_i divide) as the optional overlap member:
w.add_wavefunction_gto(shells, mo_coefficients=C, energies=eps, occupations=occ,
coeffs_are_libint_normalized=True,
overlap=S) # [n_ao, n_ao]
Producers SHOULD write it. Every reference validator then recomputes the
self-overlap of each AO from basis.json (spec Appendix A.1, Verifying with
overlap) and compares it with the diagonal of S; a wrong normalization
flag, an AO order that is not Appendix A.2, or a Cartesian d_xy normalized on
its own (diagonal 1 instead of 1/3) fails with E_NORMALIZATION. It is the one
check that catches the normalization trap at write time rather than in a
rendered orbital. A further sanity check: evaluate the HOMO of H₂O/STO-3G on a
grid and integrate |ψ|² — it must come out ≈ 1 per spin.
Effective core potentials and the electron count. If your basis replaces
core electrons by an ECP, write each potential as a basis.ecp entry
(center, n_core_electrons, label, and the radial terms
c r^(n−2) exp(−α r²) of its local part and semi-local channels; spec § 5.3)
and state n_electrons, the electrons the wavefunction describes (Σ Z − Σ n_core_electrons − charge). The occupations must sum to it, and
provenance.n_electrons, when you write it, must agree; validators check both.
Optional density_matrix (or density_matrix_alpha + density_matrix_beta)
members carry the one-particle density matrix, and symmetry_labels the
irreducible representation of each MO.
Step 3 — write, then validate¶
Use a reference writer (Python or C++), or reimplement the producer contract directly from the spec (§ “Producer conformance”). Either way, validate every archive you produce, and wire it into your CI:
python qvf_reader.py your_output.qvf # non-zero exit on any problem
The validator checks sha256 integrity, binary byte-sizing, id uniqueness,
cross-reference resolution, extension governance and the wavefunction
normalization against an overlap member — and, with jsonschema
installed, full conformance against the bundled qvf_manifest.schema.json. A
clean report confirms the checks performed on the archive. It does not
guarantee that every consumer can execute, display or preserve every section,
or establish scientific accuracy. The family workflow
shows a supported pending-job path and the current consumer limits.
Periodic systems¶
wavefunction.gtocarries molecular orbitals and the Γ-point (k = 0) crystalline orbitals over the cell’s GTO basis. Orbitals at several k-points (or one k-point other than Γ) go intowavefunction.bloch(spec § 5.3.1): the samebasispayload, akpointspayload (fractional reciprocal-lattice coordinates, integration weights summing to 1), complexmo_coefficients[n_k, n_spin, n_mo, n_ao, 2]and per-kenergies(hartree) andoccupations. The Bloch sum is φ_μk(r) = N^(−1/2) Σ_T e^{+ik·T} χ_μ(r − T); if your code uses the opposite phase sign, or puts the atom position into the phase, convert the coefficients before writing. The kind is provisional: promoted ahead of the two-producer threshold, it becomes final when a second producer ships it, so a periodic Gaussian code that adopts it is exactly what it needs. k-resolved density matrices, spinor Bloch orbitals and non-Gaussian bases have no canonical kind yet; use a vendor section for those.Use
bands(k-path eigenvalues),dos.total/dos.projected,fermi_surface,phonon_bands/phonon_dos, andequation_of_statefor the usual periodic analyses. Densities and potentials go throughvolume.*grids.A grid that samples the cell declares
periodicandincludes_endpoint(spec § 5.2). Cube files repeat the first plane as the last (includes_endpoint: true, the default:voxel_vectors[a] × (shape[a] − 1)is lattice vectora); CHGCAR files do not (includes_endpoint: false:voxel_vectors[a] × shape[a]is). Write what your points are; the validator checks the span against the structure’s cell, and consumers drop the duplicated plane when they tile a cube-style grid.The cell is stored in two conventions (
structure.lattice_vectors: rows, Å;reaction.path.lattice: columns, bohr), so convert deliberately; see “Lattice conventions” in spec § 4.3 and theLatticehelper in the writer libraries.
Several structures, several steps, and archive metadata¶
An archive may hold more than one structure, for example an ensemble, or the
start and end geometries of a workflow. Say which structure each result belongs
to with a section-level structure_ref, and which program run produced it with
run_ref naming a run.record; a free group label ("step 2: frequencies")
lets a viewer show related sections together (spec § 4.1). The validator
rejects a reference that names a section of the wrong kind. A section’s
structure_ref takes precedence over the one inside a wavefunction.gto
basis payload, so keep the two equal. The writers set these on an already
added section, so no add_* call changes:
w.annotate_section(rho, structure_ref=h2o, run_ref=run, group="water")
The optional root metadata block describes the archive itself: title,
authors, license (an SPDX identifier such as "CC-BY-4.0"), doi,
keywords, derived_from, and the created_utc / uuid pair that identifies
it (spec § 3.5). The reference writers add created_utc and a random uuid by
default; pass auto_metadata=False (C++: w.auto_metadata = false) when you
need a manifest that is the same on every run, as for test fixtures. Leave authors’ email and
affiliation out unless the person asks for them: archives get shared.
Suggesting how the file opens¶
The optional root viewer_defaults object carries hints for viewers (spec
§ 3.4): auto_open lists the sections to show first, bookmarks holds camera
bookmarks, camera the camera to open with (a bookmark’s camera), crossfade
two volume sections to blend ({"a", "b", "blend"}), and every other key is a
section id whose value suggests how that section is drawn: isovalue,
colormap, opacity, replication, for a volume.* or basis.ao section
render and transfer_function, and for a bands or dos.* section
energy_window, the [min, max] range in eV relative to the Fermi level it is
first plotted over. Hints are
suggestions. A consumer may ignore any of them and never refuses a file over a
malformed one, so a wrong hint costs you the look you wanted, not the file.
A spin density that opens as a volume, with a diverging palette and both lobes visible:
spin = w.add_volume("volume.spin", spin_density, origin, voxel_vectors,
section_id="spin")
w.set_viewer_defaults({
spin: {
"render": "volume", # "isosurface" is the default
"transfer_function": {
"range": [-0.02, 0.02], # in the section's data units
"color": [[-0.02, 0.23, 0.30, 0.75], [0.0, 0.95, 0.95, 0.95],
[0.02, 0.71, 0.02, 0.15]], # [x, r, g, b]
"opacity": [[-0.02, 0.5], [-0.002, 0.0],
[0.002, 0.0], [0.02, 0.5]], # [x, a]
"colormap": "custom", # draw these colour nodes as given
},
},
})
Without "colormap": "custom" the palette follows the section’s colormap
hint. Never give a section the id auto_open, bookmarks, camera or
crossfade.
An all-electron band structure whose core states would squeeze the valence region into a few pixels opens on the valence region, at a saved camera:
bands = w.add_bands(kpath, eigenvalues) # eigenvalues in eV
w.set_viewer_defaults({
"camera": {"position": [0.0, -12.0, 4.0], "focal_point": [0.0, 0.0, 0.0],
"view_up": [0.0, 0.0, 1.0], "view_angle": 30.0},
bands: {"energy_window": [-15.0, 10.0]}, # eV relative to the Fermi level
})
Several molecules, a reaction scheme and a picture¶
An archive that holds several species, such as the reactants and products of
a reaction, says which molecule each structure is: its charge,
multiplicity, name, smiles, inchi and inchikey go on the section
object, and its label is the compound number (spec § 5.1). A
reaction.scheme section relates the structures as the steps of a scheme
(§ 5.14), and a preview section carries a thumbnail that file managers show
(§ 5.15). Several job.spec sections, each naming its structure, ask for one
calculation per species, and each run.record names the job it ran with
job_ref (§ 5.9). Connectivity belongs to one structure: write it as the
structure’s own bonds member, or give a standalone bonds section the
structure_ref of its structure (add_bonds(pairs, structure_ref=...));
without one, a bonds section belongs to the archive’s first structure
(§ 5.1). The library guide
shows the calls.
Declaring the file type¶
A program that opens or saves .qvf files declares the type the same way as
every other (spec § 2.4): the extension .qvf, the media type
application/vnd.vibe-qc.qvf+zip, and on macOS the Uniform Type Identifier
com.vibe-qc.qvf. Copy the declarations in spec/file-type/
unchanged: qvf-uti.plist holds the dictionary for Info.plist
(UTImportedTypeDeclarations, or UTExportedTypeDeclarations in the one
application that exports it) and qvf-mime.xml the shared-mime-info
definition for Linux desktops.
Step 4 — extending QVF for code-specific data¶
When you have data with no canonical kind, don’t force it into an ill-fitting
slot. Use the vendor namespace: a kind x_<yourcode>.<name> is yours to
define, declared in the root extensions block:
w.set_extensions({"x_yourcode": {"version": "1.0", "critical": False}})
w.add_vendor_section("x_yourcode.my_analysis", json_members={"...": ...},
critical=False) # false: consumers may skip it and 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 via
the lightweight registry process (spec § 5.7) — that is exactly how EPR became
the canonical spectra.epr kind. If a payload you need is common enough to
belong in the standard, that path is open.
Becoming an officially supported producer¶
Interop is a two-way street, and we actively want more producers:
Emit valid QVF for your common outputs and validate it in your CI.
Self-certify against the conformance suite — a language-agnostic set of frozen reference archives + expected decoded values + a runner. Certifying a consumer is running the checks over the corpus; certifying a producer is emitting the same logical calculations with your code and checking they decode to the expected values. This is how “conforming producer” becomes something you can prove, not just assert.
Get listed. Once your code emits conforming archives, vibe-view can officially support your output — reach out and we’ll add your code to the supported-producer list and, if useful, write a worked integration guide for it (like the ORCA one).
Promote what you need. If you rely on a vendor kind, or a canonical kind is missing a field, tell us — the format grows with its producers.
The reverse direction is also on the table: if your code has a native output format you’d like vibe-view to read directly (in addition to QVF), that’s a conversation worth having too.
Calling the writer from other languages¶
Python:
from qvf import Writer, Reader, Editor. Install the toolkit’spython/directory orqvf-writer[numpy,validate]for numeric convenience operations and full JSON Schema checks. The standard-library core works without either extra. Existingqvf_writerandqvf_readerimports and theqvf-validatecommand remain available. Keep the entireqvfpackage and its schema when vendoring; the top-level compatibility modules alone are not standalone implementations.C++: link the static library;
#include "qvf/qvf.hpp". Zero external dependencies, C++17.C / Fortran / others: use the bundled C ABI (
qvf/qvf_c.h) — a stableextern "C"surface over one opaque handle (qvf_create,qvf_add_structure, …,qvf_write,qvf_destroy), with nested payloads passed as JSON strings. Fortran calls it viaISO_C_BINDINGbind(C)interfaces. See the library guide’s “C and Fortran” section andcpp/examples/write_h2o_c.c.
Worked examples¶
ORCA integration guide — GBW → wavefunction (with the normalization caveat), spectra parsing, EPR, scalar properties.
The reference writers themselves:
python/examples/write_h2o.pyandcpp/examples/write_h2o.cppin this toolkit are complete, runnable producers you can copy from.
A note for PySCF-style Python codes¶
Because PySCF is pure Python and uses libcint, the Python reference writer drops
straight in. The structure, spectra.*, atom_properties, and provenance
sections are unambiguous; for wavefunction.gto, build the shells from the
molecule’s basis and pass coeffs_are_libint_normalized=True (libcint stores
coefficients on un-normalized primitives, so the writer’s N_i divide applies),
then verify with the |ψ|² integral above. This is the shape of a
few-dozen-line adapter that turns any PySCF result into a .qvf a user can open
in vibe-view.