QVF writer library guide¶
This guide covers building, linking, and calling the two reference writers in
this toolkit: the C++17 library (qvf::QvfWriter) and the Python
writer (qvf_writer.QvfWriter). Both implement the same producer contract
described in the QVF specification (spec/qvf-format-spec.md); pick whichever
matches your host code.
Contents¶
Which writer do I use?¶
C++ library |
Python writer |
|
|---|---|---|
File |
|
|
Dependencies |
C++17 standard library only |
Python stdlib + NumPy |
Best for |
linking into a C/C++ code (ORCA, Turbomole, …) |
scripts, post-processing, prototyping |
Compression |
ZIP DEFLATE (per-member, STORE if smaller) |
ZIP DEFLATE |
Both produce byte-for-byte-identical binary payloads (raw little-endian IEEE arrays) and semantically identical manifests; they interoperate with any QVF consumer, including vibe-view.
The C++ library¶
Building¶
cmake -S cpp -B build # no find_package calls; zero external deps
cmake --build build
ctest --test-dir build # runs the known-answer + smoke tests
./build/write_h2o out.qvf # the worked example
Requires only CMake ≥ 3.16 and a C++17 compiler. There is nothing to install and no library to find — the ZIP writer, CRC-32, SHA-256, and JSON emitter are all self-contained in the toolkit.
Linking into your project¶
The library is small enough to vendor directly. Two options:
A. add_subdirectory. Drop the qvf-writer/cpp tree into your source and:
add_subdirectory(third_party/qvf-writer/cpp)
target_link_libraries(your_target PRIVATE qvf::qvf)
B. Add the sources directly. Compile cpp/src/qvf.cpp into your build and
add cpp/include and cpp/src to the include path. That is the entire library
— one translation unit plus header-only helpers.
Then:
#include "qvf/qvf.hpp"
C. Single-header drop-in. For the lowest-friction vendoring, grab the one
amalgamated file cpp/qvf_single.hpp (STB-style). In exactly one translation
unit define QVF_IMPLEMENTATION before including it; elsewhere just include it:
// in one .cpp:
#define QVF_IMPLEMENTATION
#include "qvf_single.hpp"
// anywhere else:
#include "qvf_single.hpp"
No CMake, no separate compile step, no include-path juggling — one file, C++17.
The header is generated from the cpp/ sources by scripts/amalgamate.py (a
drift test keeps the committed copy current); see cpp/examples/write_single_header.cpp.
D. find_package / FetchContent. The library installs a CMake package, so
after cmake --install (or a system package) adopters can:
find_package(qvf CONFIG REQUIRED)
target_link_libraries(your_target PRIVATE qvf::qvf)
or pull the independently versioned QVF repository without an install step.
Its CMake project is at cpp/ directly under the repository root. The
source repository URL is a build setting. Set QVF_SOURCE_REPOSITORY to
the repository you can access and configure Git credentials if needed. The
public publication URL is https://github.com/vibe-qc/qvf.git; it becomes
usable after the source snapshot is approved. Pin a reviewed revision explicitly:
include(FetchContent)
FetchContent_Declare(qvf
GIT_REPOSITORY "${QVF_SOURCE_REPOSITORY}"
GIT_TAG v0.1.0
SOURCE_SUBDIR cpp)
FetchContent_MakeAvailable(qvf)
target_link_libraries(your_target PRIVATE qvf::qvf)
The QvfWriter API¶
Every add_* method returns the section id it created (useful for
volume.difference operands and reaction.waypoints references). Construct a
writer, add sections and root metadata in any order, then write().
Section-creation calls check duplicate, reserved and invalid IDs before
writing payload members; a rejected ID does not leave orphan ZIP entries.
The add_atom_property helpers can still add a new role to an existing
atom_properties section.
WavefunctionGTO::localization_reference_basis is an optional non-empty
reference-basis name. add_job_spec preserves explicit JSON null for
basis / functional, distinct from omitting the field. The Python writer
uses localization_reference_basis=, and add_job_spec(basis=None, functional=None) writes null; leaving either keyword out omits it.
#include "qvf/qvf.hpp"
using namespace qvf;
QvfWriter w({"my-code", "1.2.3", "job_name"}); // Source{program, version, calc}
// structure (positions in Ångström)
std::string sid = w.add_structure({
{"O", 8, {{0.0, 0.0, 0.117}}},
{"H", 1, {{0.0, 0.757, -0.469}}},
{"H", 1, {{0.0, -0.757, -0.469}}},
});
// a spectrum
Spectrum ir; ir.frequencies = {1595, 3657, 3756}; ir.intensities = {67, 5, 42};
w.add_spectrum("spectra.ir", ir);
// provenance at the manifest root
Json prov = Json::object();
prov.set("method", "RHF").set("basis", "STO-3G").set("scf_converged", true);
w.set_provenance(prov);
w.write("result.qvf");
Method families (see the header for exact signatures):
Structure:
add_structure,add_bonds,add_bond_orders.Volumes:
add_volume(kind, grid, data, …)forvolume.density/orbital/spin/elf/generic/potential/rdg/laplacian(3-D data),volume.vector([nx, ny, nz, 3]) andvolume.complex([nx, ny, nz, 2], last axis[re, im]);add_volume_series(grid, data, axis, …)forvolume.series([n, nx, ny, nz]plus itsaxisJSON);add_volume_difference;add_basis_ao.Gridcarries the optionalperiodic,includes_endpointandstructure_refof a periodic grid (see Volumes and periodic grids).Wavefunction:
add_wavefunction_gto(WavefunctionGTO)(restricted, unrestricted,spin = "restricted_open"or"generalized"; see the normalization note below). Optional fields (spec § 5.3):ecp(a JSON array of effective core potentials),n_electrons(the written occupations must sum to it),symmetry_labels(orsymmetry_labels_alpha/_beta),complex_coefficients(each coefficientMatrixthen holdsrows * cols * 2interleaved re/im doubles; ageneralizedwavefunction needs it and2 n_aocolumns), and the[n_ao, n_ao]matricesoverlap,density_matrixordensity_matrix_alpha+density_matrix_beta.set_wavefunction_overlapandset_wavefunction_ecpadd the overlap and the ECPs to a section that is already written.add_wavefunction_bloch(WavefunctionBloch)writes the k-resolved Bloch orbitals of a periodic system (wavefunction.bloch, provisional, spec § 5.3.1): the same shells and normalization flag, k-points in fractional reciprocal-lattice coordinates with optional weights, row-major coefficients[n_k, n_spin, n_mo, n_ao, 2](last axis real, imaginary), energies (hartree) and occupations[n_k, n_spin, n_mo]. Sizes that disagree, or weights that do not sum to 1, throw.Spectra:
add_spectrum(kind, Spectrum)for 1-D spectra, oradd_spectrum(kind, Json)forspectra.nmrand other object payloads.Bands / DOS:
add_bands(kpath, eigenvalues, projections?, occupations?)— eigenvalues[n_spin, n_k, n_bands]; fat-band projections[n_spin, n_k, n_bands, n_channels], or the legacy[n_k, n_bands, n_channels], which meansn_spin = 1; occupations[n_spin, n_k, n_bands].add_dos_total;add_dos_projectedwith projections[n_spin, n_channels, n_points]or the legacy[n_channels, n_points];add_dos_coop_cohp. Shapes that disagree with spec § 5.6 throw.Trajectories / reactions / scans / vibrations:
add_trajectory,add_reaction_path,add_reaction_waypoints,add_scan_surface,add_vibrations(an optional last argumenthessian_refnames thehessiansection the modes come from).add_trajectory(metadata, coords, extras)takes aTrajectoryExtrasof optionalconst Tensor*per-frame members (latticein rows and Å,velocities,gradient,times,energies,cell_volumes,temperatures), and overloads take the cell as oneLatticeor astd::vector<Lattice>(one per frame); float32coordswrite"precision": "float32"into the metadata.add_reaction_path(..., lattice, extras)takes the sharedvelocities,gradient,timesandenergies(see Trajectories and MD).Energy derivatives:
add_forces(gradient, stress*, meta)stores the energy gradient dE/dR, float64[n_atoms, 3]in hartree/bohr (the forces are its negative), with an optional[3, 3]stress tensor in hartree/bohr³ for periodic systems;add_hessian(matrix, meta)stores the[3N, 3N]Cartesian Hessian in hartree/bohr² (spec § 5.10, § 5.11). Wrong shapes throw before anything is written.Excited states:
add_excited_states(states, meta, wavefunction_ref)writes theexcited_statestable (spec § 5.12) from a JSON array of states in ascendingindexorder; a numberexcitation_energyis written in eV and a numbertotal_energyin hartree.nto_ref/density_refmust name sections already added, and withwavefunction_refevery contribution’sfrom_mo/to_momust be an orbital row of that wavefunction. Natural transition orbitals are twoWavefunctionGTOsections withorbital_kind = "nto",nto_role = "hole"/"particle",state_refandstate_index;add_volume_transition_density(grid, data, excited_states_ref, state_index)writes the transition density. A 1-D spectrum names the table with astates_refin its payload.Property kinds:
add_electric_properties(json)(dipole in debye, quadrupole in debye·Å with itsconventionandtracelessflag, octupole, polarizability and hyperpolarizability in atomic units),add_elastic_properties(json)(the symmetric 6 × 6stiffnessin Voigt order and GPa, optionalcompliance, moduli and theiraverage) andadd_dielectric_properties(json)(electronic,ionicandstatictensors,born_chargesone 3 × 3 per atom in e,piezoelectricin C/m²) write theproperties.electric,properties.elasticandproperties.dielectrickinds of spec § 5.13. Each takes the payload as JSON, checks its shapes, required fields, enumerations, symmetry and the static = electronic + ionic rule, and throws before anything is written. A dipole that the rootdipole_momentalso states must agree with it to 1e-6 debye, and the Born tensors must number the atoms; the validators check both.Analysis / periodic:
add_atom_properties,add_scf_history,add_citations,add_structure_symmetry,add_fermi_surface,add_phonon_bands,add_phonon_dos,add_equation_of_state,add_topology_qtaim. Besides the Mulliken / Löwdin / spin-population vectors,add_atom_propertiestakes astd::mapfrom role name to values for any scalar role (see the cheat sheet), andadd_atom_propertyappends one scalar role to the section. The vector roles (magnetic_moment,force_eh_bohr,[n_atoms, 3]) go throughadd_atom_vector_property. Acustom.<name>role needs its description first: callset_atom_properties_metawith{"custom.<name>": {units, description, kind}}, and the writer emits the section’smetamember.Run record:
add_run_record— the verbatim program input and/or full log (UTF-8) plus attachments;programnames the code that ran, making the archive a self-contained record of the calculation.Job spec:
add_job_spec— the declarative specification of the calculation the archive requests (JobSpecPayload JSON:job_typerequired, plus method/basis/functional/charge/multiplicity/kpoints/tasks/ options, and the typedstructure_ref,solvation,dispersion,relativistic,scf,geometryandresources; the last are hints, not requirements). The typed fields are checked and the call throws on a wrong one. Pair withset_provenance({... "run_status": "pending" ...})for a container describing a job that has not yet run.Root metadata:
set_provenance,set_thermochemistry,set_dipole_moment,set_constraints,set_energies(the rootenergiesblock, spec § 3.6),set_extensions,set_viewer_defaults(viewer hints, spec § 3.4; example under Building JSON payloads).set_provenancedrops ahostnamekey, which is deprecated (spec § 3.2) because archives are meant to be shared; callset_provenance(prov, /*allow_hostname=*/true)to keep it. It also checks the typed fieldssoftware({name, version?, url?}items),git_commit(7 to 64 hex characters),n_threads,n_mpi_ranks,n_gpusand the UTCstarted_utc/finished_utc, and throws on a wrong one.Archive metadata:
set_metadata(Json)sets the rootmetadatablock (spec § 3.5: title, authors, licence, DOI, keywords, …). By defaultbuild_manifest()also addscreated_utcand a random version-4uuidwhen the block lacks them, made once per writer; set the public fieldw.auto_metadata = falsefor a manifest that is the same on every run.Section references:
add_bonds(pairs, section_id, structure_ref)names the structure whose atoms a standalonebondssection addresses (spec § 5.1; without it the bonds belong to the first structure).annotate_section(id, structure_ref, run_ref, group)marks an already added section with thestructureit describes, therun.recordthat produced it and a display group (spec § 4.1; an empty string leaves a field alone);set_section_field(id, key, value)sets any other section field. A reference to a missing section or one of the wrong kind throws.Escape hatch:
add_vendor_section("x_<vendor>.*", members, critical?); an overload takesstd::vector<OpaqueMember>({role, bytes, media_type}) for members stored without dtype and shape, andadd_opaque_member(section_id, role, bytes, media_type)adds one to an existing vendor section (spec § 4.2, § 7.3).Sketch documents (spec § 5.1, § 5.9, § 5.14, § 5.15):
set_structure_identity(id, Json)gives astructuresection itscharge,multiplicity,name,smiles,inchiandinchikey(a null value removes one; a charge and multiplicity impossible for the atoms throw);add_reaction_scheme(steps, label, section_id)takes the steps as a JSON array, each participant a structure id or{structure_ref, coefficient};add_preview(thumbnail, image, label, section_id, thumbnail_media_type, image_media_type)takes PNG or JPEG bytes and an optional SVG, PNG or JPEG image, sniffs and checks the media types and stores both members uncompressed;RunRecord::job_refandRunRecord::run_statusname the job a run executed and its outcome, andadd_job_specwrites astructure_refon the section object as well as in the payload. Labels of other sections go throughset_section_field(id, "label", …).
The wavefunction normalization flag¶
WavefunctionGTO::coeffs_are_libint_normalized is the one flag you must get
right (see spec Appendix A). If your engine stores contraction coefficients
already multiplied by the primitive norm N_i (libint / libcint / GBW-style
storage), set it to true and the writer divides them out so they apply to
N_i-normalized primitives, as QVF requires — N_i is spec Appendix A.1’s
axial norm, one factor per shell from the total l, not a per-component
unit norm. If your coefficients already apply to N_i-normalized primitives,
leave it false.
Then write the AO overlap matrix (WavefunctionGTO::overlap, computed by
your integral code for the AOs as written): every validator compares its
diagonal with the self-overlaps spec Appendix A.1 gives for the stored basis,
so a wrong flag, a wrong AO order or a per-component Cartesian normalization
fails validation with E_NORMALIZATION instead of producing plausible but
wrong orbitals.
Building JSON payloads¶
qvf::Json is a tiny ordered JSON value used for flexible members (spectra.nmr
payloads, EOS fit, DOS metadata, viewer defaults, vendor sections):
Json nmr = Json::object();
Json shifts = Json::array();
Json cs = Json::object();
cs.set("atom_index", 1).set("symbol", "H").set("isotropic_shift_ppm", 4.6);
shifts.push_back(cs);
nmr.set("isotope", "1H").set("reference", "TMS").set("chemical_shifts", shifts);
w.add_spectrum("spectra.nmr", nmr);
Helpers: Json::from_doubles(vec), Json::from_ints(vec),
Json::from_strings(vec), Json::array(), Json::object(), .set(key, value),
.push_back(value).
Viewer hints are built the same way. set_viewer_defaults takes the whole
object; each key other than the reserved auto_open, bookmarks, camera
and crossfade is a section id. This
one opens a spin density as a volume with its own transfer function: nodes are
[x, r, g, b] and [x, a] with x in the section’s data units, and
"colormap": "custom" draws the colour nodes as given:
std::string spin = w.add_volume("volume.spin", grid, spin_density, "", "", "spin");
Json color = Json::array();
color.push_back(Json::from_doubles({-0.02, 0.23, 0.30, 0.75}))
.push_back(Json::from_doubles({0.0, 0.95, 0.95, 0.95}))
.push_back(Json::from_doubles({0.02, 0.71, 0.02, 0.15}));
Json opacity = Json::array();
opacity.push_back(Json::from_doubles({-0.02, 0.5}))
.push_back(Json::from_doubles({-0.002, 0.0}))
.push_back(Json::from_doubles({0.002, 0.0}))
.push_back(Json::from_doubles({0.02, 0.5}));
Json tf = Json::object();
tf.set("range", Json::from_doubles({-0.02, 0.02}))
.set("color", color).set("opacity", opacity).set("colormap", "custom");
Json hint = Json::object();
hint.set("render", "volume").set("transfer_function", tf);
Json defaults = Json::object();
defaults.set(spin, hint);
w.set_viewer_defaults(defaults);
Binary tensors¶
Binary members (grids, coordinates, eigenvalues) are qvf::Tensor objects built
by the tensor_* helpers, which serialize little-endian regardless of host:
Tensor rho = tensor_f32(values, {nx, ny, nz}); // float32 3-D grid
Tensor coords = tensor_f64(flat_xyz, {n_frames, n_atoms, 3});
tensor_f32, tensor_f64, tensor_i32, tensor_i64 cover the common cases;
shape must multiply to the number of supplied values.
The Python writer¶
Same shape, no build step:
import numpy as np
from qvf_writer import QvfWriter
w = QvfWriter(program="my-code", version="1.2.3", calculation="job")
w.add_structure([{"symbol": "O", "position": [0, 0, 0.117], "atomic_number": 8}])
w.add_density(np.abs(rho), origin=[-8, -8, -8],
voxel_vectors=[[0.2, 0, 0], [0, 0.2, 0], [0, 0, 0.2]])
w.add_wavefunction_gto(shells, mo_coefficients=C, energies=eps, occupations=occ,
coeffs_are_libint_normalized=True)
w.write("result.qvf")
The Python API mirrors the C++ one method-for-method; payloads are plain lists /
dicts / NumPy arrays. See python/examples/write_h2o.py. Like the C++ writer,
set_provenance(**fields) silently drops the deprecated hostname field
unless you pass the keyword-only allow_hostname=True. It checks the typed
fields and raises ValueError on a wrong one (software=[{"name": ..., "version": ...}], git_commit, n_threads, n_mpi_ranks, n_gpus,
started_utc / finished_utc as ...Z strings or aware datetimes; None
removes a key), and add_job_spec(..., structure_ref=, solvation=, dispersion=, relativistic=, scf=, geometry=, resources=) does the same for the
job request.
Energies and their derivatives:
w.set_provenance(method="rhf", scf_energy={"value": -74.9659, "units": "hartree"})
w.set_energies(-74.9659, method="rhf", # floats are hartree
components={"nuclear_repulsion": 9.1895, "electronic": -84.1554},
by_method=[{"method": "rhf", "total": -74.9659}])
w.add_forces(gradient, meta={"structure_ref": "structure"}) # dE/dR, not the forces
w.add_hessian(hessian, meta={"atom_masses_amu": [15.999, 1.008, 1.008]})
w.add_vibrations(metadata, displacements, hessian_ref="hessian")
Excited states, their natural transition orbitals and a transition density (spec § 5.12); add the sections a state names before the table:
for role, C_nto in (("hole", C_hole), ("particle", C_particle)):
w.add_wavefunction_gto(shells, mo_coefficients=C_nto, occupations=weights,
orbital_kind="nto", state_ref="excited_states",
state_index=1, nto_role=role, section_id=f"nto_s1_{role}")
w.add_volume("volume.transition_density", rho_01, origin, voxels,
excited_states_ref="excited_states", state_index=1, section_id="tden_s1")
w.add_excited_states([{"index": 1, "excitation_energy": 7.40, # floats are eV
"oscillator_strength": 0.052,
"contributions": [{"from_mo": 4, "to_mo": 5, "weight": 0.99}],
"nto_ref": "nto_s1_hole", "density_ref": "tden_s1"}],
meta={"method": "tddft", "n_states": 1}, wavefunction_ref="wf")
w.add_spectrum("spectra.uvvis", payload={"frequencies": [7.40], "intensities": [0.052],
"peaks": [{"x": 7.40, "assignment": {"state_index": 1}}],
"states_ref": "excited_states"})
An energy that energies states for the method provenance.method names
must equal provenance.scf_energy within 1e-8 hartree; the validator reports
a disagreement as E_ROOT_INCONSISTENT.
Trajectories and MD¶
A trajectory carries the frames of an optimization, an IRC, a scan or a
molecular-dynamics run (spec § 5.5). Beyond coords, every per-frame member
is optional and in frame order; each must have the frame count of coords
(and the vectors its atom count), or the writer raises before writing:
cells = [Lattice.from_rows_angstrom(rows_k) for rows_k in box_rows] # Å, rows
w.add_trajectory(
{"symbols": symbols, "kind": "md", "ensemble": "npt", "timestep_fs": 0.5,
"pbc": [True, True, True]},
coords, # [n_frames, n_atoms, 3], Å
lattice=cells, # [n_frames, 3, 3] rows in Å (one Lattice: [3, 3])
velocities=v, # [n_frames, n_atoms, 3], Å/fs
gradient=dEdR, # [n_frames, n_atoms, 3], hartree/bohr: NOT the forces
times=t_fs, energies=e_eh, # [n_frames], fs and hartree
cell_volumes=vol_a3, temperatures=t_k,
precision="float32") # coords stored as float32, metadata.precision set
The trajectory lattice is in the structure convention (rows, Å), like
add_structure(lattice_vectors=...) and unlike add_reaction_path(lattice=...)
(columns, bohr); a plain array is written as given and must already be rows in
Å. With a lattice, the metadata must carry pbc, and a periodic pbc needs a
lattice. The member holding per-frame gradients is called gradient because
it stores dE/dR; negate it to draw forces. When the metadata also lists
energies, the binary energies must equal it within 1e-10 hartree; for long
runs prefer the binary member and leave the JSON list out. add_reaction_path
takes the shared velocities, gradient, times and energies the same way.
The validators check the same rules (E_MEMBER_MALFORMED).
Electric, elastic and dielectric properties (spec § 5.13):
w.set_dipole_moment(total_debye=1.85, vector_debye=[0, 0, -1.85])
w.add_electric_properties({
"origin": "center_of_mass",
"dipole": {"vector": [0, 0, -1.85], "total": 1.85}, # debye, as the root block
"quadrupole": {"matrix": theta, "traceless": True, # debye·Å
"convention": "buckingham"},
"polarizability": {"matrix": alpha, "frequency_ev": 0.0}}) # atomic units
w.add_elastic_properties(C, compliance=S, average="hill", # C: 6 x 6, GPa, Voigt
moduli={"bulk_modulus_gpa": 76.0, "shear_modulus_gpa": 26.0})
w.add_dielectric_properties(eps_inf, ionic=eps_ion, static=eps_inf + eps_ion,
born_charges=Z) # Z: [n_atoms, 3, 3], in e
The writers check shapes, symmetry and static == electronic + ionic and raise
ValueError before writing. A restated dipole that differs from the root
dipole_moment by more than 1e-6 debye is E_ROOT_INCONSISTENT in the
validator (the order of the two calls does not matter), and Born charges that
do not number the atoms of the section’s structure are E_MEMBER_MALFORMED.
Bloch orbitals of a periodic system at several k-points (wavefunction.bloch,
spec § 5.3.1, provisional) take the shells of add_wavefunction_gto and
complex arrays:
w.add_structure(atoms, pbc=[True, True, True], lattice_vectors=cell)
w.add_wavefunction_bloch(shells, kpoints_frac, # [n_k, 3], fractional
C_k, # complex [n_k, n_spin, n_mo, n_ao]
eps_k, occ_k, # [n_k, n_spin, n_mo], hartree / electrons
weights=w_k, spin="unrestricted",
coeffs_are_libint_normalized=True)
The coefficients follow the Bloch sum φ_μk(r) = N^(−1/2) Σ_T e^{+ik·T}
χ_μ(r − T); structure_ref must name a periodic structure, which the
validator checks.
Archive metadata and section references work the same way. set_metadata( title=..., authors=[{"name": ...}], license="CC-BY-4.0", keywords=[...])
fills the root metadata block and checks each field (time stamps may be
timezone-aware datetime objects, uuid a uuid.UUID); the writer adds
created_utc and a random uuid unless you set them or construct it with
QvfWriter(..., auto_metadata=False). annotate_section(section_id, structure_ref=..., run_ref=..., group=..., label=...) sets the generic
references of an added section (spec § 4.1) and raises ValueError for a
reference to a missing section or one of the wrong kind.
Sketch documents: species, schemes, pictures and job sets¶
A program that prepares calculations, such as a structure and reaction
sketcher, keeps its document as QVF: one structure section per species with
its chemical identity (spec § 5.1), a reaction.scheme that relates them
(§ 5.14), a preview that file managers show (§ 5.15) and, when it asks for
calculations, one job.spec per job (§ 5.9). Every add_* method takes
label=, the section’s display label; for a species it is the compound
number.
from qvf_writer import QvfWriter
w = QvfWriter(program="my-sketcher", version="1.0", calculation="")
w.add_preview(png_bytes, image=svg_text) # PNG or JPEG; SVG, PNG or JPEG
w.add_structure(methane_atoms, charge=0, multiplicity=1, name="methane",
smiles="C", inchi="InChI=1S/CH4/h1H4",
inchikey="VNWKTOKETHGBQD-UHFFFAOYSA-N", label="1",
section_id="structure_s1")
w.add_structure(cl2_atoms, charge=0, multiplicity=1, name="chlorine", label="2",
section_id="structure_s2")
w.add_structure(ch3cl_atoms, label="3", section_id="structure_s3")
w.add_structure(hcl_atoms, label="4", section_id="structure_s4")
w.add_reaction_scheme([
{"id": "r1", "reactants": ["structure_s1", "structure_s2"],
"products": ["structure_s3", {"structure_ref": "structure_s4", "coefficient": 1}],
"arrow": "forward", "conditions": ["hν", "25 °C"],
"yield_percent": 61.5, "yield_text": "61.5%"},
], label="Radical chlorination")
w.set_extensions({"x_mysketcher": {"version": "1"}})
w.add_vendor_section("x_mysketcher.document", json_members={"document": doc},
opaque_members={"drawing": (svg_bytes, "image/svg+xml")})
w.write("scheme.qvf")
add_structure refuses a charge and multiplicity impossible for the atoms;
add_reaction_scheme refuses a participant that is not an added structure
section, a repeated step id or a structure listed twice in one role.
Participants are chemical roles: the target of a retrosynthetic step is
among its products. add_preview sniffs the image types from the bytes
(pass thumbnail_media_type= / image_media_type= to state them; they are
checked) and stores both members uncompressed; an SVG must reference no
external resource and contain no script.
A job set: one job.spec per job, each naming its structure, and one
run.record per run naming its job.
w.set_provenance(run_status="pending")
w.add_job_spec(job_type="molecular", method="rks", functional="pbe0",
basis="def2-svp", structure_ref="structure_s1", section_id="job_s1")
w.add_job_spec(job_type="molecular", method="rks", functional="pbe0",
basis="def2-svp", structure_ref="structure_s3", section_id="job_s3")
# … a runner later adds, for each job it ran:
w.add_run_record(program="my-engine", log_text=log, job_ref="job_s1",
run_status="converged", section_id="run_s1")
add_job_spec writes structure_ref on the section object, where a reader
finds it in the manifest, and in the payload.
Bonds belong to one structure. add_structure(..., bonds=...) writes them as
the structure’s own member; a standalone bonds section names its structure
with add_bonds(pairs, structure_ref="structure_s3"), written on the section
object, and without it belongs to the archive’s first structure (spec § 5.1).
The reference must name an added structure section; validators check the
indices against that structure’s atoms.
Lattices¶
QVF v1 stores a periodic cell in two conventions: structure
lattice_vectors and the trajectory lattice member are row vectors in
Å, while the reaction.path lattice is a matrix whose columns are a,
b, c in bohr (spec § 4.3, “Lattice conventions”). Both are frozen in v1 and
must not be mixed up. The Lattice helper holds the cell once and converts to
whichever the member needs, in both languages:
from qvf_writer import Lattice, QvfWriter
cell = Lattice.from_rows_angstrom([[2.46, 0, 0], [1.23, 2.1304, 0], [0, 0, 15.875]])
cell.columns_bohr() # ≈ [[4.6487, 2.3244, 0], [0, 4.0259, 0], [0, 0, 29.9994]]
cell.volume_angstrom3() # ≈ 83.1974
w.add_structure(atoms, pbc=[True, True, False], lattice_vectors=cell) # rows, Å
w.add_trajectory(meta_with_pbc, coords, lattice=cell) # rows, Å
w.add_reaction_path(meta, coords, lattice=cell) # columns, bohr
# one Lattice per frame for a variable cell: lattice=[cell0, cell1, ...]
# from an engine that holds columns in bohr: Lattice.from_columns_bohr(matrix)
#include "qvf/qvf.hpp"
using namespace qvf;
Lattice cell = Lattice::from_rows_angstrom({{{{2.46, 0, 0}},
{{1.23, 2.1304, 0}},
{{0, 0, 15.875}}}});
w.add_structure(atoms, {{true, true, false}}, cell); // lattice_vectors: rows, Å
w.add_trajectory(meta_with_pbc, coords, cell); // lattice: rows, Å
w.add_reaction_path(meta, coords, &cell); // lattice: columns, bohr
// cell.columns_bohr(), cell.rows_angstrom(), cell.volume_angstrom3(),
// cell.volume_bohr3(); Lattice::from_columns_bohr(m) for the bohr convention.
Plain arrays still work as before: a nested list for lattice_vectors, an
array (Python) or TrajectoryExtras::lattice (C++) for the trajectory
lattice, and a Tensor* (C++) or array (Python) for the reaction-path
lattice are written as given, so they must already be rows in Å, rows in Å
and columns in bohr respectively. The C++ trajectory overloads take one
Lattice (a fixed cell) or a std::vector<Lattice> (one per frame); the
reaction-path overload takes one Lattice for a fixed cell, and a per-frame
[n_frames, 3, 3] path in C++ passes a Tensor built from each cell’s
columns_bohr().
Volumes and periodic grids¶
add_volume writes every volume kind of spec § 5.2. Scalar kinds take a 3-D
array; volume.vector takes [nx, ny, nz, 3] (Cartesian x, y, z on the last
axis), volume.complex takes [nx, ny, nz, 2] ([re, im]; the Python writer
also accepts a complex 3-D array and splits it), and volume.series takes
[n, nx, ny, nz] with an axis that says what indexes the slabs. The grid
shape is always the three spatial extents. Wrong ranks and trailing axes
are refused before anything is written.
w.add_volume("volume.vector", grad_rho, origin, vox) # [nx, ny, nz, 3]
w.add_volume("volume.complex", psi, origin, vox, dtype="float64") # complex array
w.add_volume("volume.laplacian", lap, origin, vox) # e/bohr^5
w.add_volume("volume.series", rho_frames, origin, vox, # [n, nx, ny, nz]
axis={"kind": "frame", "values": [0, 2, 4], # frame indices
"labels": ["R", "TS", "P"], "trajectory_ref": "trajectory"})
A grid that samples a periodic cell says so with periodic (per axis),
includes_endpoint and structure_ref. The rule to get right is the endpoint:
a cube-style grid (includes_endpoint=True, the default) repeats the first
plane as the last, so voxel_vectors[a] * (shape[a] - 1) is lattice vector a;
a CHGCAR-style grid (includes_endpoint=False) does not, so
voxel_vectors[a] * shape[a] is. Write the value that matches your points.
Validators check the span against the structure’s cell (E_MEMBER_MALFORMED
when it differs by more than 1e-6 relative), and a consumer that tiles a
cube-style grid drops the duplicated plane.
cell = Lattice.from_rows_angstrom(rows)
w.add_structure(atoms, pbc=[True, True, True], lattice_vectors=cell)
n = rho.shape # CHGCAR: n planes per period
vox = [[x * BOHR_PER_ANGSTROM / n[a] for x in rows[a]] for a in range(3)]
w.add_density(rho, [0, 0, 0], vox, periodic=[True, True, True],
includes_endpoint=False, structure_ref="structure")
Grid g;
g.voxel_vectors = vox; // bohr per step
g.periodic = std::array<bool, 3>{{true, true, true}};
g.includes_endpoint = false; // CHGCAR style
g.structure_ref = "structure";
w.add_volume("volume.density", g, rho);
Json axis = Json::object();
axis.set("kind", "time").set("values", Json::from_doubles({0.0, 0.5})).set("units", "fs");
w.add_volume_series(Grid{}, rho_t, axis, "rho(t)"); // rho_t: [2, nx, ny, nz]
The frame_volumes + volume_grid members of a reaction.path are the older
form of a volume.series with a frame axis naming the path; they stay
valid, but new code should write a volume.series.
Units¶
Every value is stored in the unit spec § 4.3 gives it, and the writers never
convert. Where a payload may say which unit it uses, it names that unit with a
unit name from the vocabulary of spec § 4.3 (angstrom, bohr,
hartree, ev, cm-1, nm, …; the schema’s $defs/UnitName). Both writers
check every name they write and refuse an unknown one, and a declaration of a
unit the spec fixes (a grid origin in anything but bohr) is refused too.
set_provenance(scf_energy_eh=…, fermi_energy_ev=…) writes hartree and ev;
the older spellings Eh, eV and Hartree remain readable but are
deprecated.
w.add_density(rho, origin, vox, units={"origin": "bohr", "voxel_vectors": "bohr"})
w.add_trajectory(meta, coords, units={"energies": "hartree", "coords": "angstrom"})
w.add_scan_surface(meta, r, theta, e, units={"axis_a": "bohr", "axis_b": "degree"})
w.add_spectrum("spectra.uvvis", frequencies=lam, intensities=f, x_units="nm")
w.add_topology_qtaim(points, units={"position": "bohr"})
from qvf_units import UNITS, canonical, convert
convert(1.0, "hartree", "kcal_per_mol") # 627.5094740631 (CODATA 2018)
convert(300.0, "nm", "ev") # photon energy of a 300 nm line
canonical("Eh") # "hartree"
In C++, Grid::units, Spectrum::x_units / y_units and a units object in
the JSON metadata play the same role; qvf/units.hpp (included by
qvf/qvf.hpp) has enum class Unit, unit_name(), parse_unit(),
canonical_unit() and the same convert(). The validators warn about an
unknown unit name with W_UNIT_UNKNOWN; a validator that runs the JSON Schema
also rejects it.
C and Fortran — the C ABI¶
For codes that can’t call C++ directly (C, Fortran via ISO_C_BINDING, Rust,
Julia, …), the library ships a stable C ABI in
cpp/include/qvf/qvf_c.h. All state lives in one
opaque qvf_writer handle; functions return 0 on success and non-zero on
error (qvf_last_error() gives the message). Nested payloads (provenance,
NMR/EPR spectra, vendor sections) are passed as JSON strings the caller
builds, so no JSON parser crosses the boundary. qvf_set_provenance_json
parses its string inside the library and drops a top-level hostname key
(there is no opt-in through the C ABI); malformed JSON there is an error.
#include "qvf/qvf_c.h"
qvf_writer* w = qvf_create("my-code", "1.0", "h2o/rhf");
const char* sym[3] = {"O", "H", "H"};
int Z[3] = {8, 1, 1};
double pos[9] = {0,0,0.117, 0,0.757,-0.469, 0,-0.757,-0.469};
qvf_add_structure(w, sym, Z, pos, 3);
double freq[3] = {1595, 3657, 3756}, inten[3] = {67, 5, 42};
qvf_add_spectrum_xy(w, "spectra.ir", freq, inten, 3);
qvf_set_provenance_json(w, "{\"method\":\"RHF\",\"basis\":\"STO-3G\"}");
qvf_write(w, "h2o.qvf");
qvf_destroy(w);
qvf_add_job_spec_json(w, spec_json) writes a job.spec from the whole
payload as a JSON string (the typed qvf_add_job_spec has no argument for the
newer fields such as structure_ref, scf or resources); the string is
parsed and its typed fields checked. qvf_set_provenance_json checks the typed
provenance fields too.
qvf_add_volume(w, kind, grid_json, dtype, rank, shape, data, label) writes
any volume kind but volume.series from a JSON grid string (origin,
voxel_vectors, and optionally units, periodic, includes_endpoint,
structure_ref) and a float ("float32") or double ("float64") array of
the given int64_t shape; qvf_add_volume_series(w, grid_json, axis_json, dtype, shape4, data, label) writes a volume.series with its axis.
qvf_add_wavefunction_bloch(w, n_shells, …, n_k, kpoints_frac, weights_or_null, n_spin, n_mo, n_ao, coeffs_re_im, energies, occupations)
takes the shell arrays of qvf_add_wavefunction_gto and row-major arrays
(coefficients [n_k, n_spin, n_mo, n_ao, 2], energies and occupations
[n_k, n_spin, n_mo]). Its basis names the structure section structure,
which must be periodic; qvf_add_structure writes molecular structures only,
so a C caller cannot yet write that structure through the C ABI.
qvf_add_forces(w, n_atoms, gradient, stress_or_null) and
qvf_add_hessian(w, n_atoms, matrix) take row-major double arrays (the
gradient dE/dR in hartree/bohr, the [3N, 3N] Hessian in hartree/bohr²), and
qvf_set_energies_json writes the root energies block from a JSON string.
qvf_add_excited_states_json(w, states_json, meta_json, wavefunction_ref)
writes an excited_states section from a JSON array string (meta_json and
wavefunction_ref may be NULL).
qvf_add_trajectory(w, metadata_json, n_frames, n_atoms, coords, lattice_rows_or_null, lattice_per_frame, velocities_or_null, gradient_or_null, times_or_null, energies_or_null) writes a trajectory section from row-major
double arrays: coords, velocities (Å/fs) and gradient (dE/dR,
hartree/bohr) are n_frames * n_atoms * 3 doubles, times (fs) and
energies (hartree) n_frames, and the lattice is in the structure
convention, rows in Å: 9 doubles when lattice_per_frame is 0, 9 * n_frames
otherwise (the metadata then needs "pbc"). "precision": "float32" in the
metadata stores the coordinates as float32.
qvf_add_electric_properties_json, qvf_add_elastic_properties_json and
qvf_add_dielectric_properties_json add the three property kinds of spec
§ 5.13 from a JSON object string; a payload with a wrong shape, an unknown key,
a missing required field or a violated symmetry or sum rule fails with a
non-zero return and writes nothing.
qvf_set_metadata_json(w, "{\"title\":\"...\",\"license\":\"CC-BY-4.0\"}")
sets the root metadata block (the automatic created_utc and uuid are
added when it lacks them), and qvf_annotate_section(w, id, structure_ref, run_ref, group) sets a section’s references, with NULL leaving a field
unset.
qvf_add_wavefunction_gto writes a restricted wavefunction and
qvf_add_wavefunction_gto_unrestricted an unrestricted one (separate α and β
coefficient matrices, energies and occupations); both create the section
"wf". qvf_set_wavefunction_overlap(w, "wf", n_ao, S) adds the AO overlap
matrix, which validators check against spec Appendix A.1, and
qvf_set_wavefunction_ecp_json(w, "wf", "[{\"center\": 0, \"n_core_electrons\": 28, ...}]") the effective core potentials.
qvf_add_atom_properties writes Mulliken, Löwdin and spin populations in one
call; qvf_add_atom_property(w, "hirshfeld_charge", q, n_atoms) adds any one
scalar atom_properties role, creating the section on the first call, and
qvf_add_atom_vector_property(w, "magnetic_moment", m, n_atoms) adds a vector
role (m is n_atoms * 3 doubles, row-major). A role named
custom.<name> needs qvf_set_atom_properties_meta_json(w, "{\"custom.<name>\": {\"units\": \"e\", \"description\": \"...\", \"kind\": \"scalar\"}}") first.
cpp/examples/write_h2o_c.c is a complete runnable C example (structure,
wavefunction, spectra, EPR, provenance, citations). The C ABI is part of the
same qvf library target — link it exactly as from C++, but with the C++ driver
(set_target_properties(<tgt> PROPERTIES LINKER_LANGUAGE CXX) in CMake, or link
-lstdc++ / -lc++) so the C++ runtime is pulled in. Fortran callers
declare the same entry points with bind(C) interfaces and pass
c_char/c_double arrays plus null-terminated strings.
Validating your output¶
Always validate what you write:
python python/qvf_reader.py result.qvf # exits non-zero on any error
python python/qvf_reader.py --json result.qvf # the findings as JSON
The validator checks the archive against the spec § 6 semantics — container
rules (unique ZIP entry names, safe member paths, size and compression-ratio
caps checked before anything is decompressed, orphan entries), manifest and
section well-formedness, id uniqueness and the reserved ids auto_open,
bookmarks, camera and crossfade, declared member sizes, sha256
integrity, JSON size and nesting
depth, byte sizing,
reference resolution, extension governance, the wavefunction.gto rules of
spec § 5.3 (including the Appendix A.1 diagonal of an overlap member,
E_NORMALIZATION) — and, when jsonschema is installed, the full JSON
Schema. With jsonschema, the payloads of JSON
members such as volume grids, the wavefunction basis and mo_metadata, and
trajectory, reaction, scan and vibration metadata are also checked against
their payload definitions (spec § 5), and a malformed one is reported as
E_PAYLOAD_SCHEMA. Each finding carries a stable code from spec
Appendix B, printed as CODE: message:
QVF validation: result.qvf
sections: 3
E_SHA256_MISMATCH: member 'volumes/density.dat': sha256 mismatch
W_ORPHAN_MEMBER: ZIP entry 'old/structure.json' is not referenced by any manifest member (orphan; consumers ignore it)
FAILED
In your own test suite, call qvf_reader.validate_qvf(path) and assert
report["ok"]. Match problems by code, not by message text:
from qvf_reader import validate_qvf, Limits
report = validate_qvf("result.qvf")
# {"ok", "errors", "warnings", "n_sections", "findings"}; each finding is
# {"severity": "error"|"warning", "code": "E_…"|"W_…", "where": …, "message": …}
assert not any(f["code"] == "E_SHA256_MISMATCH" for f in report["findings"])
# The resource caps are parameters (defaults: the spec § 6.1 reference values).
report = validate_qvf("huge.qvf", limits=Limits(max_member_bytes=1 << 30))
errors and warnings remain lists of strings ("CODE: message") for
existing callers. where is archive, entry:<zip name>, manifest.json,
or a JSON Pointer into the manifest such as /sections/2/members/data.
Warnings — W_ORPHAN_MEMBER, and W_SCHEMA_SKIPPED when jsonschema is
missing — leave ok true. The C++ and JavaScript validators need no schema
engine: they check the schema with a checker generated from it (see
Generated tables and the schema checker).
Member sizes. Both writers record each member’s uncompressed byte count
as size in its member spec (spec § 4.2), and every manifest they write
declares "byte_order": "little" (spec § 2.2). The validators compare size
with the size the ZIP central directory records for the entry before reading
the member; a disagreement is E_BINARY_SIZE, the code that also reports a
binary member whose byte length does not match its dtype and shape. size is
optional, so archives written before it existed validate unchanged. If you
post-process an archive, for example recompress it, keep the bytes of every
member identical; then sha256 and size stay correct.
Orphans and updated archives. W_ORPHAN_MEMBER names a ZIP entry that
the manifest does not reference. In an archive you wrote in one go it usually
points at a stray file. In an archive updated in place it is expected: an
update appends new entries and a new manifest.json last, and the previous
manifest and any replaced members stay behind (spec § 2.3). Such an archive
holds several manifest.json entries; the validators read the last one and
report each earlier one as W_ORPHAN_MEMBER with where entry:manifest.json,
not as the error E_ZIP_DUPLICATE_ENTRY, which still applies to every other
duplicated name. Repacking writes the archive anew without the orphans. How to
append to and repack an archive with the toolkit is described in “Streaming
and updating archives”.
conformance/negative/ holds one must-fail archive per rule; it shows what
each code looks like in practice and is what a third-party validator certifies
against (see conformance/README.md).
Generated tables and the schema checker¶
The JSON Schema spec/qvf_manifest.schema.json is the single source of truth,
and the tables every implementation needs are generated from it rather than
copied by hand. python3 scripts/gen_kinds.py reads the schema,
registry.json (canonical kind order, readable versions) and the finding-code
table of spec Appendix B, and writes, deterministically:
cpp/include/qvf/kinds_generated.hpp—namespace qvf::gen: oneKindInfoper canonical kind with aRoleInfoper member role (required,Format::Json/Binary/Opaque, allowed dtypes, shape rank bounds, payload definition; a pattern role such ascustom.<name>carries its regular expression),kind_info(),kCanonicalKinds,payload_def(kind, role), theUnitenum withunit_name()/parse_unit()(re-exported byqvf/units.hpp), the finding codes and the schema$id;python/qvf_kinds.py— the same tables (KINDS,CANONICAL_KINDS,PAYLOAD_DEFS,UNITS,FIXED_UNITS,FINDING_CODES, …), imported byqvf_readerandqvf_units;cpp/src/qvf_schema_table.hppandjs/src/schemaTable.ts— the schema compiled into a compact instruction table: one node per schema object, one op per keyword, pooled strings, numbers and literals, and everypatterncompiled to a small Pike-VM program.
A small interpreter of that table, cpp/src/qvf_schema_check.hpp and
js/src/schemaCheck.ts, is the C++ and JavaScript validators’ JSON Schema
check: no schema engine ships, yet both report E_SCHEMA for manifest.json
and E_PAYLOAD_SCHEMA for every JSON member with a payload definition, at the
same location (where, and the payload’s own JSON pointer in the message) as
the Python validator, which uses jsonschema. The checker implements the JSON
Schema 2020-12 subset the schema uses (type, const, enum, required,
properties, patternProperties, additionalProperties,
dependentRequired, minProperties / maxProperties, prefixItems,
items, contains / minContains / maxContains, minItems / maxItems,
minimum / maximum / exclusiveMinimum / exclusiveMaximum, minLength
/ maxLength in code points, pattern, allOf / anyOf / oneOf / not,
if / then / else, $ref to $defs, boolean schemas; also
uniqueItems, multipleOf, propertyNames, dependentSchemas), with
format an annotation as in the Python reference. Patterns follow ECMA-262:
$ matches only at the end of a string, where Python’s re also matches
before a final newline. python/tests/test_schema_checker_parity.py holds
both checkers to jsonschema on all three corpora and on thousands of mutated
documents.
After a schema change, run python3 scripts/gen_kinds.py (then
python3 scripts/amalgamate.py); --check fails when a generated file is
stale, and CI runs it. The generator refuses a keyword outside the subset, a
pattern outside the supported regular-expression subset, a *Payload
definition that no (kind, role) names, and a registry.json that disagrees
with the schema’s kinds, so a schema change can never leave the generated
checkers silently behind. Wiring a new payload definition to the role that
carries it is the one manual step: add a row to PAYLOAD_DEFS in
scripts/gen_kinds.py (payload definitions describe archive members, so the
manifest never $refs them).
Reading QVF in C++¶
The toolkit also ships a reference reader (qvf/qvf_reader.hpp) that
completes the round-trip — it opens a .qvf, parses manifest.json, verifies
each member’s sha256, and decodes JSON and binary members. Like the writer it is
zero-dependency: the ZIP reader, INFLATE decompressor, JSON parser, and SHA-256
are all in the toolkit, and it reads archives from any producer (it handles
stored, fixed-, and dynamic-Huffman DEFLATE, so PySCF/zlib/miniz output is fine).
#include "qvf/qvf_reader.hpp"
using namespace qvf;
// Validate first: validate_file() never throws on a malformed archive — an
// unreadable ZIP, duplicate entries or a broken manifest become findings too.
std::vector<Finding> findings = QvfReader::validate_file("result.qvf");
for (const Finding& f : findings)
std::cerr << f.code << ": " << f.message << " [" << f.where << "]\n";
if (has_errors(findings)) return 1; // warnings leave it valid
QvfReader r = QvfReader::open("result.qvf");
// Decode a JSON member (resolved through the manifest by section id + role):
Json st = r.read_json_member("structure", "structure");
std::string sym = st.at("atoms").at(0).at("symbol").as_string();
// Decode a binary member (little-endian, with dtype + shape):
BinaryMember mo = r.read_binary_member("wf", "mo_coefficients");
std::vector<double> coeffs = mo.as_doubles(); // mo.dtype == "float64"
QvfReader verifies sha256 on every read by default (pass verify=false to
skip). The parsed Json supports at(key) / at(index) / as_string() /
as_double() / as_int() / as_bool() / contains() / items() /
elements(). The reader is included in the single-header amalgamation too.
r.typed_manifest() returns the manifest as the typed model of
qvf/model.hpp beside the raw r.manifest(): a Manifest with
qvf_version, source, the root blocks, and sections, each a Section
(id, kind, label, …, critical, and members, role → MemberSpec with
path, format (Json / Binary / Opaque), dtype, shape, sha256,
size). Every optional field is a std::optional, and the model is
lossless: whatever it does not type — kind-specific section fields such as
operand_a, vendor keys, a field of an unexpected type — is kept in the
extra JSON object of its record, so Manifest::from_json(j).to_json()
equals j. The Python counterpart is qvf_model (QvfReader.typed_manifest).
The C++ validator emits the same finding codes as the Python one (spec
Appendix B), the JSON Schema findings E_SCHEMA / E_PAYLOAD_SCHEMA
included: it checks the schema with the checker generated from it. Pass
ValidationOptions{false} (or set schema_check = false) to skip that check;
the validator then reports W_SCHEMA_SKIPPED, as a validator without a schema
engine does.
QvfReader::validate_file(path, limits, options)/validate_bytes(bytes, limits, options)— validate without opening a reader first.r.validate_findings(limits, options)— validate an open reader.r.validate(errors)— the legacy form: returnstruewhen there is no error, otherwise appends"CODE: message"strings for the errors.The ratio policy defaults to disabled (
max_ratio = 0). A positive value opts into a stricter application policy; constant scientific arrays remain valid under the default absolute size limits. ZIP CRC-32 is always checked, including members without SHA-256.Limits— the resource caps (max_member_bytes,max_total_bytes,max_ratio,ratio_exempt_bytes,max_json_bytes,max_json_depth), defaulting to the spec § 6.1 reference values; it isqvf::container::Limits, which the ZIP reader enforces.QvfReader::open(path, limits)applies them too: it refuses duplicate ZIP entry names (exceptmanifest.json, whose last entry it reads, spec § 2.3) and a manifest over the JSON size or depth cap, never inflates an entry past its recorded size, checks each entry’s CRC-32, and applies every cap to each member read.Json::parse(text, max_depth)throwsqvf::JsonDepthErroron deeper input (default depth 64).
examples/read_qvf.cpp is a ready-made CLI: it prints every finding as
CODE: message [where] and exits non-zero on any error.
Streaming and updating archives¶
Both writers can update an existing archive in place, and the C++ writer can
also stream a new one, so a large grid never has to sit in memory twice. An
update follows spec § 2.3: new members are appended, a new manifest.json is
written last (STORED), no existing entry name is reused, and
metadata.modified_utc is set to the time of the update (unless you set one).
The previous manifest stays in the archive as a superseded entry; readers use
the last manifest.json, and validators report each superseded copy, like any
other orphan, as W_ORPHAN_MEMBER. repack writes the archive anew from its
current manifest, without orphans or superseded manifests.
C++¶
#include "qvf/qvf.hpp"
using namespace qvf;
// A streaming writer: the file is created now and every member is written to
// it as soon as it is added. write() puts manifest.json last and finishes it.
QvfWriter w({"my-code", "2.1", "density"}, "density.qvf");
w.add_structure(atoms);
Json meta = Json::object();
meta.set("note", "1 gigavoxel");
Json members = Json::object();
members.set("meta", meta);
w.add_vendor_section("x_mycode.grid", members, false, "", "grid");
// Stream a binary member into an existing section, slab by slab. sha256, CRC
// and size are computed on the fly; close() adds the member spec.
MemberSink s = w.begin_binary("grid", "data", DType::Float64, {nx, ny, nz});
for (const std::vector<double>& slab : slabs) // little-endian bytes
s.write(slab.data(), slab.size() * sizeof(double));
s.close(); // throws unless 8 * nx * ny * nz bytes arrived
w.write();
// An in-place update: the manifest is loaded, add_* append, write() appends
// the new members and a new manifest.json to the same file.
QvfWriter u = QvfWriter::open_for_update("result.qvf");
u.add_spectrum("spectra.ir", ir);
u.remove_section("x_mycode_doc"); // replace a section: remove it, then
u.add_vendor_section("x_mycode.doc", doc, false, "", "x_mycode_doc"); // add it anew
std::vector<uint8_t> preview = u.to_bytes(); // the updated archive; file untouched
u.write();
repack("result.qvf", "result-packed.qvf"); // no orphans, no superseded manifests
A buffered writer (
QvfWriter(source)) acceptsbegin_binary()too; the bytes are then kept untilto_bytes()/write(path), which both produce the same archive (writestreams it to the file).Only one
MemberSinkis open at a time; close it before adding anything else. A sink destroyed unclosed abandons its member, which never appears in the manifest or the central directory.open_for_updatekeeps root keys it does not model, setsauto_metadata = false(created_utcanduuidstay those of the archive), and never rewrites an existing entry: a new member whose default path the archive already holds gets a fresh one,<stem>.<n><suffix>(adding a custom role to an existingatom_propertiessection writesatom_properties/meta.2.json).remove_section(id)drops a section from the manifest and returns it: a buffered writer discards its pending files, a streaming writer drops the entries it wrote from the central directory, and entries the archive held before stay as orphans untilrepack(). An archive stamped with the withdrawnqvf_version: 2is written back as version 1. Untilwrite()returns, the file being updated has no central directory once members stream into it; copy it first if an interruption must not cost the archive (spec § 2.3).The Python writer has three conveniences the C++ one lacks:
label=on everyadd_*method,open_for_updatefrom bytes, andrepack=Trueonwriteandto_bytes.force_zip64 = truewrites every ZIP64 structure even for small members; without it ZIP64 is used exactly when a size, an offset or the entry count passes the 32-bit limits. A 32-bit archive is byte-for-byte what earlier versions wrote for the same entries.
The ZIP layer underneath is public as qvf/container.hpp:
container::ZipWriter over a FileSink, MemorySink or CallbackSink
(non-seekable, so streamed entries carry data descriptors),
ZipWriter::append(path), and container::ZipReader, whose read_into()
streams one entry in chunks under the Limits caps (member and total size,
and an optional positive compression-ratio policy):
#include "qvf/container.hpp"
container::ZipReader zip = container::ZipReader::open("big.qvf");
zip.read_into("volumes/density.dat",
[&](const uint8_t* p, size_t n) { consume(p, n); },
container::Limits{});
QvfReader::open(path) reads from the file on demand as well, so an archive
need not fit in memory to be read or validated.
Python¶
from qvf_writer import QvfWriter, repack
w = QvfWriter.open_for_update("result.qvf")
w.add_spectrum("spectra.ir", frequencies=[1595.0, 3657.0], intensities=[67.0, 3.0])
w.write() # appends via zipfile mode "a"
repack("result.qvf", "result.qvf") # in place: drops orphans and old manifests
data = QvfWriter("my-code", "1.0").to_bytes(force_zip64=True)
To replace a section, remove it and add the new one under the same id.
The update never rewrites an entry (spec § 2.3), so a new member whose default
path the archive already holds gets a fresh one (doc/doc.2.json), and the
old members stay behind as superseded entries until the archive is repacked.
A program that saves the same document again and again passes repack=True.
open_for_update also takes an archive’s bytes:
u = QvfWriter.open_for_update(archive_bytes) # or a path
u.remove_section("vibesketch_document")
u.add_vendor_section("x_vibesketch.document", json_members={"document": doc},
section_id="vibesketch_document")
u.add_structure(atoms, charge=-1, multiplicity=1, label="5", section_id="structure_s5")
new_bytes = u.to_bytes(repack=True) # or u.write("doc.qvf", repack=True)
remove_section drops the members of a section added in the same writer,
and returns the removed section; it does not touch other sections, so a
reference to a removed section dangles until one with the same id is added
again. An archive stamped with the withdrawn qvf_version: 2 opens for update
and is written as version 1.
to_bytes() and write() take force_zip64, which puts a ZIP64 extra field
in every local header; zipfile adds ZIP64 central-directory fields and end
records only when a size, an offset or the entry count needs them. The Python
writer buffers members in memory; it has no streaming entry point.
JavaScript reader¶
js/ holds @qvf/core, a reader and validator for .qvf archives in pure
TypeScript that runs in Node and in browsers (one runtime dependency, fflate;
SHA-256 through WebCrypto). It opens an archive from a Uint8Array, exposes the
typed manifest, decodes JSON members and binary members into typed arrays with
their shape (Float32Array, Float64Array, … BigInt64Array), and validates
with the same finding codes as the Python and C++ validators (spec Appendix B),
the JSON Schema findings included (through the generated checker; pass
schemaCheck: false to skip it); it has no writer, and it is certified by
running the conformance corpus. See js/README.md for a Node example, a
browser example (js/examples/browser.html) and the test commands.
Section-kind cheat sheet¶
To publish… |
Call |
Kind |
|---|---|---|
Geometry |
|
|
A species’ charge, multiplicity, name, SMILES, InChI, InChIKey, compound label |
|
|
Reaction scheme (reactants, reagents, products, arrow, conditions, yield) |
|
|
A picture of the archive for file managers |
|
|
Basis + MOs (GBW) |
|
|
Bloch orbitals at several k-points (periodic) |
|
|
IR / Raman / UV-Vis / ECD / VCD |
|
|
NMR shifts/tensors |
|
|
Per-atom charges, populations, valences, Fukui indices, energies, volumes, shieldings, magnetic moments and gradients ( |
|
|
Mayer / Wiberg bond orders |
|
|
Electron density grid |
|
|
Electrostatic potential |
|
|
Laplacian of the density |
|
|
Vector field (density gradient, current, field) |
|
|
Complex orbital or Bloch function on a grid |
|
|
Densities along a trajectory, in time or by state |
|
|
Periodic (CHGCAR- or cube-style) grid |
|
|
Optimization / IRC frames |
|
|
MD run: per-frame cell (rows, Å), velocities, gradients, times, energies, cell volumes, temperatures; float32 coords |
|
|
Normal modes |
|
|
SCF convergence trace |
|
|
References |
|
|
Input + log of the run |
|
|
Declarative job request (not yet run); several make a job set |
|
|
The run of one job of a set |
|
|
Nuclear gradient (forces are its negative), stress |
|
|
Cartesian Hessian |
|
|
Excited states (energies, strengths, transition dipoles, contributions) |
|
|
Natural transition orbitals (hole + particle) |
|
|
Transition density grid |
|
|
Dipole, quadrupole, octupole, polarizability, hyperpolarizability |
|
|
Elastic constants (Voigt stiffness, moduli) |
|
|
Dielectric tensors, Born charges, piezoelectric tensor |
|
|
Total energy, its components, energies by method |
|
root |
Band structure |
|
|
DOS / PDOS |
|
|
Anything not yet standard |
|
|
For the ORCA-specific mapping (GBW → wavefunction, spectra parsing, EPR), see
orca_integration.md.