QTAIM: critical points and bond paths¶
A converged wavefunction gives you orbitals, but orbitals are not observables and their shapes depend on the basis and on unitary rotations that leave the energy unchanged. The electron density \(\rho(\mathbf r)\) is different: it is an observable, and its topology, the pattern of maxima, saddle points and minima, is the same whichever orbitals produced it. Bader’s quantum theory of atoms in molecules (QTAIM, Bader, R. F. W. (1985). *Atoms in molecules.* Accounts of Chemical Research, **18**(1), 9–15. doi:[10.1021/ar00109a003](https://doi.org/10.1021/ar00109a003)) reads chemistry from that topology. Every nucleus sits at a maximum of \(\rho\); two atoms are bonded when a ridge of density, the bond path, connects their maxima through a saddle point, the bond critical point (BCP); and the density and its Laplacian at the BCP tell a covalent bond from an ionic one or a hydrogen bond.
vibe-qc ships the critical-point search and the bond-path tracer in
vibeqc.qtaim, on the analytic AO Hessian of the native core, and writes
the result into the QVF archive as a topology.qtaim section. The
analysis is production on every molecular SCF reference (RHF, UHF, RKS,
UKS; see Methods: theory and status).
This page runs it on water, reads the numbers, repeats it on four bonds of
different character, and derives the classification rules in the theory
section.
Setup¶
Two routes lead to the same analysis. run_job(..., qtaim=True) runs the
topology after the SCF and stores it in the QVF; the library function
qtaim_analysis runs it on any converged result and returns Python
objects. The script does both, so you can see where each number ends up.
import json
import zipfile
import numpy as np
import vibeqc as vq
from vibeqc.qtaim import qtaim_analysis
ANG = 1.8897259886 # Angstrom -> bohr (vibe-qc coordinates are bohr)
BOHR = 0.529177210903 # bohr -> Angstrom
# Water, experimental-like geometry: r(OH) = 0.958 A, HOH = 104.5 deg.
mol = vq.Molecule([
vq.Atom(8, [0.0, 0.0, 0.0]),
vq.Atom(1, [0.0, 0.7572 * ANG, 0.5865 * ANG]),
vq.Atom(1, [0.0, -0.7572 * ANG, 0.5865 * ANG]),
], 0, 1)
# Route 1: the runner. qtaim=True adds a topology.qtaim section to the QVF.
result = vq.run_job(mol, basis="6-31g*", method="rhf", qtaim=True,
output="h2o_qtaim")
print(f"E(RHF/6-31G*) = {result.energy:.8f} Ha")
with zipfile.ZipFile("h2o_qtaim.qvf") as qvf:
manifest = json.loads(qvf.read("manifest.json"))
section = next(s for s in manifest["sections"] if s["kind"] == "topology.qtaim")
topology = json.loads(qvf.read(section["members"]["critical_points"]["path"]))
print("QVF critical points:", [cp["type"] for cp in topology["points"]])
# Route 2: the library function on the same converged result.
basis = vq.BasisSet(mol, "6-31g*")
top = qtaim_analysis(result, basis, mol) # grid_spacing=0.15 bohr by default
print(f"{'type':>4} {'atoms':>7} {'x/A':>7} {'y/A':>7} {'z/A':>7} "
f"{'rho/au':>8} {'lap/au':>9} {'eps':>6}")
for cp in top.critical_points:
x, y, z = cp.position # Angstrom
rho = cp.rho * BOHR**3 # e/A^3 -> e/bohr^3
lap = cp.laplacian * BOHR**5 # e/A^5 -> e/bohr^5
eps = "" if cp.ellipticity is None else f"{cp.ellipticity:6.3f}"
pair = "" if cp.atom_pair is None else f"{cp.atom_pair[0]}-{cp.atom_pair[1]}"
print(f"{cp.type:>4} {pair:>7} {x:7.3f} {y:7.3f} {z:7.3f} {rho:8.4f} {lap:9.4f} {eps:>6}")
print("bond paths:", [bp.atoms for bp in top.bond_paths])
for bp in top.bond_paths:
pts = np.asarray(bp.path) # Angstrom; runs atoms[0] -> BCP -> atoms[1]
length = np.linalg.norm(np.diff(pts, axis=0), axis=1).sum()
print(f"bond path {bp.atoms}: {len(pts)} points, length {length:.4f} A")
On a vibe-qc 0.17.10 development build (Apple M5 Max) the whole script takes about six seconds: the SCF converges in nine iterations to \(E = -76.00910804\) Ha, the runner’s topology adds about one second, and the second, explicit analysis another second.
E(RHF/6-31G*) = -76.00910804 Ha
QVF critical points: ['bcp', 'bcp', 'ncp', 'ncp', 'ncp']
type atoms x/A y/A z/A rho/au lap/au eps
bcp 2-0 -0.000 -0.605 0.474 0.3555 -1.9395 0.024
bcp 1-0 -0.000 0.605 0.474 0.3555 -1.9395 0.024
ncp 0.000 0.000 0.000 291.1051 -1926203.7956
ncp 0.000 0.757 0.586 0.3899 -16.8004
ncp 0.000 -0.757 0.586 0.3899 -16.8004
bond paths: [(2, 0), (1, 0)]
bond path (2, 0): 22 points, length 0.9579 A
bond path (1, 0): 22 points, length 0.9579 A
What just happened¶
qtaim_analysis lays a uniform grid over the molecule (0.15 bohr spacing,
2.5 bohr of padding), evaluates \(\rho\), \(\nabla\rho\) and the \(3\times3\)
Hessian \(\nabla\nabla\rho\) at every point with the analytic AO second
derivatives, picks the voxels where \(|\nabla\rho|\) is a local minimum as
seeds, and polishes each seed with Newton-Raphson steps until
\(|\nabla\rho| < 10^{-7}\). The sign pattern of the Hessian eigenvalues at
the converged point classifies it. Water has the textbook set: three
nuclear critical points (ncp) and two bond critical points (bcp), no
ring, no cage. The Poincare-Hopf relation \(n - b + r - c = 1\) (nuclei
minus bonds plus rings minus cages) gives \(3 - 2 + 0 - 0 = 1\), the first
sanity check on any critical-point search: a missing BCP or a spurious
ring shows up as a violation.
The two BCPs are what you look at. Each sits on the O-H axis, 0.769 A from the oxygen and 0.189 A from the hydrogen. That asymmetry is the first qualitative signal: the BCP lies close to the less electronegative partner, and its distance to each nucleus defines the bonded radius of that atom in this bond. The density at the BCP, \(\rho_b = 0.356\) e/bohr\(^3\), is high (a hydrogen bond has \(\rho_b \approx 0.01\) to \(0.04\), a C-C single bond about \(0.25\)), and its Laplacian \(\nabla^2\rho_b = -1.94\) e/bohr\(^5\) is strongly negative: density is locally concentrated at the BCP, which is the QTAIM signature of a shared-shell, covalent interaction. The ellipticity \(\varepsilon = 0.024\) is almost zero, so the density is cylindrically symmetric around the bond axis, as it should be for a \(\sigma\) bond with no \(\pi\) character.
The nuclear critical points are (3,-3) maxima. The density at the oxygen
nucleus, 291 e/bohr\(^3\), and its enormous negative Laplacian are dominated
by the 1s core and carry no bonding information; the values at the
hydrogens (0.39 e/bohr\(^3\)) are also cusp values and not comparable with
the BCP numbers. Strictly a Gaussian basis has no cusp, so the nuclear
maxima are found at the nuclear positions by construction rather than by
the search (qtaim_analysis inserts an NCP at every nucleus it did not
find within 0.5 bohr).
The unit conventions are worth keeping straight. CriticalPoint.position
is in Angstrom, rho in e/A\(^3\) and laplacian in e/A\(^5\); the script
multiplies by \(a_0^3\) and \(a_0^5\) to quote atomic units, which is what the
QTAIM literature uses. The QVF section
(topology/critical_points.json inside the archive) stores the same
fields in the same units, plus the bond_paths list; vibe-view’s
terminal mode lists it among the sections it reads (see
vibe-view terminal mode).
Each BondPath holds the two atoms it connects and the traced path
polyline in Angstrom. The tracer leaves the BCP along plus and minus the
eigenvector of the one positive Hessian eigenvalue and follows steepest
ascent on \(\rho\) to the two nuclei, so the polyline starts on
atoms[0], passes through the BCP and ends on atoms[1]. On water each
bond path has 22 points and a length of 0.9579 A, equal to the 0.9578 A
nuclear separation to 1e-4 A: the O-H bonds are straight. A strained or
bent bond shows up as a bond-path length longer than the internuclear
distance.
Variation: covalent, polar, ionic and pi bonds¶
The value of the Laplacian at the BCP is the classic QTAIM discriminator. The loop below runs the same analysis on four diatomics and one alkene, all at RHF/6-31G*, and prints only the bond critical points.
import numpy as np
import vibeqc as vq
from vibeqc.qtaim import qtaim_analysis
ANG = 1.8897259886
BOHR = 0.529177210903
def bcps(mol, label):
basis = vq.BasisSet(mol, "6-31g*")
res = vq.run_rhf(mol, basis)
top = qtaim_analysis(res, basis, mol)
for cp in top.critical_points:
if cp.type == "bcp":
print(f"{label:6s} {str(cp.atom_pair):8s} rho_b={cp.rho*BOHR**3:.4f}"
f" lap_b={cp.laplacian*BOHR**5:+.4f} eps={cp.ellipticity:.3f}")
bcps(vq.Molecule([vq.Atom(3, [0, 0, 0]), vq.Atom(1, [0, 0, 1.5957 * ANG])]), "LiH")
bcps(vq.Molecule([vq.Atom(9, [0, 0, 0]), vq.Atom(1, [0, 0, 0.917 * ANG])]), "HF")
bcps(vq.Molecule([vq.Atom(7, [0, 0, 0]), vq.Atom(7, [0, 0, 1.098 * ANG])]), "N2")
# Ethylene, D2h: r(CC) = 1.339 A, r(CH) = 1.087 A, HCH = 117.4 deg.
half_cc, r_ch, half = 1.339 * ANG / 2, 1.087 * ANG, np.deg2rad(117.4 / 2)
sy, cz = r_ch * np.sin(half), r_ch * np.cos(half)
c2h4 = vq.Molecule([
vq.Atom(6, [0, 0, -half_cc]), vq.Atom(6, [0, 0, half_cc]),
vq.Atom(1, [0, sy, -half_cc - cz]), vq.Atom(1, [0, -sy, -half_cc - cz]),
vq.Atom(1, [0, sy, half_cc + cz]), vq.Atom(1, [0, -sy, half_cc + cz]),
])
bcps(c2h4, "C2H4")
LiH (0, 1) rho_b=0.0368 lap_b=+0.1562 eps=0.000
HF (1, 0) rho_b=0.3512 lap_b=-2.6609 eps=0.000
N2 (0, 1) rho_b=0.6806 lap_b=-2.4681 eps=0.000
C2H4 (3, 0) rho_b=0.2792 lap_b=-1.0137 eps=0.013
C2H4 (5, 1) rho_b=0.2792 lap_b=-1.0137 eps=0.013
C2H4 (0, 1) rho_b=0.3483 lap_b=-1.0911 eps=0.445
C2H4 (2, 0) rho_b=0.2792 lap_b=-1.0137 eps=0.013
C2H4 (4, 1) rho_b=0.2792 lap_b=-1.0137 eps=0.013
Read the table row by row. LiH is the ionic case: a small \(\rho_b\) of 0.037 and a positive Laplacian, density depleted at the BCP and concentrated in the two closed shells on either side, the closed-shell interaction of a Li\(^+\) and an H\(^-\). HF and N\(_2\) are shared-shell interactions with large \(\rho_b\) and negative Laplacians; the triple bond of N\(_2\) carries almost twice the density of the single O-H bond of water. Ethylene shows what the ellipticity is for: the four C-H bonds have \(\varepsilon \approx 0.01\), but the C=C bond has \(\varepsilon = 0.445\), because the \(\pi\) density flattens the bond cross-section into an ellipse whose major axis lies in the \(\pi\) plane. The Poincare-Hopf count for ethylene is again \(6 - 5 + 0 - 0 = 1\). The five analyses together take about seven seconds; the ethylene grid (36 basis functions, a bigger box) accounts for half of that.
Two knobs matter when a search misses a critical point. grid_spacing
(bohr) sets the seed density; halving it to 0.1 costs eight times the
grid work (water goes from one to four seconds) and on water finds the
same two BCPs to four digits. gradient_tol (default 0.2) is the
\(|\nabla\rho|\) ceiling for a voxel to count as a seed; raise it for
diffuse, weak interactions such as hydrogen bonds, where the gradient
minimum is shallow, and lower cp_rho_floor if a BCP with
\(\rho_b < 10^{-4}\) is being discarded as vacuum.
Theory¶
QTAIM starts from a partition of real space rather than of the wavefunction. The gradient vector field \(\nabla\rho(\mathbf r)\) is integrated from every point; almost every trajectory ends at a nucleus, and the set of points whose trajectories end at a given nucleus is that atom’s basin. Basins are separated by surfaces that no gradient path crosses, the zero-flux surfaces,
where \(\mathbf n\) is the surface normal. Bader showed that a region bounded by a zero-flux surface satisfies its own virial theorem, which is what makes the basin a physically meaningful “atom in the molecule” and its integrated charge and energy well defined. vibe-qc does not yet integrate basins; this page is about the critical points that organise them.
A critical point is a point where the gradient vanishes, \(\nabla\rho(\mathbf r_c) = 0\). It is characterised by the eigenvalues \(\lambda_1 \le \lambda_2 \le \lambda_3\) of the Hessian \(H_{ij} = \partial^2\rho/\partial x_i\partial x_j\) at \(\mathbf r_c\), written as a pair (rank, signature), the rank being the number of non-zero eigenvalues and the signature the sum of their signs:
Signature |
Name |
Meaning |
|---|---|---|
\((3,-3)\) |
nuclear critical point, |
local maximum; a nucleus (in rare cases a non-nuclear attractor) |
\((3,-1)\) |
bond critical point, |
maximum in two directions, minimum along the third: the saddle between two bonded atoms |
\((3,+1)\) |
ring critical point, |
minimum in the ring plane, maximum perpendicular to it |
\((3,+3)\) |
cage critical point, |
local minimum, enclosed by rings |
_classify_cp in vibeqc.qtaim does exactly this count of negative
eigenvalues. For a molecule the counts obey the Poincare-Hopf relation
\(n - b + r - c = 1\); for a periodic system the right-hand side is 0.
The eigenvector of the single positive eigenvalue \(\lambda_3\) at a BCP points along the bond. Following \(\nabla\rho\) uphill from the BCP in the \(\pm\mathbf v_3\) directions traces the two halves of the bond path, the line of maximum density linking the two nuclei; a bond path exists between two atoms exactly when a BCP exists between them, which is QTAIM’s operational definition of a chemical bond. The two negative eigenvalues measure how fast the density falls off perpendicular to the bond, and their ratio defines the ellipticity
which is zero for a cylindrically symmetric \(\sigma\) bond and grows with \(\pi\) character (0.45 for the C=C bond above, and it would be zero again for a triple bond, whose two \(\pi\) systems restore the symmetry).
The Laplacian \(\nabla^2\rho = \lambda_1 + \lambda_2 + \lambda_3\) says whether density is locally concentrated (\(\nabla^2\rho < 0\)) or depleted (\(\nabla^2\rho > 0\)) relative to its surroundings. Its physical content comes from the local form of the virial theorem,
with \(G \ge 0\) the kinetic-energy density and \(V \le 0\) the potential-energy density (atomic units). Where \(\nabla^2\rho_b < 0\) the potential energy dominates and electrons are shared between the nuclei: covalent and polar bonds (water, HF, N\(_2\), ethylene above). Where \(\nabla^2\rho_b > 0\) the kinetic energy dominates, the density is squeezed out of the internuclear region into the atomic shells, and the interaction is closed-shell: ionic bonds (LiH), hydrogen bonds and van der Waals contacts. The magnitude of \(\rho_b\) then sorts the closed-shell cases by strength, and for bonds within one family \(\rho_b\) correlates well with bond order and inversely with bond length.
Two limitations frame the interpretation. First, the topology is a property of \(\rho\) alone, so it inherits nothing from the orbital picture but also cannot separate a bond’s \(\sigma\) and \(\pi\) components beyond what the ellipticity encodes. Second, the numbers depend on the quality of \(\rho\): a small basis or a poor functional shifts \(\rho_b\) and \(\nabla^2\rho_b\) by amounts that matter near the covalent/closed-shell boundary (the Laplacian sign of polar bonds such as C-O or Si-O is known to be delicate), so compare values within one level of theory and one basis family.
Resources¶
QTAIM user guide: the data types, algorithm sketch and the location of every piece in the code.
Bond analysis and post-SCF properties: Mulliken, Lowdin and Mayer descriptors that partition the wavefunction rather than real space, for comparison with the density-based picture here.
Volumetric data: export \(\rho\) as a cube file to look at the surfaces the critical points organise.
QVF file format: where
topology.qtaimsits among the section kinds.Feature matrix and Methods: theory and status: the status row and the one-paragraph summary.
Tests:
tests/test_qtaim.pypins the water and ethylene topologies used on this page.
Cost: the grid evaluation dominates and scales with the number of voxels times the number of basis functions; water at 0.15 bohr is about one second, ethylene in 6-31G* about four. Memory is the grid arrays plus the \(3\times3\) Hessian per voxel, evaluated in chunks of \(10^5\) points.
References¶
QTAIM. R. F. W. Bader, “Atoms in molecules,” Acc. Chem. Res. 18, 9 (1985), Bader, R. F. W. (1985). *Atoms in molecules.* Accounts of Chemical Research, **18**(1), 9–15. doi:[10.1021/ar00109a003](https://doi.org/10.1021/ar00109a003); and R. F. W. Bader, Atoms in Molecules: A Quantum Theory (Oxford University Press, 1990), the full account of zero-flux surfaces, critical points and the atomic virial theorem.
Classification of interactions. R. F. W. Bader and H. Essen, “The characterization of atomic interactions,” J. Chem. Phys. 80, 1943 (1984). Shared-shell versus closed-shell interactions from the sign of the Laplacian; the local virial relation used above.
Bond ellipticity and pi character. R. F. W. Bader, T. S. Slee, D. Cremer and E. Kraka, “Description of conjugation and hyperconjugation in terms of electron distributions,” J. Am. Chem. Soc. 105, 5061 (1983).
Reference implementation. T. Lu and F. Chen, “Multiwfn: A multifunctional wavefunction analyzer,” J. Comput. Chem. 33, 580 (2012). The seed-and-refine critical-point search that
vibeqc.qtaimfollows.
Next¶
Post-SCF properties computes the orbital-space bond orders and charges you will want to place beside the BCP descriptors.
Natural orbitals shows the other basis-independent view of a correlated density, its natural occupation numbers.
Orbital visualization exports the density and orbitals to cube files for vibe-view, where the bond critical points can be placed on the isosurfaces.