Moving from PySCF

vibe-qc validates its molecular SCF stack against PySCF to machine precision (see external_codes § PySCF and the regression suite under examples/regression/), so users moving from PySCF have the easiest migration path of any QC code. The chemistry is the same; the idioms differ at a handful of well-defined points.

This page is a side-by-side crosswalk. Each section pairs a PySCF snippet with the equivalent vibe-qc call and notes any behavioural difference worth knowing.

Molecules

PySCF

from pyscf import gto

mol = gto.M(
    atom = """O 0 0 0
              H 0 0.757  0.587
              H 0 -0.757 0.587""",
    basis = "6-31g*",
    charge = 0,
    spin = 0,                 # 2S = 0  (singlet)
)

vibe-qc

import vibeqc as vq

mol = vq.Molecule([
    vq.Atom(8, [ 0.0,  0.000,  0.000]),    # bohr, not Angstrom
    vq.Atom(1, [ 0.0,  1.431, -0.987]),
    vq.Atom(1, [ 0.0, -1.431, -0.987]),
], charge=0, multiplicity=1)                # 2S+1, not 2S

basis = vq.BasisSet(mol, "6-31g*")           # separate from Molecule

Important

Three differences worth pinning to:

  1. Units. vibe-qc uses bohr internally for everything. The convenient Molecule.from_xyz("h2o.xyz") parser reads Ångström from the file (XYZ convention) and converts on the way in. PySCF defaults to Ångström.

  2. Spin convention. PySCF’s mol.spin is 2S (number of unpaired electrons). vibe-qc’s Molecule.multiplicity is 2S+1 (multiplicity). PySCF singlet = 0; vibe-qc singlet = 1. The Molecule constructor validates multiplicity against the electron count.

  3. Basis sets are a separate object. PySCF folds the basis into the Mole. vibe-qc keeps Molecule and BasisSet as two distinct objects so the same geometry can be evaluated at multiple basis qualities without rebuilding the molecule.

For XYZ-on-disk geometries the convenience method matches PySCF’s ergonomics:

mol = vq.Molecule.from_xyz("h2o.xyz")        # Angstrom in file
# Equivalent to: gto.M(atom="h2o.xyz", basis="6-31g*")

Restricted Hartree-Fock

PySCF

from pyscf import scf

mf = scf.RHF(mol)
mf.conv_tol = 1e-9
mf.max_cycle = 100
mf.kernel()
print(mf.e_tot)
print(mf.mo_energy)
print(mf.mo_coeff)

vibe-qc

opts = vq.RHFOptions()
opts.conv_tol_energy = 1e-9
opts.max_iter        = 100

result = vq.run_rhf(mol, basis, opts)
print(result.energy)
print(result.mo_energies)
print(result.mo_coeffs)
print(result.converged)        # bool — never silent like mf.converged

PySCF

vibe-qc

mf.e_tot

result.energy

mf.mo_energy

result.mo_energies

mf.mo_coeff

result.mo_coeffs

mf.mo_occ

result.occupations

mf.make_rdm1()

result.density

mf.get_fock()

result.fock

mf.get_ovlp()

result.overlap

mf.scf_summary

the formatted block in output-*.out

The result object is a frozen dataclass — mf is a stateful object that re-runs SCF on .kernel() calls.

Unrestricted / open-shell

PySCF

mol = gto.M(atom="...", basis="6-31g*", spin=1)   # doublet
mf = scf.UHF(mol).run()

vibe-qc

mol = vq.Molecule([...], multiplicity=2)          # doublet (2S+1=2)
result = vq.run_uhf(mol, basis)

result.alpha.mo_energies      # per-spin blocks
result.alpha.mo_coeffs
result.beta.mo_energies
result.beta.mo_coeffs
result.density                # total D = D_α + D_β
result.spin_density           # D_α − D_β

Note

The single source of truth for spin in vibe-qc is Molecule.multiplicity — there is no spin field on UHFOptions. This is a common source of confusion for PySCF users; see molecules § Configuring open-shell systems.

Kohn-Sham DFT

PySCF

from pyscf import dft

mf = dft.RKS(mol, xc="PBE")
mf.grids.level = 3
mf.kernel()

vibe-qc

opts = vq.RKSOptions()
opts.functional   = "PBE"
opts.grid.level   = 3        # 0=coarse, 5=ultrafine; default 3
result = vq.run_rks(mol, basis, opts)

For hybrid functionals just change the name — both codes resolve through libxc and accept the same short aliases:

opts.functional = "B3LYP"     # PySCF: dft.RKS(mol, xc="B3LYP")
opts.functional = "PBE0"
opts.functional = "wB97X-D"   # range-separated hybrid (queued for v0.x.x)

Warning

B3LYP convention difference at v0.8.0. vibe-qc’s b3lyp keyword resolves to libxc id 475 (VWN5 — ORCA / ADF convention); PySCF’s default b3lyp is id 402 (VWN3 — Gaussian convention). The two differ by ~10-15 mHa per heavy atom. For strict PySCF parity, pass functional="b3lyp/g" (the Gaussian-compatible variant). See functionals for details.

Geometry optimisation

PySCF (via PyBerny / geomeTRIC)

from pyscf.geomopt.berny_solver import optimize
mol_opt = optimize(mf, maxsteps=200)

vibe-qc (via ASE / BFGS)

from vibeqc import run_job

run_job(
    mol, basis="6-31g*", method="rks", functional="PBE",
    optimize=True,
    fmax=1e-3,
    max_opt_steps=200,
    output="output-h2o-opt",
)

run_job writes the trajectory to output-h2o-opt.traj (an ASE binary trajectory — ase gui output-h2o-opt.traj animates it), the final geometry to output-h2o-opt.xyz, and the SCF log to output-h2o-opt.out. See output_files for the full file family.

For programmatic optimisation outside run_job, use the ASE Calculator integration:

from ase.optimize import BFGS
from ase.build import molecule
from vibeqc.ase import VibeQC

atoms = molecule("H2O")
atoms.calc = VibeQC(method="rks", functional="PBE", basis="6-31g*")
BFGS(atoms).run(fmax=1e-3)

Periodic systems

PySCF.pbc

from pyscf.pbc import gto, scf

cell = gto.Cell()
cell.atom = "Mg 0 0 0; O 2.105 2.105 2.105"
cell.a = [[4.21, 0, 0], [0, 4.21, 0], [0, 0, 4.21]]    # Angstrom
cell.basis = "pob-tzvp"
cell.build()

kpts = cell.make_kpts([4, 4, 4])
mf = scf.KRHF(cell, kpts=kpts).run()
print(mf.e_tot)

vibe-qc

import numpy as np

sysp = vq.PeriodicSystem(
    dim=3,
    lattice=np.eye(3) * 7.957,     # bohr (4.21 Å)
    unit_cell=[
        vq.Atom(12, [0.0, 0.0, 0.0]),               # Mg
        vq.Atom(8, [3.979, 3.979, 3.979]),          # O — bohr
    ],
)
basis = vq.BasisSet(sysp.unit_cell_molecule(), "pob-tzvp")
kmesh = vq.monkhorst_pack(sysp, [4, 4, 4])

opts = vq.PeriodicRHFOptions()
opts.lattice_opts.coulomb_method = vq.CoulombMethod.EWALD_3D
result = vq.run_rhf_periodic(sysp, basis, kmesh, opts)
print(result.energy)

PySCF.pbc

vibe-qc

gto.Cell

vq.PeriodicSystem(dim=3, ...)

cell.a (Å, rows)

lattice (bohr, columns)

cell.make_kpts([n,n,n])

vq.monkhorst_pack(sysp, [n,n,n])

scf.KRHF(cell, kpts=kpts)

vq.run_rhf_periodic(sysp, basis, kmesh, opts)

scf.KRKS(cell, kpts=kpts, xc="PBE")

vq.run_rks_periodic(sysp, basis, kmesh, opts) with opts.functional = "PBE"

mf.with_df = df.GDF(cell)

the default for Γ-only via run_rhf_periodic_gamma_gdf

Important

Lattice vectors are columns, in bohr. PySCF.pbc stores them as rows in Ångström. The conversion is lattice_bohr_cols = lattice_angstrom_rows.T * 1.8897259886.

For 1D / 2D systems, set dim=1 or dim=2 and put generous vacuum (30+ bohr) on the non-periodic axes. PySCF.pbc uses the dimension keyword on Cell for the same purpose.

DFT-D3 / D4 dispersion

PySCF

# PySCF wires dispersion through dft.RKS via the xc string:
mf = dft.RKS(mol, xc="PBE-D3BJ").run()
# or via the dftd3/dftd4 PySCF plugin

vibe-qc

# Via run_job (recommended):
run_job(mol, basis="def2-tzvp", method="rks",
        functional="PBE", dispersion="d3bj",
        output="output-pbe-d3bj")

# Via run_rks directly:
opts = vq.RKSOptions()
opts.functional = "PBE"
opts.dispersion = "d3bj"        # or "d3", "d4"
result = vq.run_rks(mol, basis, opts)
print(result.energy)              # = E_DFT + E_disp
print(result.dispersion_energy)   # = E_disp alone

D3(BJ) is wired through the molecular runner via compute_d3bj; D4 lands at v0.8.0 via the optional dftd4 package.

Density fitting (RIJ / RIJK / RIJCOSX)

PySCF

mf = scf.RHF(mol).density_fit(auxbasis="def2-svp-jkfit")
mf.kernel()

vibe-qc

opts = vq.RHFOptions()
opts.density_fit = True
opts.aux_basis = "def2-svp-jk"     # or vq.default_aux_basis_for("def2-svp", kind="jk")
opts.cosx = False                  # set True for RIJCOSX (large hybrid DFT)
result = vq.run_rhf(mol, basis, opts)

The JKBuilder polymorphic Fock build picks one of three concrete kernels — direct four-index, DF (RIJK), or DF + COSX — based on the flags. See density_fitting for the when-to-use-which table.

MP2 / RI-MP2

PySCF

from pyscf import mp
mp2 = mp.MP2(mf).run()
print(mp2.e_corr, mp2.e_tot)

vibe-qc

mp2_result = vq.run_mp2(mol, basis, result)        # `result` from run_rhf
print(mp2_result.e_corr)
print(mp2_result.e_total)      # = e_HF + e_corr

# Same-spin / opposite-spin decomposition:
print(mp2_result.e_ss, mp2_result.e_os)

# RI-MP2 (much cheaper at large basis):
mp2_result = vq.run_mp2(mol, basis, result, aux_basis="def2-tzvp-rifit")

UMP2 is vq.run_ump2(mol, basis, uhf_result).

Effective Core Potentials

PySCF

mol = gto.M(
    atom = "Pt 0 0 0",
    basis = "lanl2dz",
    ecp = "lanl2dz",        # ECP carried by the basis
)

vibe-qc

# Until Phase 14e (basissetdev-conditional auto-ECP) lands on main,
# wire ECPCenter manually:
mol = vq.Molecule([vq.Atom(78, [0.0, 0.0, 0.0])])
basis = vq.BasisSet(mol, "lanl2dz")

opts = vq.UHFOptions()
opts.ecp_centers = [vq.ECPCenter(Z=78, xyz=[0.0, 0.0, 0.0])]
opts.ecp_library = "lanl2dz"     # or "ecp60mdf" for Stuttgart MDF

result = vq.run_uhf(mol, basis, opts)

See ecp for the full table mapping basis-set families to ECP libraries.

Initial guess

PySCF

mf.init_guess = "minao"     # or "atom", "1e", "huckel"
mf.kernel()

vibe-qc

opts = vq.RHFOptions()
opts.initial_guess = vq.InitialGuess.SAP       # SAP / SAD / HCORE / AUTO / ...
result = vq.run_rhf(mol, basis, opts)

InitialGuess.AUTO (default in v0.8.0) inspects the system and picks SAP (light closed-shell) or SAD (transition metal / periodic). See initial_guess for the full table.

SCF convergence acceleration

PySCF

mf.DIIS = scf.EDIIS            # diagonal switch
mf.diis_space = 8
mf.level_shift = 0.2
mf.damp = 0.5

vibe-qc

opts.scf_accelerator = vq.SCFAccelerator.EDIIS_DIIS   # default v0.8.0+
opts.diis_subspace_size = 8
opts.level_shift = 0.2
opts.damping = 0.5

# Second-order finalizer:
opts.newton_threshold = 1.0    # Phase D2c Newton

vibe-qc defaults to the EDIIS+DIIS hybrid (Garza-Scuseria 2012); PySCF still defaults to plain DIIS as of pyscf 2.x. See the stiff-convergence tutorial for when each matters.

Properties

PySCF

charges = mf.mulliken_pop()[1]
dip = mf.dip_moment()

vibe-qc

mul = vq.mulliken_charges(result, basis, mol)
low = vq.loewdin_charges(result, basis, mol)
bonds = vq.mayer_bond_orders(result, basis, mol)
dip = vq.dipole_moment(result, basis, mol)
print(dip.total_debye)

run_job writes a formatted properties block to output-*.out automatically — see properties.

What vibe-qc does that PySCF doesn’t (yet)

Things you don’t get out of the box in PySCF that vibe-qc gives you:

  • Auto-citations. Every run_job writes a .bibtex / .references pair listing every paper to cite for the functional / basis / dispersion combination used. See citations.

  • Pre-flight memory estimator. Aborts before the SCF starts if the dense-ERI / DFT-grid / MP2 working set exceeds available RAM. See memory.

  • .system manifest. TOML manifest with hardware, library versions, plan + outputs status. Lets the vq queue detect crashed jobs and fetch only the declared artefacts. See output_files.

  • pob- basis sets.* The Peintinger-Vilela Oliveira-Bredow solid-state basis family is bundled. See basis_sets and tutorial 16.

  • CRYSTAL-format basis parser. Drop a CRYSTAL-style per-element file under basis_library/custom/ and setup_basis_library.sh picks it up.

What PySCF does that vibe-qc doesn’t (yet)

Things to keep in PySCF for now:

  • Range-separated hybrids (HSE06, ωB97X-V, ωB97M-V). The libxc ids resolve but the RSH machinery for periodic K isn’t wired — queued for v0.x.x.

  • Meta-GGAs and r²SCAN-3c composites. Queued for v0.9.0.

  • Coupled-cluster (CCSD / CCSD(T)). vibe-qc tops out at MP2 today. The roadmap targets CCSD via the cyclic-cluster model in v2.x.

  • TD-DFT / excited states. Not in scope for v0.8.x.

  • MCSCF / multi-reference. Not on the roadmap.

  • Multi-k UHF / UKS. Γ-only and multi-k closed-shell are shipped; open-shell multi-k is roadmap.

  • CCSD-DLPNO / domain-based local correlation. Not on the roadmap.

For any of these, drop back to PySCF or another QC code — vibe-qc plays well with everything via the external_codes framework, so you can do an HF reference in vibe-qc and the post-SCF correlation in PySCF / ORCA / Q-Chem on the same Atoms.

Validating vibe-qc against PySCF

Drop-in cross-validation lives at examples/regression/runner_pyscf.py. Per CLAUDE.md § 10 vibe-qc never imports PySCF at runtime — the runner subprocesses PySCF, parses its output, and produces a side-by-side comparison artefact. The molecular HF / DFT / MP2 paths in vibe-qc match PySCF to machine precision on the bundled examples.

See also