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:
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.Spin convention. PySCF’s
mol.spinis2S(number of unpaired electrons). vibe-qc’sMolecule.multiplicityis2S+1(multiplicity). PySCF singlet = 0; vibe-qc singlet = 1. The Molecule constructor validatesmultiplicityagainst the electron count.Basis sets are a separate object. PySCF folds the basis into the
Mole. vibe-qc keepsMoleculeandBasisSetas 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 |
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
the formatted block in |
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 |
|---|---|
|
|
|
|
|
|
|
|
|
|
|
the default for Γ-only via |
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_jobwrites a.bibtex/.referencespair 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.
.systemmanifest. TOML manifest with hardware, library versions, plan + outputs status. Lets thevqqueue 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/andsetup_basis_library.shpicks 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¶
ase_integration — the ASE Calculator interface (a clean way to use vibe-qc through PySCF-adjacent ASE workflows).
external_codes — the cross-code validation framework. Run the same Atoms through vibe-qc + PySCF + ORCA and compare.
Tutorial 26 — cross-validation — end-to-end worked example of the same.
citations — vibe-qc’s auto-bibliography surface.
output_files — what every
run_jobwrites.