Solid-state walkthrough: LiH rocksalt with pob-TZVP¶
A complete v0.5 solid-state calculation: build a real ionic crystal, run multi-k periodic RHF with the full Ewald Coulomb sum, read off properties (gap, bands, DOS), and visualize a crystalline orbital. LiH was the canonical testbed for the pob-TZVP basis (Peintinger, Vilela Oliveira & Bredow, 2013), so this tutorial doubles as a calibration check that your install reproduces the published numbers.
Wall: ~5–15 min on a laptop, ~2–3 min on a 32-core box.
Pass progress=True to run_rhf_periodic_scf (v0.5.1) to
stream per-iteration SCF lines to stdout — without it the script
looks frozen during the multi-minute Fock build. Under
nohup python my_script.py > log 2>&1 & + tail -f log the
flushed output shows progress unfold in real time.
Companion script:
examples/input-lih-pob-tzvp.py
reproduces every code block on this page end-to-end.
Why LiH + pob-TZVP¶
LiH rocksalt is the simplest 3D ionic crystal: one Li⁺ and one H⁻ per primitive (FCC) cell, lattice parameter 4.084 Å, gap of ~5 eV. Small enough to converge in minutes, big enough to exercise every periodic kernel — multi-k Ewald Coulomb, IBZ symmetry reduction, lattice ERI sums.
pob-TZVP is the modern standard solid-state Gaussian basis (Peintinger, Vilela Oliveira & Bredow 2013). Designed by removing or re-tuning the smallest-exponent primitives in TZVP that cause linear dependencies in periodic calculations. LiH was one of the test systems in the original paper — H is among the cleanest pob-TZVP atoms.
Anything you can do on LiH with this script you can do on any other 3D crystal of similar size — MgO, CaO, NaCl, simple oxides, ionic fluorides. Replace the lattice + atom block, keep the rest.
Step 1: Build the cell¶
import numpy as np
import vibeqc as vq
# Conventional cubic cell — 4 Li + 4 H, 8 atoms total. Easier to
# reason about visually than the 2-atom primitive FCC cell, at the
# cost of a 4× larger SCF.
A_ANG = 4.084 # experimental, RT
A = A_ANG / 0.529177210903 # bohr
lat = A * np.eye(3)
LI_FRAC = [(0,0,0), (0,0.5,0.5), (0.5,0,0.5), (0.5,0.5,0)]
H_FRAC = [(0.5,0.5,0.5), (0.5,0,0), (0,0.5,0), (0,0,0.5)]
unit_cell = []
for fx, fy, fz in LI_FRAC:
unit_cell.append(vq.Atom(3, [fx*A, fy*A, fz*A])) # Li
for fx, fy, fz in H_FRAC:
unit_cell.append(vq.Atom(1, [fx*A, fy*A, fz*A])) # H
system = vq.PeriodicSystem(dim=3, lattice=lat, unit_cell=unit_cell)
basis = vq.BasisSet(system.unit_cell_molecule(), "pob-tzvp")
print(f"Basis: pob-TZVP, {basis.nbasis} basis functions per cell")
The conventional cell has 8 atoms; pob-TZVP gives 52 basis functions per cell on this geometry. The primitive FCC cell has 2 atoms (and ~13 basis functions) — same chemistry, smaller compute, less obvious geometry. Pick the conventional cell whenever the visualization matters, the primitive when wall-time matters.
For a quick “is this a known crystal” sanity check, attach spglib symmetry and read off the spacegroup:
vq.attach_symmetry(system, symprec=1e-4)
print(system.symmetry.international_symbol) # → 'Fm-3m'
print(system.symmetry.number) # → 225
Rocksalt is Fm-3m (#225, point group O_h). If your build returns something else, double-check the atom assignment.
Step 2: SCF with EWALD_3D + IBZ-reduced k-mesh¶
opts = vq.PeriodicSCFOptions()
opts.lattice_opts.coulomb_method = vq.CoulombMethod.EWALD_3D
opts.lattice_opts.cutoff_bohr = 12.0
opts.lattice_opts.nuclear_cutoff_bohr = 25.0
opts.conv_tol_energy = 1e-8
opts.max_iter = 80
# 4×4×4 Monkhorst-Pack mesh, reduced to the IBZ via spglib.
# Fm-3m at this mesh has only 4 irreducible k-points — the SCF is
# 4× cheaper than the naïve 64-point full mesh.
kpts = vq.KPoints.monkhorst_pack(system, [4, 4, 4], symmetry=True)
print(f"k-mesh: 4×4×4 → {len(kpts)} IBZ points")
result = vq.run_rhf_periodic_scf(system, basis, kpts, opts, progress=True)
print(f"E/cell = {result.energy:.8f} Ha")
print(f"Iters = {result.n_iter} ({'converged' if result.converged else 'NOT'})")
progress=True (v0.5.1) streams a flushed banner + per-stage
timings + per-iteration SCF lines to stdout while the SCF runs.
Critical for any multi-minute job: without it the script looks
frozen between the “running SCF…” print and the next print. Set
VIBEQC_LIVE_LOGGING=0 in the environment to silence it for
batch scripts.
CoulombMethod.EWALD_3D is the right default for any 3D bulk: the
direct-truncated Coulomb sum is conditionally convergent (see
tutorial 6 — Madelung with Ewald) and
gives shape-dependent answers. The Ewald path is unconditionally
convergent and routes through the same dispatcher
(run_rhf_periodic_scf).
The symmetry=True flag wires through spglib’s IBZ reduction (Phase
K2). For high-symmetry crystals like rocksalt this is a 16× saving
on a 4×4×4 mesh; for low-symmetry P1 cells it’s a no-op.
Step 3: Read the fundamental gap¶
vibe-qc doesn’t expose per-site Mulliken charges for periodic systems
yet (that’s a v0.6 follow-up — see roadmap), but the per-k orbital
energies are right there in result.mo_energies and give you the
HOMO–LUMO gap for free:
n_occ = sum(int(a.Z) for a in system.unit_cell) // 2
homo = max(float(eps[n_occ - 1]) for eps in result.mo_energies)
lumo = min(float(eps[n_occ]) for eps in result.mo_energies)
print(f"HOMO max = {homo:.4f} Ha ({homo * 27.2114:.3f} eV)")
print(f"LUMO min = {lumo:.4f} Ha ({lumo * 27.2114:.3f} eV)")
print(f"Gap = {(lumo - homo) * 27.2114:.3f} eV")
For LiH at HF/pob-TZVP with this k-mesh, expect a gap around 8–9 eV. HF systematically over-estimates band gaps by roughly 3 eV in ionic crystals (no electron correlation in the conduction band). Experimental optical gap is ~5 eV; PBE/pob-TZVP gives ~3 eV (DFT under-estimates by a similar amount); HSE06 and beyond give the right answer. The number HF gives you is consistent — useful for trends across systems, not for matching a single experimental gap.
Step 4: Band structure along the standard HPKOT path¶
# Auto-detect Bravais → seekpath returns the canonical Hinuma 2017
# k-path. For Fm-3m: Γ-X-W-K-Γ-L-U-W-L-K|U-X.
band_kp = vq.KPoints.band_path(system)
print(f"Path: {' → '.join(label for _, label in band_kp.labels)}")
bands = vq.band_structure_hcore(
system, basis, band_kp.to_kpath(),
n_electrons_per_cell=2 * n_occ,
)
For now we use Hcore bands — non-interacting eigenvalues of T+V
along the path. They give the right band shape and symmetry; the
full SCF Fock matrix would shift everything by the Hartree + exchange
contributions but doesn’t change the topology. Quantitative SCF
bands are queued for v0.6 (we need the converged real-space Fock
exposed as a LatticeMatrixSet; today the SCF result returns
per-k Fock matrices instead).
Note
The path vq.KPoints.band_path(system) returns is the
HPKOT (Hinuma 2017) standard — the same convention used by
Materials Project, AFLOW, and seekpath itself. Cross-checking
your bands against published reference data is straightforward
because the path is fixed by Bravais type.
Step 5: DOS + atom-projected DOS¶
dos = vq.density_of_states_hcore(
system, basis, [4, 4, 4],
sigma=0.01, n_electrons_per_cell=2 * n_occ,
)
# atom + l projection — separates Li-s, H-s, plus any p contribution
# at higher energies.
pdos = vq.density_of_states_projected_hcore(
system, basis, [4, 4, 4],
projection="atoms_l", # or "atoms" for atom-only
sigma=0.01,
n_electrons_per_cell=2 * n_occ,
)
The valence band of LiH is pure H 1s with a small Li 2s tail — the H⁻ holds the extra electron in an essentially atomic 1s. The conduction band is predominantly Li 2s + 2p. The PDOS makes this immediate: at E_F – δ, H-s dominates; at E_F + δ, Li-s/Li-p take over. That’s the chemical signature of a closed-shell ionic crystal.
Step 6: HOMO Bloch orbital cube¶
# Find Γ in the IBZ list — typically index 0, but verify.
k_cart = np.asarray(kpts.kpoints_cart)
gamma_idx = int(np.argmin(np.linalg.norm(k_cart, axis=1)))
# 2×2×2 supercell tile of the orbital, ready to open in VESTA / XCrySDen.
vq.write_cube_mo_periodic(
"lih-homo-gamma.cube",
system, basis,
np.asarray(result.mo_coeffs[gamma_idx]),
k_cart[gamma_idx],
n_occ - 1, # HOMO band index
n_replica=(2, 2, 2),
spacing_bohr=0.25,
title="LiH HOMO @ Γ (RHF/pob-TZVP)",
)
Open the cube with VESTA (vesta lih-homo-gamma.cube) or
XCrySDen (xcrysden --cube lih-homo-gamma.cube). At Γ the
orbital is real and the isosurface shows H 1s spheres on every
H site, slightly delocalized into the Li 2s neighbors via small
amplitude tails. That’s the picture: H⁻ holds the electron, with a
~10% admixture of Li 2s — the “ionic with covalent character”
description that pob-TZVP is designed to capture.
For non-Γ k-points the orbital is complex; pass
component="real", "imag", "abs", or "abs2" to
write_cube_mo_periodic to control which scalar field gets
written. The default is "real".
Tip
Why a cube and not the moltui terminal viewer? moltui
(tutorial 27) is a great molecular
viewer but doesn’t yet handle periodic-cell metadata — the cube
file’s lattice vectors, the BZ, image-atom replication. Open
discussion with the upstream developer to extend it for solids;
in the meantime VESTA / XCrySDen / VMD are the right tools for
crystalline orbitals.
Theory¶
Bloch’s theorem in a Gaussian basis¶
For a translationally invariant Hamiltonian, eigenstates are labeled by a band index \(n\) and a crystal momentum \(\mathbf{k}\) in the first Brillouin zone:
Each AO \(\chi_\mu\) in the reference cell is replaced by a Bloch-summed combination:
The Roothaan eigenvalue problem becomes block-diagonal in \(\mathbf{k}\):
with each k-space matrix the discrete Fourier transform of the real-space lattice-summed matrix \(\mathbf{F}(\mathbf{k}) = \sum_\mathbf{T} e^{i\mathbf{k}\cdot\mathbf{T}}\mathbf{F}(\mathbf{T})\). This is the “build in real space, diagonalize in k-space” pattern that CRYSTAL pioneered for Gaussian crystalline orbitals (tutorial 4 covers it in detail).
Why pob-TZVP¶
Standard molecular Gaussian basis sets (TZVP, def2-TZVP, cc-pVTZ, …) include a long tail of small-exponent primitives that extend deep into neighboring cells and produce a near-singular overlap matrix when summed across the lattice. Stripping those primitives (or replacing them with sharper alternatives) gives pob-* — the “Periodic, Optimized, Bredow” family. Three variants ship today:
Basis |
Source |
Strengths |
|---|---|---|
pob-DZVP |
Bredow group, 2013 |
Cheapest; minimal redundancy |
pob-TZVP |
PVO-Bredow, 2013 |
Standard production basis; this tutorial |
pob-TZVP-rev2 |
PVO-Bredow, 2018 |
Refined valence space; pairs well with PBE |
pob-DZVP-rev2 |
PVO-Bredow, 2018 |
Cheaper rev2 variant |
For more on the design, see tutorial 16.
Coulomb sum convergence in 3D¶
The per-cell electron–electron Coulomb sum
\(\sum_\mathbf{T} 1/|\mathbf{r}-\mathbf{T}|\) is conditionally
convergent: in 3D it sums to different values depending on the
shape over which you truncate (sphere, cube, slab). Direct truncation
(CoulombMethod.DIRECT_TRUNCATED, vibe-qc’s default for
back-compat) gives shape-dependent energies and 0.1–1 Ha per-cell
errors on bulk. The fix is Ewald summation — split each \(1/r\)
into an erfc-screened short-range piece (real-space, exponential
convergence) plus a Gaussian-charge long-range piece (FFT-based
reciprocal-space convolution, exponential convergence too). vibe-qc’s
EWALD_3D path is validated to ω-invariance at \(10^{-8}\) Ha across
H₂ / LiH / MgO / Ne reference cells — see
user_guide/ewald.md.
IBZ reduction and the speed-up¶
For an MP mesh of \(N\) k-points and a system with point-group order \(|G|\), the irreducible Brillouin zone contains roughly \(N/|G|\) distinct k-points (modulo time-reversal symmetry). Per-iteration SCF cost scales as \(O(N_\text{IBZ} \cdot N_\text{bf}^4)\) — for LiH at 4×4×4 with \(|G|=48\), that’s a 16× saving over the full mesh.
Hex / trigonal cells have a subtle trap: the classical
Monkhorst-Pack convention shifts even meshes by half a step, which
breaks the three-fold rotational symmetry and inflates the IBZ
count for no accuracy gain. vibe-qc’s K2 phase refuses non-zero
shifts on spacegroups 143–194 with an actionable error pointing
at KPoints.gamma_centred(...). For Fm-3m (this tutorial) the
classical shift is fine and gets applied automatically.
What’s next¶
Phonons + thermochemistry of LiH. Needs Phase 21 (periodic phonons via finite-difference on top of Phase G1 periodic gradients, both queued for v0.7-ish).
Full SCF bands instead of Hcore bands. The piece we’re missing is exposing the converged real-space Fock matrix as a
LatticeMatrixSet; that’s a small wiring change in v0.6.DFT bands with PBE / B3LYP. Replace
run_rhf_periodic_scfwithrun_rks_periodic_scfand passvq.PeriodicKSOptions(functional="PBE"). Same script structure, better gap.Other ionic crystals. MgO is the next standard pob-TZVP testbed; CaO, NaCl, KCl follow.
References¶
Original pob-TZVP paper. M. F. Peintinger, D. Vilela Oliveira, T. Bredow, “Consistent Gaussian basis sets of triple-zeta valence with polarization quality for solid-state calculations,” J. Comp. Chem. 34, 451 (2013).
HPKOT band-path standardization. Y. Hinuma, G. Pizzi, Y. Kumagai, F. Oba, I. Tanaka, “Band structure diagram paths based on crystallography,” Comp. Mat. Sci. 128, 140 (2017). Implemented via seekpath.
Saunders–Dovesi multipolar 3D Ewald. V. R. Saunders et al., “On the electrostatic potential in crystalline systems where the charge density is expanded in Gaussian functions,” Mol. Phys. 77, 629 (1992).
Monkhorst-Pack k-sampling. H. J. Monkhorst, J. D. Pack, “Special points for Brillouin-zone integrations,” Phys. Rev. B 13, 5188 (1976).
CRYSTAL-style Gaussian crystalline orbitals. C. Pisani, R. Dovesi, C. Roetti, Hartree-Fock Ab Initio Treatment of Crystalline Systems, Lecture Notes in Chemistry 48, Springer (1988).
See also¶
Tutorial 4 — Periodic HF on a 1D H-chain — Bloch-sum machinery, simpler 1D system
Tutorial 5 — Periodic KS-DFT — same machinery for DFT functionals
Tutorial 6 — Madelung constants via Ewald — what conditional convergence in 3D actually means
Tutorial 12 — Band structure — Hcore bands on the H-chain
Tutorial 16 — pob-TZVP rationale — basis design
Tutorial 21 — Projected DOS — PDOS in detail
Tutorial 22 — Periodic orbital cubes — the Bloch-orbital cube path in depth
User guide — Ewald — the EWALD_3D dispatch
User guide — k-points — the
KPointsbuilder APIUser guide — Crystal lattices — Bravais types + WS cells + BZ shapes