Design — QVF container format for visualization data¶
Status: draft — this document describes the QVF format as understood by the
vibe-viewconsumer. The producer-side contract lives indocs/design_output_module.mdPhase O6 (volumetric writers) and will be extended as writers land.
A QVF file (.qvf, “quantum visualization format”) is a zip
archive containing a JSON manifest and typed binary payloads. It is
the interchange format between vibe-qc’s computational core and both
the terminal viewer (moltui, already handled) and the GPU-accelerated
interactive viewer (vibe-view, this design).
1. QVF archive layout¶
example.qvf
├── manifest.json # required, must be first logical entry
├── sections/
│ ├── structure.json # atoms, lattice vectors, pbc flags
│ ├── bonds.json # optional explicit bond table
│ ├── density.dat # binary volumetric blob (float32)
│ ├── orbital_5.dat # another volumetric blob
│ ├── bands.json # band path metadata (segments, Fermi)
│ ├── eigenvalues.dat # binary eigenvalues array
│ ├── ir_spectrum.json # frequencies + intensities
│ ├── trajectory.json # frames metadata
│ ├── traj_coords.dat # binary per-frame coordinates
│ ├── vib_modes.json # normal mode metadata
│ ├── vib_displacements.dat # binary normal mode displacements
│ └── viewer_defaults.json # optional viewer hints
1.1 manifest.json — the central index¶
{
"qvf_version": 1,
"source": {
"program": "vibe-qc",
"version": "0.9.0",
"calculation": "h2o_pbe0_def2tzvp"
},
"sections": [
{
"id": "structure",
"kind": "structure",
"members": {
"structure": {
"path": "sections/structure.json",
"format": "json",
"sha256": "abcdef..."
}
}
},
{
"id": "density",
"kind": "volume.density",
"members": {
"grid": {
"path": "sections/density.grid.json",
"format": "json",
"sha256": "123456..."
},
"data": {
"path": "sections/density.dat",
"format": "binary",
"dtype": "float32",
"shape": [72, 80, 80],
"sha256": "789abc..."
}
}
},
{
"id": "homo",
"kind": "volume.orbital",
"component": "real",
"members": {
"grid": {
"path": "sections/homo.grid.json",
"format": "json",
"sha256": "..."
},
"data": {
"path": "sections/homo.dat",
"format": "binary",
"dtype": "float32",
"shape": [72, 80, 80],
"sha256": "..."
}
}
},
{
"id": "bands",
"kind": "bands",
"members": {
"kpath": {
"path": "sections/kpath.json",
"format": "json",
"sha256": "..."
},
"eigenvalues": {
"path": "sections/eigenvalues.dat",
"format": "binary",
"dtype": "float64",
"shape": [100, 26],
"sha256": "..."
}
}
},
{
"id": "ir",
"kind": "spectra.ir",
"members": {
"spectrum": {
"path": "sections/ir_spectrum.json",
"format": "json",
"sha256": "..."
}
}
},
{
"id": "opt_trajectory",
"kind": "trajectory",
"members": {
"metadata": {
"path": "sections/trajectory.json",
"format": "json",
"sha256": "..."
},
"coords": {
"path": "sections/traj_coords.dat",
"format": "binary",
"dtype": "float32",
"shape": [15, 3, 3],
"sha256": "..."
}
}
},
{
"id": "vib",
"kind": "vibrations",
"members": {
"metadata": {
"path": "sections/vib_modes.json",
"format": "json",
"sha256": "..."
},
"displacements": {
"path": "sections/vib_displacements.dat",
"format": "binary",
"dtype": "float64",
"shape": [9, 3, 3],
"sha256": "..."
}
}
}
],
"viewer_defaults": {
"auto_open": ["density"],
"density": {
"isovalue": 0.05,
"colormap": "viridis",
"opacity": 0.6
}
}
}
1.2 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},
{"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
floats in angstroms and pbc has one or more true entries.
1.3 volume.*.grid.json¶
{
"origin": [0.0, 0.0, 0.0],
"voxel_vectors": [
[0.2, 0.0, 0.0],
[0.0, 0.2, 0.0],
[0.0, 0.0, 0.2]
],
"shape": [72, 80, 80]
}
The grid is a 3D Cartesian grid defined by an origin plus three voxel basis vectors (columns of the cell matrix). For orthogonal grids the off-diagonals are zero. For non-orthogonal grids (e.g. from a periodic cell), the off-diagonals position the volume correctly in 3D space.
1.4 Content kinds¶
Kind |
Description |
v1 supported? |
|---|---|---|
|
3D molecular/crystal view |
✅ |
|
Explicit bond table (consulted if present, inferred if absent) |
✅ (via structure renderer) |
|
Scalar field for isosurface (electron density) |
✅ |
|
Scalar field for isosurface (molecular orbital) |
✅ |
|
Band structure path + eigenvalues |
✅ |
|
IR spectrum (frequencies + intensities) |
✅ |
|
Frame-by-frame animation (opt/IRC) |
✅ |
|
Normal-mode displacement animation |
✅ |
|
Mulliken/Löwdin charges table |
✅ |
|
Vendor-namespace extensions |
❌ (skipped, vendor namespace) |
2. Design note — vibe-view interactive viewer¶
2.1 Tech stack choice: PyVista + Trame¶
Choice: PyVista with the Trame web backend.
Why not the alternatives:
Alternative |
Rejected because |
|---|---|
Trame + VTK directly |
Steeper learning curve; PyVista wraps VTK with a much cleaner Python API that still exposes all the GPU features. We get the same VTK power without the boilerplate. |
Plotly Dash + VTK.js |
Requires a JavaScript build step (npm, webpack) — breaks the “Python-only” repo pattern. VTK.js is a JS reimplementation with a smaller feature set than native VTK. |
FastAPI + React/Three.js |
Two-language project, frontend build toolchain, npm lockfile churn, bundler config. Massive complexity for a viewer that should “just work” after |
Why PyVista + Trame:
Pure Python — no npm, no JavaScript build step, no two-language project. Matches the existing repo pattern (vibe-queue is also Python-only: FastAPI + Jinja2).
GPU-accelerated marching cubes — VTK’s
vtkMarchingCubesruns on the GPU; interactive frame rates on 200³ grids are well within VTK’s capabilities.Built-in interactivity — rotate, zoom, pan all handled by VTK’s interactor. Trame adds web-serving with zero additional code.
Isosurface + colormap + opacity — all first-class PyVista features (
add_meshwithscalars,cmap,opacity).Maturity — PyVista and VTK are the standard Python 3D visualization stack in scientific computing. Trame is Kitware’s recommended web backend.
Existing ecosystem alignment — vibe-qc already ships
moltui[viewer]for terminal visualization; PyVista+Trame is its natural GPU-accelerated GUI/web complement.
Licensing: PyVista (MIT), Trame (Apache 2.0), VTK (BSD 3-clause). All MPL 2.0 compatible. No bundling needed — pure pip dependencies.
2.2 Module layout¶
Following the vibe-queue pattern (separate top-level directory
with own pyproject.toml, co-located in the same repo):
vibe-view/
├── pyproject.toml # package "vibeview", CLI "vibe-view"
├── src/
│ └── vibeview/
│ ├── __init__.py # version, package metadata
│ ├── __main__.py # python -m vibeview
│ ├── cli.py # click CLI: vibe-view open <file.qvf>
│ ├── qvf.py # QVF reader: zip open, manifest parse,
│ │ # schema validate, sha256 verify,
│ │ # lazy binary extraction
│ ├── schema.json # JSON Schema for manifest.json
│ ├── kinds.py # SUPPORTED_KINDS registry + filter/sort
│ ├── viewer_defaults.py # parse + apply viewer_defaults hints
│ ├── banner.py # summary banner (section listing)
│ ├── renderers/
│ │ ├── __init__.py # kind → renderer dispatch table
│ │ ├── structure.py # 3D structure: atoms, bonds, unit cell
│ │ ├── volume.py # isosurface: marching cubes, slider, cmap
│ │ ├── bands.py # 2D band structure plot
│ │ ├── spectra.py # 1D IR spectrum plot
│ │ ├── trajectory.py # frame animation + energy plot
│ │ └── vibrations.py # normal-mode displacement animation
│ └── app.py # Trame application: sidebar, layout,
│ # lazy section activation, UI controls
└── tests/
├── __init__.py
├── test_qvf.py
├── test_kinds.py
└── test_renderers.py
Relationship to the main vibe-qc package: vibe-view is a peer
sub-project, not a sub-package of vibeqc. The main pyproject.toml
gains an optional viewer-gpu extra group that depends on
vibeview (editable install from the vibe-view/ directory).
2.3 Kind-to-renderer mapping¶
# vibeview/kinds.py
SUPPORTED_KINDS: set[str] = {
"structure",
"volume.density",
"volume.orbital",
"bands",
"spectra.ir",
"trajectory",
"vibrations",
}
# vibeview/renderers/__init__.py
_KIND_RENDERER: dict[str, type] = {
"structure": StructureRenderer,
"volume.density": VolumeRenderer,
"volume.orbital": VolumeRenderer, # same class, different defaults
"bands": BandsRenderer,
"spectra.ir": SpectraRenderer,
"trajectory": TrajectoryRenderer,
"vibrations": VibrationsRenderer,
}
def dispatch(section: Section, viewer: "QVFViewer") -> BaseRenderer:
kind = section.kind
cls = _KIND_RENDERER.get(kind)
if cls is None:
raise UnsupportedKindError(kind)
return cls(section, viewer)
2.4 File-open lifecycle¶
User: vibe-view open h2o.qvf
│
├─ 1. QVFReader.__init__(path)
│ ├─ Open zipfile.ZipFile(path)
│ ├─ Read manifest.json from zip → parse JSON
│ ├─ Validate against JSON Schema (jsonschema)
│ │ └─ ValidationError → hard error, exit
│ └─ Parse sections list into Section dataclasses
│
├─ 2. banner.print_summary(sections)
│ ├─ For each section:
│ │ ├─ kind in SUPPORTED_KINDS → "rendered"
│ │ ├─ kind matches x_<vendor>.* → "skipped, vendor namespace (<vendor>)"
│ │ └─ else → "skipped, unsupported"
│ └─ Print table to stdout
│
├─ 3. Process viewer_defaults (if present)
│ ├─ Parse auto_open list: which section ids to open by default
│ ├─ Parse per-section hints: isovalue, colormap, opacity, replication
│ └─ Store as mutable state (UI can override any hint)
│
├─ 4. Load structure section eagerly
│ ├─ Verify sha256 of structure.json member against manifest
│ ├─ Parse structure.json → atoms, pbc, lattice_vectors
│ └─ Render 3D structure in main viewport
│
├─ 5. Build sidebar tree
│ ├─ Every section listed (rendered + skipped)
│ ├─ Skipped sections greyed out with reason
│ └─ Click on a section → lazy activation
│
├─ 6. Auto-open viewer_defaults sections
│ └─ For each section id in auto_open:
│ ├─ If section is a volume.*: read grid.json (eager),
│ │ defer .dat blob (lazy — read when user activates)
│ ├─ Verify sha256 of grid.json → if mismatch, skip this section
│ └─ Add isosurface to viewport with default hints
│
└─ 7. Start Trame server, open browser
2.5 Partial-support contract — the four rules¶
Implemented in vibeview/kinds.py and vibeview/qvf.py:
Rule 1 — supported kinds are declared explicitly:
# vibeview/kinds.py
SUPPORTED_KINDS: frozenset[str] = frozenset({
"structure",
"volume.density",
"volume.orbital",
"bands",
"spectra.ir",
"trajectory",
"vibrations",
})
Rule 2 — unknown kinds are listed as “skipped, unsupported”:
def classify_section(kind: str) -> tuple[str, str | None]:
"""Return (status, detail) for a section kind."""
if kind in SUPPORTED_KINDS:
return ("rendered", None)
if kind.startswith("x_"):
# vendor namespace: "x_<vendor>.<rest>"
parts = kind.split(".", 1)[0].split("_", 1)
vendor = parts[1] if len(parts) > 1 else "unknown"
return ("skipped", f"vendor namespace ({vendor})")
return ("skipped", "unsupported")
Rule 3 — vendor-namespace sections get the vendor name extracted:
The x_<vendor> prefix is parsed and the vendor name is displayed in
the summary banner. The section is not rendered, nor does it cause an
error.
Rule 4 — sha256 verification before use:
def _verify_member(zf: zipfile.ZipFile, member: MemberSpec) -> bytes:
"""Read a binary member and verify its sha256. Hard error on mismatch."""
data = zf.read(member.path)
actual = hashlib.sha256(data).hexdigest()
if actual != member.sha256:
raise SHA256MismatchError(
f"sha256 mismatch for {member.path}: "
f"expected {member.sha256}, got {actual}"
)
return data
Every call site that reads a binary member calls _verify_member
first. JSON members are also verified. The mismatch is a hard error
for that section only — the viewer continues to render other
sections and lists the failed section in the banner as “error, sha256
mismatch”.
2.6 Dependencies¶
Core (pip install -e '.[gpu]' or pip install vibeview):
pyvista>=0.44— 3D rendering, marching cubes, VTK bindingstrame>=3.6— web application servertrame-vtk>=2.8— VTK widgets in Trametrame-vuetify>=2.7— Material Design UI components (sliders, dropdowns, tree views)click>=8.0— CLI entry pointpydantic>=2.0— data models for manifest sectionsjsonschema>=4.20— manifest.json validationnumpy>=1.26— array handlingmatplotlib>=3.8— 2D plots (bands, spectra, energy curve)
Optional:
ase>=3.22— covalent radii for bond inference (or we ship a small table)
3. QVF file generation (producer side)¶
The producer is implemented at python/vibeqc/output/formats/qvf.py.
See docs/handover_qvf_writer.md for status, coverage, and next steps.
Quick start:
result = run_job(mol, basis="sto-3g", method="rhf",
output="h2o", output_qvf=True)
# → h2o.qvf
The manifest shape emitted by the producer matches this consumer
contract exactly (qvf_version, source, sections[].members).
10 v1 section kinds are implemented.
4. JSON Schema for manifest.json¶
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://vibe-qc.com/schemas/qvf-manifest-v1.json",
"title": "QVF Manifest",
"type": "object",
"required": ["qvf_version", "source", "sections"],
"properties": {
"qvf_version": {"type": "integer", "const": 1},
"source": {
"type": "object",
"required": ["program", "version", "calculation"],
"properties": {
"program": {"type": "string"},
"version": {"type": "string"},
"calculation": {"type": "string"}
}
},
"sections": {
"type": "array",
"items": {
"type": "object",
"required": ["id", "kind", "members"],
"properties": {
"id": {"type": "string"},
"kind": {"type": "string"},
"component": {"type": "string"},
"members": {
"type": "object",
"additionalProperties": {
"type": "object",
"required": ["path", "format", "sha256"],
"properties": {
"path": {"type": "string"},
"format": {"type": "string", "enum": ["json", "binary"]},
"dtype": {"type": "string"},
"shape": {
"type": "array",
"items": {"type": "integer"}
},
"sha256": {"type": "string", "pattern": "^[a-f0-9]{64}$"}
}
}
}
}
}
},
"viewer_defaults": {
"type": "object",
"properties": {
"auto_open": {
"type": "array",
"items": {"type": "string"}
}
},
"additionalProperties": {
"type": "object",
"properties": {
"isovalue": {"type": "number"},
"colormap": {"type": "string"},
"opacity": {"type": "number", "minimum": 0, "maximum": 1},
"replication": {
"type": "array",
"items": {"type": "integer", "minimum": 1},
"minItems": 3,
"maxItems": 3
}
}
}
}
}
}