vibe-view: interactive viewer¶
vibe-view is a GPU-accelerated browser-based viewer for vibe-qc’s
QVF (.qvf) output archives. It reads the
zip + JSON manifest format produced by run_job(..., output_qvf=True)
and renders every section the calculation produced: structure,
electron density, molecular orbitals, band structures, spectra,
geometry-optimisation trajectories, vibrational modes, NEB / IRC
reaction paths, and more.
What you need on disk¶
Two things:
A
.qvffile produced by vibe-qc. Passoutput_qvf=Truetorun_joborrun_periodic_joband a{stem}.qvflands next to{stem}.out.vibe-view installed (it is a peer subproject under
vibe-view/inside the vibe-qc checkout, with its ownpyproject.toml).
Install¶
vibe-view is a co-located peer package, not a built-in part of
vibeqc. Two install routes:
# From a vibe-qc checkout. Installs both vibe-qc and vibe-view.
pip install -e '.[viewer-gpu]'
# Or, vibe-view alone (still from the same checkout because the
# package is not yet on PyPI).
pip install -e vibe-view/
The dependency footprint is pure-pip (PyVista + Trame + VTK + Plotly + Click + Pydantic + jsonschema). No conda, no JavaScript build step, no npm. The heaviest single dep is VTK (~200 MB wheel) so the first install takes a minute or two.
Verify the install:
vibe-view --version
# vibe-view 0.1.0
Produce a .qvf file from vibe-qc¶
Pass output_qvf=True to either runner. The flag stacks freely with
write_density, write_molden_file, and the other artefact
toggles.
Molecular: H2O / PBE / 6-31G*¶
from vibeqc import Atom, Molecule, run_job
mol = Molecule([
Atom(8, [0.0, 0.00, 0.00]),
Atom(1, [0.0, 1.43, -0.98]),
Atom(1, [0.0, -1.43, -0.98]),
])
run_job(
mol,
basis="6-31g*",
method="rks",
functional="PBE",
optimize=True, # writes a geometry trajectory
output="water",
output_qvf=True, # produces water.qvf
write_density=True, # embeds the SCF density isosurface
write_molden_file=True, # embeds wavefunction.gto + MOs
)
Produces water.qvf alongside the usual water.out / water.molden
/ water.traj / water.bibtex siblings.
Periodic: MgO rocksalt / RHF / sto-3g¶
import numpy as np
import vibeqc as vq
a = 4.21 / 0.529177 # Angstrom to bohr
mgo = vq.PeriodicSystem(
3,
np.eye(3) * a,
[vq.Atom(12, [0.0, 0.0, 0.0]),
vq.Atom(8, [a/2, a/2, a/2])],
)
basis = vq.BasisSet(mgo.unit_cell_molecule(), "sto-3g")
kpath = vq.kpath_from_segments(
mgo,
segments=[
([0.0, 0.0, 0.0], "G", [0.5, 0.0, 0.5], "X"),
([0.5, 0.0, 0.5], "X", [0.5, 0.5, 0.5], "L"),
([0.5, 0.5, 0.5], "L", [0.0, 0.0, 0.0], "G"),
],
points_per_segment=20,
)
vq.run_periodic_job(
mgo,
basis=basis,
method="RHF",
output="mgo",
output_qvf=True,
write_density=True,
band_structure=vq.band_structure_hcore(mgo, basis, kpath),
)
The optional band_structure= kwarg embeds a band-structure section
in the archive so vibe-view can render the interactive Plotly band plot
without having to re-diagonalise.
Launch¶
Command line¶
vibe-view open water.qvf
Boots the Trame server on http://127.0.0.1:8080 and opens the
default browser at that URL. Flags:
vibe-view open water.qvf --port 9876 # bind to a different port
vibe-view open water.qvf --no-browser # skip the auto-open
vibe-view open water.qvf --host 0.0.0.0 # bind to all interfaces (remote use)
Programmatic¶
from vibeview import launch_qvf
launch_qvf("water.qvf") # path on disk
launch_qvf(open("water.qvf", "rb")) # any seekable file-like
launch_qvf(io.BytesIO(qvf_bytes), open_browser=False) # in-memory
launch_qvf blocks until the Trame server stops (Ctrl+C). Pass an
already-constructed QVFReader instance if you want to inspect the
archive before launching the UI.
The UI¶
When the browser opens you get three regions:
Top bar: the source banner (program, version, calculation name) and the section-status pill.
Sidebar (left): the section tree. Click a section to make it the active section (loads its binary data on first click). Active sections drive the main viewport.
Viewport (centre): 3D scene for structure / volume / vibrations / trajectory sections; interactive Plotly chart for bands / spectra; table for
atom_propertiesandscf_history.
Structure section¶
Atoms are drawn as CPK spheres with element-coloured surfaces and
the standard van-der-Waals radii. Bonds come from the explicit
bonds section if the producer wrote one; otherwise vibe-view
infers them from covalent radii. Crystals show the unit-cell
wireframe.
Sidebar controls:
Replication (periodic only): set Nx, Ny, Nz to replicate the cell along the lattice vectors. Atoms, isosurfaces, and the cell wireframe all expand in lock-step.
Atom radii / colours: choose between CPK (default), van der Waals, and unit-radius schemes.
volume.density, volume.orbital, volume.spin, volume.elf, volume.difference, volume.generic¶
All volume kinds drive the same renderer: VTK marching cubes over
the grid .dat payload, producing an isosurface at the
configurable isovalue. Sidebar controls:
Isovalue: slider over the volume’s data range (default from
viewer_defaults, falling back to a kind-specific heuristic: 0.05 e/bohr^3 for densities, 0.04 bohr^(-3/2) for MOs).Colour map (signed-volume kinds):
volume.orbital,volume.spin,volume.differenceget a divergent map so the +/- lobes show in different colours.Opacity: 0 to 1 alpha.
Volume .dat blobs are lazy-loaded: nothing is read from the
zip until you activate the section. Large MO grids stay on disk
until you click them.
bands¶
Interactive Plotly plot of every band along the k-path. The Fermi
level (from the bands.fermi field) is drawn as a horizontal
reference line. Hover any band to see its energy in eV at that
k-point. Energies are stored in eV in QVF v1 (see
QVF spatial and energy units).
spectra.ir, spectra.uvvis, spectra.raman, spectra.ecd, spectra.vcd, spectra.nmr, spectra.generic¶
Each spectrum renders as a Plotly stem chart of intensities vs frequency, with hover tooltips showing the per-peak metadata. The x-axis unit follows the spectrum kind (cm^-1 for IR / Raman, eV for UV-Vis, ppm for NMR).
trajectory and reaction.path¶
Frame-by-frame animation with a play / pause / step button strip
underneath the viewport. Bonds are inferred per-frame from
covalent radii so they update with the geometry. A small energy
chart above the timeline plots metadata.energies vs frame index
for geometry-optimisation runs.
reaction.path has the same binary layout as trajectory but
additionally renders waypoint markers (reactant / TS / intermediate
/ product) on the timeline.
Periodic reaction paths (QVF v2). When the frames are
PeriodicSystem instances (slabs, surfaces, periodic NEB
trajectories), the archive ships as qvf_version: 2 and carries
the per-frame lattice + dimensionality so the renderer can draw
the unit cell and (eventually) wrap atoms across periodic
boundaries. The schema additions are minimal: an optional
lattice binary member on the reaction.path section
(columns = a, b, c, in bohr, matching
vibeqc.PeriodicSystem.lattice) plus a dim integer in the
metadata JSON. A shared lattice across all frames stores once as
shape [3, 3]; per-frame lattices store as [n_frames, 3, 3]
(forward-compat with variable-cell scans). Molecular reaction paths
keep emitting v1 archives — there is no migration to do. See
python/vibeqc/output/formats/qvf_manifest_v2.schema.json for
the canonical v2 contract; the writer detects periodic frames
automatically and bumps the archive version.
Rendering (Increment B). When a v2 periodic reaction.path
is activated, vibe-view draws the unit-cell wireframe in the 3D
scene (the same parallelepiped style the structure renderer
uses) and wraps every frame’s atom positions into the central
cell along the first dim lattice vectors. For a slab
(dim=2) that means a + b are wrapped while the vacuum
direction c stays open — an adsorbate that climbs above the
surface keeps climbing in the scene instead of jumping back to
the bottom of the cell. The wrap is naive modulo-1 on
fractional coords; frame-to-frame continuity is not anchored
across cell crossings (atoms can flip from one boundary to the
opposite mid-animation in pathological cases — chemistry NEBs
typically stay within the central cell, so this is rare in
practice). Variable-cell paths ([n_frames, 3, 3] lattices)
re-emit the box per frame, so the cell animates with the
geometry.
vibrations¶
Mode-selector dropdown to pick the normal mode (sorted by frequency); the structure animates with the displacement vector applied sinusoidally. Frequencies are in cm^-1.
atom_properties¶
Tabular display of mulliken_charge / loewdin_charge /
spin_population (whichever the producer wrote). Color-coded
per-atom highlighting in the 3D viewport.
wavefunction.gto¶
Browse the molecular orbitals as a list (with energies and occupations) and click any MO to render its isosurface. vibe-view resamples the orbital from the embedded GTO basis + MO coefficients on a grid of its own choosing, so you can inspect any MO without the producer having to pre-evaluate it.
scf_history¶
Two-pane chart: energy vs iteration (top) and |DIIS error| vs
iteration (bottom). Hover any iteration to see the exact numbers.
structure.symmetry¶
Compact info panel with the spglib output: space group name, Hall number, international symbol, Wyckoff positions, equivalent atoms.
citations¶
The embedded BibTeX bundle is rendered as a copy-paste-friendly block. Pairs with the citations user guide: the viewer shows you what to cite, the producer wrote the database entries.
viewer_defaults: producer-side hints¶
The producer can suggest defaults to the viewer in the manifest
under viewer_defaults. vibe-view picks these up on load and
applies them before rendering anything.
write_qvf(
"water.qvf",
...,
viewer_defaults={
"auto_open": ["density"], # activate the density section by default
"density": { # per-section render hints
"isovalue": 0.04,
"colormap": "viridis",
"opacity": 0.55,
},
"bookmarks": [ # camera bookmarks
{"name": "front", "camera": {...}},
{"name": "above", "camera": {...}},
],
},
)
Hints are non-binding: the user can override any of them through the UI, and unknown viewer_defaults fields are ignored rather than rejected.
Supported kinds¶
The viewer’s renderer registry lives at
vibe-view/src/vibeview/kinds.py::SUPPORTED_KINDS. The full
writer / viewer support matrix is in the
QVF design doc § 1.4.
Anything not in the registry is classified as “skipped,
unsupported” by the viewer and listed in the banner. The viewer
never aborts on an unknown kind; it just leaves that sidebar entry
unclickable.
If you write a custom kind under the x_<vendor>.* namespace, the
viewer reports it as “skipped, vendor namespace” with the vendor
name extracted. To get a kind rendered, add it to
SUPPORTED_KINDS and wire a matching renderer under
vibe-view/src/vibeview/renderers/.
Common pitfalls¶
“vibe-view: command not found”¶
You installed vibeqc but not vibe-view. Install with one of the
two routes above (pip install -e '.[viewer-gpu]' or
pip install -e vibe-view/). The viewer is a separate package
because its dependency footprint (VTK, PyVista, Trame) is large
and not every vibe-qc user wants it.
“ManifestValidationError”¶
The archive’s manifest.json does not satisfy the canonical JSON
Schema at python/vibeqc/output/formats/qvf_manifest.schema.json.
This usually means the producer is older than the viewer or vice
versa. Both sides load the same schema (vibe-view bundles it as a
symlink), so a sha256-identity test pins them; if the archive was
produced by a third party, ask them to validate with
validate_qvf before sharing.
“SHA256MismatchError”¶
A binary payload in the archive has a different sha256 than the manifest claims. Usually means the archive was edited after writing (zip-shuffled, copied truncated, …). The viewer reports this per-section: other sections still load.
“the browser opened but the page is blank”¶
Trame uses Vue 3 + WebSocket. A very strict ad-blocker or
corporate proxy can break the WebSocket handshake. Either
allow the page in the blocker, or launch with --host 0.0.0.0
and open the URL by hand from a different browser profile.
Firefox on macOS 15+ “Unable to connect to 127.0.0.1”¶
macOS 15 (Sequoia) added a system-level Local Network
permission gate that is denied to Firefox by default. The
symptom is unmistakable: Safari and Chrome connect to
http://127.0.0.1:8080 fine, but Firefox shows “Unable to
connect” with a hint about Local Network permissions in
macOS Privacy & Security. Two fixes:
Grant Firefox the permission. System Settings → Privacy & Security → Local Network → toggle Firefox on. Refresh the page. (vibe-view does not need elevated permissions itself; only the browser does.)
Or use a different browser. Safari and Chrome are unaffected because they were grandfathered into the permission system. Open
http://127.0.0.1:8080there while the vibe-view server keeps running in the terminal.
vibe-view itself binds to 127.0.0.1 only by default; nothing
on the server side needs reconfiguring. If you launched with
--host 0.0.0.0 for remote access the same Local Network
gate applies to any LAN browser hitting the box, not just
Firefox.
“vibe-view server starting on …” printed but the browser cannot connect for a few seconds¶
vibe-view announces “ready” only once uvicorn has actually
bound the TCP port (the watcher polls the listener in a
background thread, so the “ready” line and the automatic
browser-open happen post-bind). If you set --no-browser
and open the URL by hand immediately after launching, you
may still race the bind on slow machines; refresh once and
you should connect. If “ready” never appears, see the next
entry.
“vibe-view: warning, server did not bind within 10s”¶
uvicorn entered the asyncio main loop but never made it to binding the TCP socket. Two common causes:
Port already in use. Another process is bound to the same port (a stale vibe-view, an aborted SSH tunnel, Docker Desktop, …). Pick a different port with
--port 9876and retry. To find the offender:lsof -nP -iTCP:8080 -sTCP:LISTEN
uvicorn import-time failure. A missing dependency in the venv (the historical case was the now-fixed missing
uvicorndeclaration in vibe-view’s pyproject; reinstall if you see aModuleNotFoundErrorin the stderr just before the warning).
Programmatic API¶
vibe-view’s API is intentionally minimal. Most users want
launch_qvf:
from vibeview import launch_qvf, QVFReader, QVFOpenError
# One-line launch
launch_qvf("water.qvf")
# In-memory archive (no temp file). Pair with vibeqc.output.qvf_bytes
# for an end-to-end producer → viewer hand-off that never touches disk.
from vibeqc.output import qvf_bytes
data = qvf_bytes(plan, molecule=mol, result=scf_result, mo_data=mos)
launch_qvf(data)
# Inspect first, then launch
try:
reader = QVFReader("water.qvf")
except QVFOpenError as e:
print(f"bad archive: {e}")
else:
print(reader.sections) # list of Section objects
print(reader.source.version) # producer version
launch_qvf(reader) # reuses the open reader
QVFReader accepts a path, raw zip bytes, or any seekable binary
file-like (BytesIO, opened file). It validates the manifest at
construction time and lazy-loads section payloads on demand.
The full reading-side API for callers who want to consume .qvf
archives outside vibe-view is in the
QVF consumer reference.
See also¶
QVF format design: the producer / consumer contract, manifest schema, per-section payload conventions.
QVF consumer reference: the reading-side Python API for callers that want to ingest
.qvfarchives without launching vibe-view.Tutorial 45: vibe-view walkthrough: short end-to-end example that runs a calc, opens the result in vibe-view, and walks through what each panel does.
Tutorial 27: moltui terminal viewer: the terminal-only sibling viewer for SSH / no-X11 environments.
Output files reference: the full family of artefacts vibe-qc emits alongside the
.qvf.