QVF format specification (v1)

Status: Normative. This document specifies the QVF v1 container format and its producer/consumer contract. The machine-readable contract is the JSON Schema qvf_manifest.schema.json (shipped alongside this file); where prose and schema disagree, the schema wins. This is a self-contained copy of the specification maintained by the vibe-qc project, distributed under Apache-2.0 so that any code can implement a conforming producer or consumer.

QVF (.qvf, “Quantum Visualization Format”) is a ZIP-based container for quantum-chemistry visualization and analysis data. It keeps one calculation’s structure, scalar fields, spectra, bands, trajectories, wavefunctions, provenance, and viewer hints in a single random-access file, instead of scattering them across Cube, XYZ, Molden, XSF, log, and program-specific sidecar files.

The format is intended for the whole quantum-chemistry ecosystem. A QVF producer may be any quantum-chemistry code. A QVF consumer may be a structure viewer, a band plotter, a spectrum tool, a validator, a notebook script, or a GPU viewer.

QVF v1 is deliberately not a restart format. It does not encode integral caches, SCF internal state, engine-internal grids, or every possible wavefunction representation. The optional wavefunction.gto section is a visualization-oriented, Molden-like molecular-orbital carrier, not a universal restart record.

The keywords MUST, MUST NOT, REQUIRED, SHOULD, SHOULD NOT, and MAY are used as in RFC 2119.


1. Design goals

  1. One shareable artifact. A calculation’s visualization data travels as one .qvf file.

  2. Random access. A consumer reads only the members it needs.

  3. Typed payloads. Every member declares format, dtype, shape, and a sha256 checksum where applicable.

  4. Stable common vocabulary. Common data types use canonical section kinds.

  5. Partial support. A consumer can support a subset of kinds and still open the file. Unsupported vendor sections are reported, not misinterpreted.

  6. Producer neutrality. source.program can name any code; core kinds are not producer-specific.


2. Archive model

A QVF file is a ZIP archive. The only mandatory member is manifest.json at the archive root. Every other member is named from the manifest — member paths are part of the manifest contract, not hard-coded by the container.

A typical archive:

example.qvf
├── manifest.json          (REQUIRED — the only member read by name)
├── structure/
│   └── structure.json
├── wavefunction/
│   ├── basis.json
│   ├── mo_metadata.json
│   └── mo_coefficients.dat
├── volumes/
│   ├── density.dat
│   └── density_grid.json
├── spectra/
│   └── ir.json
└── citations/
    └── references.bib

Path layout is not semantic. Consumers MUST follow the path fields in manifest.json and MUST NOT assume ZIP entry order or directory names. Producers SHOULD use stable, readable directory names for debuggability.

2.1 Byte-level container requirements

  • The archive MUST be a valid ZIP (PKZIP APPNOTE, deflate/store methods).

  • manifest.json MUST be present at the archive root (path exactly manifest.json, no leading directory).

  • Producers SHOULD store manifest.json uncompressed (STORE method) so a consumer can read the table of contents cheaply; other members SHOULD be DEFLATE-compressed. Neither is mandatory — a consumer decompresses via the ZIP method recorded in the central directory regardless.

  • Producers SHOULD write manifest.json last (or ensure its recorded checksums match the already-written member bytes). Because each member’s sha256 is embedded in the manifest, all member bytes must be finalized before the manifest is serialized.

  • Member paths MUST use forward slashes, MUST NOT begin with /, and MUST NOT contain any .. path segment. (Schema: ZipPath.)

  • A consumer SHOULD enforce a per-member uncompressed-size cap to guard against zip-bomb inputs. The vibe-qc reference tools cap at 8 GiB (1 gigavoxel of float64) per member.

2.2 Endianness

All binary members are stored in little-endian byte order. QVF v1 dtype names (§ 4.2) do not carry an explicit byte-order mark; little-endian is assumed. Producers on big-endian platforms MUST byte-swap to little-endian before writing. (An explicit byte-order tag is a candidate for a future minor revision; see § 9.)


3. The manifest

3.1 Root object

{
  "qvf_version": 1,
  "schema_uri": "https://vibe-qc.org/spec/qvf/1/manifest.schema.json",
  "source": {
    "program": "my-code",
    "version": "1.2.3",
    "calculation": "h2o_rhf_sto3g"
  },
  "sections": [ /* … */ ]
}

Root field

Required

Meaning

qvf_version

yes

Integer format version. v1 = 1.

source

yes

{program, version, calculation}, all strings; program and version non-empty.

sections

yes

Array of section objects (§ 4).

schema_uri

no

Canonical URI of the JSON Schema. Informational.

provenance

no

Calculation-level metadata (§ 3.2).

viewer_defaults

no

Viewer hints: camera bookmarks, per-section render defaults, auto_open.

thermochemistry

no

Root thermochemistry block (§ 3.3).

dipole_moment

no

Root dipole block (§ 3.3).

constraints

no

Root geometry-constraint block (§ 3.3).

extensions

no

Extension governance block (§ 7).

qvf_version MUST be 1. A consumer that only understands v1 MUST refuse to open a file whose qvf_version is greater than 1.

3.2 provenance

Optional, open (additionalProperties: true). Recognized fields include method, functional, basis (strings); charge, multiplicity, n_electrons, n_scf_iterations (integers); scf_converged (bool); scf_energy, fermi_energy (each a {value, units} object); wall_seconds (number); hostname (string); dimensionality (0–3). A producer emits only what it knows.

3.3 Root metadata blocks

These describe the calculation as a whole (not binary payloads), so they live at the manifest root rather than as sections.

  • thermochemistryzpve_eh, enthalpy_eh, entropy_cal_mol_k, gibbs_free_energy_eh (Hartree / cal·mol⁻¹·K⁻¹), temperature_k, pressure_atm. All optional.

  • dipole_momenttotal_debye, vector_debye (3-vector, Debye), origin ("center_of_mass", "center_of_nuclear_charge", or "origin").

  • constraintsfrozen_atoms (0-based indices), distance_constraints (each {atoms: [i, j], target_angstrom}).


4. Sections and members

4.1 Section object

{
  "id": "vol_dens_0",
  "kind": "volume.density",
  "label": "Electron density",
  "members": { /* role -> member spec */ }
}

Field

Required

Meaning

id

yes

Unique within the archive. ^[A-Za-z0-9_.-]+$.

kind

yes

Canonical kind (§ 5) or x_<vendor>.*.

members

yes

Map from member role to a JSON or binary member spec.

label

no

Human-readable display label.

component

no

Component hint (e.g. for volume.orbital).

critical

no

If true, a consumer that does not support kind MUST refuse to open the archive. Default false (§ 7.4).

schema_uri

no

URI of a JSON Schema for a vendor section’s members.

Some kinds add kind-specific peer fields on the section object (e.g. volume.difference carries operand_a/operand_b; reaction.waypoints carries trajectory_ref). These are documented per kind in § 5.

4.2 Member specs

JSON member:

{ "path": "structure/structure.json", "format": "json", "sha256": "<64 hex>" }

Binary member:

{
  "path": "volumes/density.dat",
  "format": "binary",
  "dtype": "float32",
  "shape": [80, 80, 80],
  "sha256": "<64 hex>"
}
  • format is "json" or "binary".

  • sha256 is the lowercase hex sha256 of the exact bytes stored for that member (the uncompressed payload). Consumers MUST verify it before use.

  • JSON members MUST parse as UTF-8 JSON.

  • Binary members are raw, C-contiguous, little-endian arrays. dtype is one of: int8, int16, int32, int64, uint8, uint16, uint32, uint64, float32, float64. The stored byte length MUST equal itemsize(dtype) × ∏(shape).

  • citations.references is the one binary member with no dtype/shape (it is opaque UTF-8 BibTeX bytes).

4.3 Units and numeric conventions

Producers convert to these units at write time. Consumers MUST treat on-disk units as the contract (they may convert for display).

Field

Unit / convention

structure.atoms[].position

Ångström

structure.lattice_vectors

Ångström, row vectors

volume.*.grid.origin

bohr

volume.*.grid.voxel_vectors

bohr per grid step

volume.density.data

e / bohr³ (electron density)

volume.orbital.data

orbital amplitude on a bohr grid

volume.potential.data

hartree / e (atomic units of potential)

volume.rdg.data

dimensionless, s(r) = |∇ρ| / (2(3π²)^⅓ ρ^⅔)

trajectory.coords, reaction.path.coords

Ångström

trajectory.metadata.energies, reaction.path.waypoints[].energy_eh

Hartree

bands.eigenvalues, bands.kpath.fermi, fermi_energy_ev

eV

spectra.ir.frequencies / intensities

cm⁻¹ / km·mol⁻¹

spectra.uvvis.frequencies / energies_ev

eV

vibrations.metadata.frequencies

cm⁻¹

wavefunction.gto.basis.shells[].exponents

bohr⁻²

wavefunction.gto.mo_metadata.energies

Hartree

dos.*.energies

eV (referenced to E_F at 0.0)

fermi_surface.energies

eV (signed: E(k) − E_F)

phonon_bands.frequencies, phonon_dos.frequencies

cm⁻¹

equation_of_state.volumes / energies

ų / eV

equation_of_state.fit.B0

GPa

When adding fields, prefer unit-bearing names (energy_eh, energy_ev, frequency_cm) or a {value, units} object.


5. Canonical section kinds

Every kind below is a oneOf branch in the schema. A section’s kind selects its required member set. The table lists each kind and its required members; subsections give the payload shapes that matter for interoperability.

Kind

Required members (roles)

structure

structure (JSON); optional bonds (JSON)

bonds

bonds (JSON)

bond_orders

bond_orders (JSON)

volume.density / .orbital / .spin / .elf / .generic / .potential / .rdg

grid (JSON) + data (3-D float32/64 binary)

volume.difference

grid + data; optional section fields operand_a, operand_b, description

wavefunction.gto

basis + mo_metadata (JSON) + mo_coefficients or (mo_coefficients_alpha + mo_coefficients_beta) (float64 binary)

basis.ao

grid + data; section field ao_metadata

atom_properties

≥1 of mulliken_charge, loewdin_charge, spin_population (float64 [n_atoms] binary)

trajectory

metadata (JSON) + coords (float64 [n_frames, n_atoms, 3])

reaction.path

metadata + coords; optional frame_volumes (4-D) + volume_grid

reaction.waypoints

waypoints (JSON); section field trajectory_ref (REQUIRED)

scan.surface

metadata + axis_a + axis_b + energies; optional geometries

vibrations

metadata (JSON) + displacements (float64 [n_modes, n_atoms, 3])

spectra.ir / .raman / .uvvis / .ecd / .vcd / .nmr / .epr / .generic

spectrum (JSON)

bands

kpath (JSON) + eigenvalues (float64 [n_spin, n_k, n_bands]); optional projections

dos.total

energies + dos (binary)

dos.projected

energies + projections (binary)

dos.coop / dos.cohp

energies + projections + integrated + meta

structure.symmetry

data (JSON)

scf_history

iterations (JSON)

citations

references (binary BibTeX)

fermi_surface

mesh (JSON) + energies (4-D float32/64)

phonon_bands

qpath (JSON) + frequencies (binary)

phonon_dos

meta (JSON) + frequencies + dos (binary)

equation_of_state

volumes + energies (binary) + fit (JSON)

topology.qtaim

critical_points (JSON)

x_<vendor>.*

any mix of valid JSON/binary members

5.1 structure and bonds

structure.json:

{
  "atoms": [
    {"symbol": "O", "position": [0.0, 0.0, 0.1173], "atomic_number": 8},
    {"symbol": "H", "position": [0.0, 0.7572, -0.4692], "atomic_number": 1}
  ],
  "pbc": [false, false, false],
  "lattice_vectors": null
}

For periodic systems, lattice_vectors is a [3,3] array of row vectors in Å and pbc marks periodic axes; the section object MAY also carry dimensionality (0–3). Explicit connectivity is a separate bonds section ({"pairs": [{"i": 0, "j": 1, "order": 1.0}, …]}). If absent, a viewer may infer bonds from covalent radii.

The structure section object MAY carry optional biomolecule metadata as peer fields (chains, residues, secondary_structure) for cartoon rendering; consumers that don’t understand them ignore them.

5.2 volume.*

All volume kinds share the same members: a JSON grid and a 3-D data binary (float32 default, float64 opt-in). Grid JSON:

{
  "origin": [-8.0, -8.0, -8.0],
  "voxel_vectors": [[0.2, 0, 0], [0, 0.2, 0], [0, 0, 0.2]],
  "shape": [80, 80, 80]
}

voxel_vectors are three per-voxel step vectors in bohr. For data index (i, j, k) the point position is origin + i·v_i + j·v_j + k·v_k. This is point-centered data: the rendered grid has exactly shape points. Non-orthogonal grids use non-zero off-diagonal components.

volume.difference may add operand_a and operand_b (section ids); if one is present both MUST be. Sign convention: data = operand_a operand_b.

5.3 wavefunction.gto

Carries an atom-centered Gaussian basis and MO coefficient matrices for molecular wavefunctions and the Γ-point (k = 0) crystalline orbitals of periodic systems. See Appendix A for the AO-ordering and primitive-normalization contract — this is the single most error-prone part of implementing a producer, and getting it wrong silently corrupts every rendered orbital.

{
  "kind": "wavefunction.gto",
  "members": {
    "basis":       {"path": "wavefunction/basis.json", "format": "json", "sha256": "…"},
    "mo_metadata": {"path": "wavefunction/mo_metadata.json", "format": "json", "sha256": "…"},
    "mo_coefficients": {
      "path": "wavefunction/mo_coefficients.dat", "format": "binary",
      "dtype": "float64", "shape": [41, 41], "sha256": "…"
    }
  }
}

mo_coefficients shape is [n_mo, n_ao] (rows are MOs). Unrestricted wavefunctions replace it with mo_coefficients_alpha + mo_coefficients_beta. basis.json conventions: structure_ref names the structure section; shells[].center is a 0-based atom index; l is the shell angular momentum; exponents are in bohr⁻²; pure: true = spherical harmonics, false = Cartesian. mo_metadata carries spin ("restricted"/"unrestricted"), orbital_kind ("canonical"/"natural"/"localized"), energies (Hartree), occupations (electron count per MO), and optionally k_point (fractional; absent or [0,0,0] = Γ).

5.4 Spectra

spectra.ir/raman/uvvis/ecd/vcd/generic each carry one JSON spectrum member of the form {"frequencies": [...], "intensities": [...]}; units depend on kind (§ 4.3). spectra.nmr is different — its spectrum is an object with isotope, reference, solvent, chemical_shifts (each {atom_index, symbol, isotropic_shift_ppm}), shielding_tensors, and j_couplings. The NMR payload is intentionally loose in v1.

spectra.epr follows the same object-shaped pattern for electron paramagnetic resonance. Its spectrum is an object carrying the g_tensor (with matrix / isotropic / principal), a hyperfine list (per-nucleus a_iso_mhz / a_tensor_mhz), and a zero_field_splitting (d_mhz / e_mhz). g-values are dimensionless; hyperfine and zero-field-splitting parameters are in MHz. Like NMR, the payload shape is intentionally loose in v1.

5.5 Trajectories, reactions, vibrations

trajectory: JSON metadata (atom identities + optional energies) + coords float64 [n_frames, n_atoms, 3] in Å. reaction.path uses the same layout and adds waypoint records ({frame_index, label, kind, energy_eh?}, kind ∈ reactant/transition_state/intermediate/product/point). reaction.waypoints carries only annotations and references an existing trajectory section via the section-level trajectory_ref. vibrations: JSON metadata (frequencies in cm⁻¹) + displacements float64 [n_modes, n_atoms, 3].

5.6 Bands, DOS, and periodic kinds

bands: JSON kpath + eigenvalues float64 [n_spin, n_k, n_bands] in eV; optional projections [n_k, n_bands, n_channels] for fat bands (with a channels array in kpath). dos.total: energies + dos. dos.projected: energies + projections [n_channels, n_points] (+ meta.channels). fermi_surface: JSON mesh + energies [nk1, nk2, nk3, n_bands]. phonon_bands: JSON qpath + frequencies [n_q, n_modes] (cm⁻¹). phonon_dos: meta + frequencies + dos. equation_of_state: volumes + energies + JSON fit (model ∈ birch_murnaghan/murnaghan/vinet, V0, E0, B0, B0_prime, …).

5.7 Analysis kinds

atom_properties: one or more of mulliken_charge, loewdin_charge, spin_population, each float64 [n_atoms]. bond_orders: JSON {method, pairs: [{i, j, order, distance_ang?, symbol_i?, symbol_j?}]}. topology.qtaim: JSON {points: [{type, position, rho, laplacian, …}], bond_paths?: […]} with type ∈ bcp/rcp/ccp/ncp. structure.symmetry: JSON data (spglib-style summary). scf_history: JSON {iterations: [{iter, energy_eh, delta_e, diis_error}]}. citations: one binary references member of UTF-8 BibTeX bytes. basis.ao: a per-AO grid + ao_metadata (quantum numbers + provenance).


6. Validation semantics

A conforming validator checks:

  • ZIP readability and a per-member uncompressed-size cap.

  • manifest.json exists and parses; conforms to the JSON Schema.

  • Section id values are unique.

  • Every declared member path exists in the archive.

  • Every declared member sha256 matches the stored bytes.

  • Every JSON member parses as UTF-8 JSON.

  • Every binary member’s byte length equals itemsize × ∏(shape).

  • volume.difference.operand_a/operand_b resolve when present.

  • reaction.waypoints.trajectory_ref resolves to a trajectory section.

  • Extension governance (§ 7): a critical: true section whose kind is neither canonical nor a declared vendor namespace is an error.


7. Extension and versioning model

7.1 qvf_version

Integer. v1 = 1. A value > 1 signals breaking changes; a v1-only consumer MUST refuse such files. Minor, backward-compatible growth (new optional kinds, new optional members) does not bump the integer — capability is detected via supported-kind checking and the extensions block.

7.2 Canonical kinds

Canonical kinds appear in the schema and § 5. They may grow in minor revisions; no canonical kind may be removed or have its required members changed without a major version bump.

7.3 Vendor namespace x_<vendor>.*

Any kind matching x_<vendor>.<specific> is a vendor extension. <vendor> SHOULD be a DNS-like identifier (x_orca, x_pyscf, x_vasp). Vendor members must be valid JSON/binary member specs. A consumer MUST list vendor sections as present-but-unsupported unless the extension is critical: true. Unknown non-vendor kinds are not valid QVF.

7.4 Root extensions block and per-section critical

The manifest root may declare vendor extensions:

{ "extensions": { "x_orca": { "version": "1.0", "schema_uri": "…", "critical": false } } }

Each section may carry critical (default false). If critical: true and the consumer does not support the kind, the consumer MUST refuse to open the file rather than silently drop the section. Use critical: true only when the section changes correct interpretation of the rest of the file (e.g. an ECP that alters the effective electron count).

7.5 Promoting a vendor kind to canonical

  1. The kind is used in production by ≥ 2 independent producers for ≥ 1 minor cycle.

  2. A spec PR adds it to § 5, the schema oneOf branches, and the validator.

  3. Its member contract is stable across the two producers.

  4. At least one reference consumer renders it.


8. Conformance

8.1 Producer conformance

A conforming producer MUST:

  • write a valid ZIP with manifest.json at the root;

  • set qvf_version to 1;

  • include source.program, source.version, source.calculation;

  • use unique section ids matching ^[A-Za-z0-9_.-]+$;

  • use canonical kinds or x_<vendor>.*;

  • include only member paths that exist in the ZIP, with no .. segments;

  • compute sha256 over the exact stored bytes of each member;

  • write numeric fields in the units of § 4.3;

  • write binary payloads matching the declared dtype and shape, little-endian;

  • ensure JSON members parse as UTF-8 JSON;

  • declare each x_<vendor> namespace in root extensions when any section in that namespace is critical: true;

  • set critical: true only when consumer support is truly required.

A conforming producer SHOULD include provenance when known; include citations when results depend on citable methods/bases/functionals; prefer a specific volume.* kind over volume.generic; use a vendor namespace for experimental payloads; and run a validator on its own output in tests.

8.2 Consumer conformance

A conforming consumer MUST:

  • read manifest.json by name, not by ZIP entry order;

  • sanity-check qvf_version, source, and sections;

  • verify sha256 before using a member’s bytes;

  • interpret only kinds it supports;

  • report unsupported vendor sections without corrupting or misinterpreting them;

  • respect declared dtype, shape, and units;

  • honor the per-section critical flag and the root extensions critical flags, refusing to open when an unsupported extension is critical.

A consumer MAY require specific kinds for a given operation (a band plotter may refuse a file without bands; a structure viewer may open it and ignore all scalar fields).


9. Known open items (informative)

These are acknowledged limitations of v1, not defects:

  1. Endianness tag. dtype names do not carry byte order; v1 assumes little-endian. A future minor revision may add an explicit prefix.

  2. k-resolved Bloch wavefunctions. wavefunction.gto covers molecular systems and the Γ-point only. A canonical cross-consumer k-resolved section is future work; producers may use x_<vendor>.* meanwhile.

  3. NMR schema tightening. spectra.nmr payload shape is intentionally loose in v1.

  4. Fermi-surface meshes. v1 assumes a Γ-centered uniform Monkhorst-Pack grid.


10. Governance and stewardship

QVF is an open standard. It was created by and is stewarded by the vibe-qc project — that origin is explicit — but the format is producer-neutral by design (§ 1) and its specification and reference implementations are Apache-2.0, so any code may implement a conforming producer or consumer, including a proprietary one, with no obligation back to vibe-qc. vibe-qc’s own non-standard data lives under the x_vibeqc.* vendor namespace, exactly like any other adopter’s.

The governance model, versioning policy, change process, and the vendor→canonical promotion process are described in GOVERNANCE.md; the machine-readable list of canonical kinds and registered vendor namespaces is in registry.json (kept in lock-step with the schema). Stewardship is intended to broaden as independent producers adopt the format.


Appendix A. AO ordering and primitive normalization (REQUIRED for wavefunction.gto)

A wavefunction.gto section is only correct if the MO coefficients, the AO ordering, and the primitive normalization all agree with this appendix. This is the part most producers get subtly wrong.

A.1 Contraction and the normalization convention

Each contracted basis function is reconstructed as

χ(r) = Σ_i c_i · φ_i(r)

where each primitive φ_i is unit-normalized:

φ_i(r) = N_i · (x−A_x)^{l_x} (y−A_y)^{l_y} (z−A_z)^{l_z} · exp(−α_i r²)

N_i = (2 α_i / π)^{3/4} · (4 α_i)^{l/2} / √((2l − 1)!!),   l = l_x + l_y + l_z

The coefficients c_i written into basis.json MUST apply to these unit-normalized primitives.

The trap. Integral libraries such as libint and libcint (used by PySCF, and by many other codes including patterns ORCA developers will recognize from GBW-style storage) store contraction coefficients already pre-multiplied by N_i — i.e. the stored coefficient is c_i · N_i, so it multiplies an un-normalized primitive. If your engine stores coefficients that way, you MUST divide each stored coefficient by N_i before writing it into basis.json. Writing the engine-native (pre-scaled) coefficients produces orbitals that look plausible but are quantitatively wrong, especially for high-l and tightly contracted shells.

Reference implementation of N_i (Python; the C++ toolkit has the identical routine as qvf::primitive_norm):

import math
def primitive_norm(alpha: float, l: int) -> float:
    radial  = (2.0 * alpha / math.pi) ** 0.75
    angular = (4.0 * alpha) ** (l / 2.0)
    df = 1.0
    for k in range(1, 2 * l, 2):   # (2l-1)!!
        df *= k
    return radial * angular / math.sqrt(df)

Both reference writers expose a flag: pass your coefficients as-is if they are already on unit-normalized primitives, or ask the writer to divide by N_i if they are libint/libcint-style. When in doubt, verify against a known orbital: the HOMO of H₂O/STO-3G evaluated on a grid should integrate to ~1 electron per spin.

A.2 AO ordering within a shell

AO order is part of the contract and MUST match the pure flag:

  • Spherical (pure: true): functions are ordered m = −l, −l+1, …, +l.

  • Cartesian (pure: false): libint lexicographic ordering of (l_x, l_y, l_z) with l_x + l_y + l_z = l, decreasing l_x first, then l_y.

The row/column index of an AO in mo_coefficients is its position in the concatenation of all shells (shell order = shells array order), each shell contributing 2l+1 (spherical) or (l+1)(l+2)/2 (Cartesian) functions.

If your engine uses a different in-shell order (common differences: p as x,y,z vs m = −1,0,+1; d as xx,yy,zz,xy,xz,yz vs m = −2…+2), you MUST permute the coefficient rows into QVF order before writing.