Post-SCF properties: charges, bonds, dipole¶
Once an SCF is converged, vibe-qc gives you the standard chemical interpretation tools: atomic charges (Mulliken and Löwdin), Mayer bond orders, and the molecular dipole moment. This tutorial walks through them on water, then a short comparison on H-bonded systems.
These properties are emitted automatically by run_job when it has
enough information; you can also call the helpers directly to get the
raw arrays for plotting or downstream analysis.
A single call¶
from vibeqc import Atom, Molecule, run_job
mol = Molecule([
Atom(8, [ 0.0, 0.00, 0.00]),
Atom(1, [ 0.0, 1.43, -0.98]),
Atom(1, [ 0.0, -1.43, -0.98]),
])
run_job(mol, basis="6-31g*", method="rhf", output="water")
(Positions are in bohr. Molecule.from_xyz("examples/h2o.xyz")
works too if you have an XYZ file.)
Open water.out — after the SCF trace and orbital-energy table
you’ll find (HF/6-31G*, experimental geometry):
Atomic charges
----------------------------------------------------
# elem Mulliken Löwdin
----------------------------------------------------
1 O -0.910152 -0.646928
2 H 0.455076 0.323464
3 H 0.455076 0.323464
----------------------------------------------------
sum -0.000000
Bond orders (Mayer)
----------------------------------------
atoms bond order
----------------------------------------
O1 — H2 0.7628
O1 — H3 0.7628
Dipole moment (origin: centre of mass)
----------------------------------------------------
component a.u. (e·bohr) Debye
----------------------------------------------------
x -0.00000000 -0.0000
y -0.00000000 -0.0000
z -0.81069504 -2.0606
|μ| 0.81069504 2.0606
What the numbers mean¶
Mulliken charges partition the electron density according to which basis function it was expanded in. Simple, interpretable, but basis-set dependent — a polarization function added to H shifts charge onto it.
Löwdin charges partition in the orthonormalised AO basis (S^{-1/2} transform). Less basis-set sensitive, generally preferred for trend analysis across systems.
Mayer bond orders generalise the Wiberg index to non-orthogonal bases.
B_ij ≈ 1for single bonds,≈ 2for double,≈ 3for triple. Values under ~0.1 are hidden by default.Dipole moment is computed with the origin at the center of mass, which makes it origin-invariant for neutral molecules. For ions pass an explicit
origin=todipole_moment()if you want a different reference.
Pulling the arrays yourself¶
When you need raw data for plotting or fitting, call the helpers
directly — they take the same result object you get back from
run_rhf / run_job.
import numpy as np
from vibeqc import (
Atom, BasisSet, Molecule, run_rhf,
mulliken_charges, loewdin_charges,
mayer_bond_orders, dipole_moment,
)
from vibeqc.properties import prominent_bonds
mol = Molecule([
Atom(8, [ 0.0, 0.00, 0.00]),
Atom(1, [ 0.0, 1.43, -0.98]),
Atom(1, [ 0.0, -1.43, -0.98]),
])
basis = BasisSet(mol, "6-31g*")
result = run_rhf(mol, basis)
q_mull = mulliken_charges(result, basis, mol) # (n_atoms,) np.ndarray
q_lowd = loewdin_charges(result, basis, mol) # same shape
bonds = mayer_bond_orders(result, basis, mol) # (n_atoms, n_atoms)
print(prominent_bonds(bonds, mol, threshold=0.10)) # [(i, j, B_ij), ...]
mu = dipole_moment(result, basis, mol)
print(f"|μ| = {mu.total_debye:.3f} D")
Basis-set dependence: a quick sweep¶
Mulliken charges drift significantly between basis sets — run the same water molecule at three levels and print the oxygen charge:
mol = Molecule([
Atom(8, [ 0.0, 0.00, 0.00]),
Atom(1, [ 0.0, 1.43, -0.98]),
Atom(1, [ 0.0, -1.43, -0.98]),
])
for basis_name in ("sto-3g", "6-31g*", "def2-tzvp"):
basis = BasisSet(mol, basis_name)
r = run_rhf(mol, basis)
q = mulliken_charges(r, basis, mol)
print(f"{basis_name:12s} q_O = {q[0]:+.3f} μ (D) = "
f"{dipole_moment(r, basis, mol).total_debye:.3f}")
Output (HF, experimental geometry):
sto-3g q_O = -0.418 μ (D) = 1.725
6-31g* q_O = -0.910 μ (D) = 2.061
def2-tzvp q_O = -0.685 μ (D) = 2.008
Two lessons:
Mulliken is sensitive to basis.
q_Oswings half an electron across these sets — never compare absolute Mulliken charges between different bases, different codes, or different molecules unless you hold the basis fixed. Löwdin charges are more stable but not bulletproof either.Dipole moment is well-behaved.
6-31g*already overshoots slightly; TZVP pulls it back toward 2.0 D. The experimental gas-phase water dipole is 1.85 D. The residual over-estimate comes from missing electron correlation — rerun the sweep withmethod="rks", functional="PBE"and the dipole drops further.
Following the H-bond in a water dimer¶
Run the dimer example (examples/molecular/input-h2o-dimer-opt.py) and load
output-h2o-dimer-opt.molden in Avogadro. The .out file already
lists the Mulliken charges at the optimized geometry — note the
donor-hydrogen charge is noticeably larger than the free hydrogens,
and the oxygen of the donor water is slightly more negative than the
acceptor. That’s the H-bond polarization, visible in the numbers.
Same in Python:
from vibeqc import Molecule, BasisSet, run_rhf, mulliken_charges
mol = Molecule.from_xyz("h2o_dimer_optimised.xyz") # after optimisation
basis = BasisSet(mol, "6-31g*")
r = run_rhf(mol, basis)
q = mulliken_charges(r, basis, mol)
print("Charges on:", q)
Theory¶
The density matrix¶
Every post-SCF property in this tutorial derives from the one-particle density matrix \(\mathbf{P}\) that came out of the HF / DFT SCF:
for a closed-shell system (factor 2 from the spin sum). The AO-basis density is the single object that encodes all observables: every expectation value \(\langle \hat{O} \rangle = \operatorname{Tr}(\mathbf{P} \mathbf{O})\) where \(\mathbf{O}\) is the AO-basis matrix of the operator.
Mulliken population analysis¶
Mulliken starts from the observation that
is the total electron count. Each term in the double sum is assigned half to each basis function:
It’s the simplest partitioning imaginable and has the problem you’d expect: it depends on which basis function the electron is “in,” and two different bases that describe the same physics will give different \(q_A\). Don’t compare Mulliken charges across different basis sets, different codes, or different method families.
Löwdin population analysis¶
Löwdin orthogonalises the AO basis first with \(\mathbf{S}^{-1/2}\) — the symmetric Löwdin transformation — then partitions the trace in the orthogonal basis:
Orthogonalizing the basis first smooths out a lot of Mulliken’s worst basis-set sensitivity, though it’s still not perfectly stable. Löwdin is generally preferred for within-system trend analysis and for comparing similar molecules.
Mayer bond order¶
Mayer’s bond order generalises the one-basis-function-per-atom Wiberg index to general contracted bases. For closed-shell, the bond order between atoms \(A\) and \(B\) is
For idealised bonding patterns this reproduces chemical intuition: \(B \approx 1\) for single bonds, \(\approx 2\) for double, \(\approx 3\) for triple. Unlike raw orbital analyses, Mayer’s index is rotationally invariant and converges to a finite limit as the basis improves — a genuine measure of “number of shared electron pairs” rather than an artefact of how the basis partitions charge.
Dipole moment¶
The electric dipole moment is a first-order response of the energy to a uniform electric field:
The electronic contribution comes from a trace against the dipole- integral matrices \(\mathbf{r}_\alpha\) that libint computes analytically; the nuclear contribution is a classical sum over point charges at atomic positions. The result is origin-independent for neutral molecules (the two contributions’ origin dependences cancel) — vibe-qc reports at the center of mass as a convention.
Resources¶
Negligible — adds <100 MB and <0.1 s on top of whatever SCF you ran. Mulliken / Löwdin / Mayer / dipole are all \(\mathcal{O}(N_\text{bf}^3)\) matrix products against the converged density and ride free on top of any SCF you’ve already paid for.
References¶
Mulliken population analysis. R. S. Mulliken, “Electronic population analysis on LCAO-MO molecular wavefunctions. I,” J. Chem. Phys. 23, 1833 (1955); parts II-IV in consecutive issues.
Löwdin orthogonalisation. P.-O. Löwdin, “On the non- orthogonality problem connected with the use of atomic wave functions in the theory of molecules and crystals,” J. Chem. Phys. 18, 365 (1950).
Mayer bond order. I. Mayer, “Charge, bond order and valence in the ab initio SCF theory,” Chem. Phys. Lett. 97, 270 (1983).
Dipole moment in quantum chemistry. Textbook treatment in any of Szabo-Ostlund (chapter 3), Helgaker-Jørgensen-Olsen (chapter 4), or Jensen’s Introduction to Computational Chemistry. The dipole operator’s matrix elements are standard one-electron Gaussian integrals — evaluated by libint in vibe-qc.
Next¶
Dipole moment tracks the molecular geometry; see geometry optimization for relaxations.
For vibrational contributions (IR intensities = dipole derivatives) see vibrational frequencies once the Hessian infrastructure lands.