Volumetric data: cube and XSF files

vibe-qc writes the two interchange formats every solid-state / molecular viewer reads:

  • Gaussian cube (.cube) — molecular electron densities, molecular orbitals, and ESP. Read by VMD, Avogadro, PyMOL, ChimeraX.

  • XCrySDen XSF / BXSF (.xsf, .bxsf) — periodic crystal structures, periodic volumetric data, and band data on a 3D k-mesh for Fermi-surface plots. Read by VESTA and XCrySDen.

Molecular: cube files

import vibeqc as vq

mol = vq.Molecule.from_xyz("h2o.xyz")
basis = vq.BasisSet(mol, "6-31g*")
res = vq.run_rhf(mol, basis)

# Density: ρ(r) = Σ D_μν χ_μ χ_ν
vq.write_cube_density("rho.cube", res.density, basis, mol)

# A single MO (zero-based index)
homo = mol.n_electrons() // 2 - 1
vq.write_cube_mo("homo.cube", res.mo_coeffs, homo, basis, mol)

# Multiple MOs in one file (multi-volume cube; viewer flips between them)
vq.write_cube_mos("mos.cube", res.mo_coeffs, [homo - 1, homo, homo + 1, homo + 2],
                   basis, mol)

The grid is built automatically: an axis-aligned bounding box around the molecule plus 4 bohr of padding, with 0.2 bohr cubic voxels (~10⁶ voxels for a small molecule). Override with spacing= and padding=, or pass a fully custom :class:vibeqc.CubeGrid:

grid = vq.make_uniform_grid(mol, spacing=0.1, padding=6.0)
vq.write_cube_density("rho-fine.cube", res.density, basis, mol, grid=grid)

For UHF / UKS pass D = D_alpha + D_beta (the total density). A typical sanity check: integrating ρ over the box should return the electron count to ~1% on the default grid, exact in the limit.

Periodic: XSF and BXSF

# Just the structure — useful for visualising the unit cell in VESTA.
vq.write_xsf_structure("crystal.xsf", system)

# Periodic volumetric data — pass a 3D ndarray sampled over one cell.
# (vibe-qc doesn't yet have a built-in periodic-density evaluator; this
# is the entry point for when you produce one yourself or via a
# user-side helper.)
vq.write_xsf_volume("rho.xsf", system, data=rho_grid_3d, name="density")

# Fermi-surface data — band energies on a regular MP-style mesh.
# Shape (nkx, nky, nkz, nbands), energies in Hartree, e_fermi in Hartree.
vq.write_bxsf("bands.bxsf", system, energies, e_fermi=e_f)

XSF files use ångström for all positions and eV in the case of BXSF — vibe-qc does the conversion internally so you keep working in atomic units everywhere else.

Validation

tests/test_cube.py checks:

  • the writer header parses back as expected (atom count, voxel vectors, grid shape, multi-value flag),

  • ρ(r) integrates to the electron count on a fine grid,

  • single-MO files have ⟨φ|φ⟩ ≈ 1.

tests/test_xsf.py checks:

  • ångström conversion is correct for lattice and atom positions,

  • XSF DATAGRID traversal order (first index runs fastest, then j, then k) — required by VESTA/XCrySDen,

  • BXSF conventions: 1/ångström reciprocal lattice and eV energies.