Molecular basis-set optimisation¶
vibe-qc can re-optimise the free parameters of a Gaussian basis set,
its exponents and contraction coefficients, against a molecular energy,
using the analytic basis-parameter gradient and a robust BDIIS loop.
It is the basis-set analogue of CRYSTAL’s OPTBASIS, but it drives the
optimiser with the analytic gradient instead of per-parameter
finite-difference re-SCFs, and it works for any of RHF / UHF / RKS / UKS.
The one-call entry point is optimize_molecular_basis.
Note
This is the in-process molecular path. The separate multi-crystal,
derivative-free production recipe
(vibeqc.basis_optimization.recipes.production, NLopt over an
EnergyEngine) is a different workflow aimed at
solid-state basis libraries. It needs an extra install step, see
The external-program path below.
The external-program path¶
The molecular recipe below runs entirely inside vibe-qc and needs
nothing beyond a normal install. The solid-state recipes are different:
they drive an external SCF program (CRYSTAL today) out of process
through the co-located vibe-basis driver, so they need that driver
installed.
These six modules import vibe_basis at module load:
Module |
Role |
|---|---|
|
|
|
multi-crystal objective |
|
single-atom stage |
|
bulk-solid stage |
|
full test-set stage |
|
production refit |
Without the driver they raise ModuleNotFoundError: No module named 'vibe_basis' at import. For a standalone environment that does not build
vibe-qc, use the component lifecycle:
./vibe-basis/scripts/install.sh
That creates vibe-basis/.venv; matching update.sh, reinstall.sh, and
uninstall.sh commands manage it safely. To add the driver to an existing
vibe-qc environment instead, use vibe-qc’s basisopt extra:
uv pip install -e '.[basisopt]'
pip ignores [tool.uv.sources], so under pip install it in two steps:
pip install -e . && pip install -e vibe-basis/
Check an environment before starting a campaign:
.venv/bin/python examples/basisset_dev/check_basisopt_install.py
That script reports both paths separately and exits nonzero when the
external path is unusable, so it works as a guard at the top of a
campaign script. See Installation for the full
notes, including the --no-build-isolation caveat.
Note
vibe-basis is a driver, not a solver. It emits input decks, submits
them (locally or through the vq queue), and parses what comes back;
the energies are computed by the external program. It is versioned
independently of vibe-qc, see vibe-basis/VERSIONING.md.
The one-call recipe¶
You declare which parameters are free, supply the molecule, pick the reference, and call the optimiser:
import vibeqc as vq
from vibeqc.basis_crystal import parse_crystal_atom_basis_file, emit_g94
from vibeqc.basis_optimization.parametrise import (
BasisParametrisation, FreeSpec, Transform,
)
from vibeqc.basis_optimization.recipes.molecular import optimize_molecular_basis
# 1. The starting basis and which parameters are free. FreeSpec picks one
# parameter by (symbol, shell index, primitive index, kind); LOG transform
# plus a lower bound guard against collapse toward linear dependence.
atoms = {"O": parse_crystal_atom_basis_file(...), "H": parse_crystal_atom_basis_file(...)}
parametrisation = BasisParametrisation(
atoms=atoms,
free=[
FreeSpec("O", 7, 0, "exponent", transform=Transform.LOG, bounds=(0.05, None)),
FreeSpec("H", 3, 0, "exponent", transform=Transform.LOG, bounds=(0.05, None)),
],
)
# 2. A factory that returns the molecule to optimise against (re-evaluated
# each step). Geometry in bohr.
def molecule_factory():
return vq.Molecule([vq.Atom(8, [0.0, 0.0, 0.0]),
vq.Atom(1, [0.0, 1.4304, 1.1071]),
vq.Atom(1, [0.0, -1.4304, 1.1071])], multiplicity=1)
# 3. Optimise (closed-shell RKS/PBE here; functional=None gives HF).
res = optimize_molecular_basis(parametrisation, molecule_factory, functional="PBE")
print(res.summary())
optimised = res.optimized_atoms # {symbol: CrystalAtomBasis} at the optimum
print(emit_g94([optimised["O"]])) # write the improved O basis as a Gaussian-94 deck
Choosing the reference¶
The functional and open_shell arguments select the SCF reference the
basis is optimised against, exactly as for an ordinary single point:
call |
reference |
|---|---|
|
RHF (closed shell) or UHF ( |
|
RKS (closed shell) or UKS ( |
What you get back¶
optimize_molecular_basis returns a MolecularOptResult:
optimized_atoms, the{symbol: CrystalAtomBasis}dict at the optimum, ready to write out withemit_g94(...)oremit_crystal(...).summary(), a formatted report of the objective, the improvement in mHa, and the per-parameter start and end values.improvement_mha,starting_objective/optimal_objective(Ha),n_evaluations,wall_seconds,converged,message.x_optimalandhistoryfor the optimiser trajectory.citations, the optimiser-method citation keys to cite when you publish an optimised basis (BDIIS, the conditioning penalty, the Pulay-DIIS papers);summary()prints them. Also cite the starting basis set and the functional.
Worked example¶
The bundled script examples/molecular/optimize_basis_h2o_pbe.py
re-optimises three pob-TZVP valence and polarisation exponents (O
diffuse-s, O d-polarisation, H p-polarisation) for an isolated water
molecule at RKS/PBE. Its measured run:
molecular basis optimisation (RKS/PBE)
objective: -76.36859599 -> -76.37085698 Ha
improvement: +2.2610 mHa (lower is better)
evals: 14 wall: ~55 s
converged: True
O.shell3.prim0.exponent 0.356587 -> 0.346200 (diffuse s)
O.shell7.prim0.exponent 0.253462 -> 0.513548 (d polarisation)
H.shell3.prim0.exponent 0.800000 -> 1.194617 (p polarisation)
pob-TZVP is calibrated for solids, so the molecular optimum tightens the polarisation functions and the landing point differs from the published pob-TZVP values: the point is the machinery, not the numbers.
Options and limits¶
The defaults are tuned for a well-conditioned exponent optimisation; the knobs worth knowing:
analytic(defaultTrue), drive BDIIS with the analytic gradient. SetFalseto fall back to the driver’s finite-difference gradient, needed for SP-coupledcoeff_s/coeff_pcontraction parameters, which have no closed-form derivative. Single-primitive exponents and ordinary contraction coefficients all have analytic gradients.use_cond_penalty/cond_gammaanduse_ld_penalty/ld_lambda, add a conditioning or linear-dependence penalty to keep the optimiser away from near-degenerate exponent sets (combine withgated_symbolsto apply it per element).max_iter,tol_energy,tol_grad,diis_size,trust_radius, the BDIIS controls.tol_graddefaults to 1e-5 for HF and 1e-4 for KS (the grid-based KS gradient is accurate to about 1e-5).
The recipe optimises a fixed contraction pattern (it moves exponents and coefficients, it does not add or remove primitives), and it is a molecular path: solid-state basis libraries go through the multi-crystal production recipe noted above.
See also¶
Basis sets, the bundled basis library and how custom decks are loaded.
examples/molecular/optimize_basis_h2o_pbe.py, the runnable script the measured numbers above come from.examples/basisset_dev/check_basisopt_install.py, the preflight guard for the external-program path.Installation § “Optional basis-set optimization driver (vibe-basis)”, how to install the driver and what it pulls in.
vibe-basis/TUTORIAL.md, the end-to-end walkthrough for a solid-state refit, from smoke test to publication-grade run.