Effective core potential theory#
Heavy elements make explicit all-electron calculations expensive in two independent ways: there are simply
more electrons to correlate/converge, and — for the heaviest elements — relativistic effects on the core
electrons become significant even though chemistry happens almost entirely in the valence shell. An
effective core potential (ECP), also called a pseudopotential, replaces the chemically inert core
electrons with a fixed, parametrized one-electron operator, cutting both the electron count and the basis
size while (for a well-fitted ECP) implicitly folding in scalar relativistic effects the core electrons
would otherwise need explicit treatment for. This chapter derives the semilocal ECP operator qc-rs
implements, its non-obvious radial-power convention, and the semi-numerical evaluation method that makes
it numerically robust for heavy, tightly contracted primitives — grounded in .design/24qc.scf-ecp.md and
qc-rs’s AGENTS.md qc-ecp rules.
The valence-only Hamiltonian#
With nuclear charge \(Z_A\) and \(N_A^{\text{core}}\) core electrons removed by the ECP, the effective nuclear charge is \(Z_A^{\text{eff}}=Z_A-N_A^{\text{core}}\), and the non-relativistic valence-only electronic Hamiltonian becomes
Every piece of this — the reduced nuclear attraction, the reduced nuclear repulsion, and the new \(\hat U_A^{\text{ECP}}\) operator itself — flows from a single substitution: an all-electron atom is simply the special case \(N_A^{\text{core}}=0\), \(\hat U_A^{\text{ECP}}=0\). The valence electron count follows directly, \(N_e=\sum_AZ_A^{\text{eff}}-Q\) (with \(Q\) the molecular charge), and this is the electron count qc-rs’s spin/multiplicity parity checks use — an ECP calculation’s electron-counting logic is not a special case bolted onto the all-electron logic, it is the all-electron logic applied to \(Z_A^{\text{eff}}\) instead of \(Z_A\).
The semilocal scalar ECP operator#
The chemically important part is \(\hat U_A^{\text{ECP}}\) itself. A semilocal ECP (the scalar/AREP — averaged relativistic effective potential — variety qc-rs implements) splits into a purely radial local channel plus a sum of angular-momentum projectors, each carrying its own radial correction:
where \(Y_{lm}^A\) are real spherical harmonics centered on atom \(A\) and \(L\) is the highest angular momentum
explicitly represented in the ECP (electrons with \(l\ge L\) see only the local channel — the physical
picture being that beyond some angular momentum, the core no longer needs a channel-specific correction).
The projector \(|Y_{lm}^A\rangle\langle Y_{lm}^A|\) means an electron of angular momentum \(l\) relative to
center \(A\) experiences \(U_{A,L}^{\text{loc}}+\Delta U_{A,l}\), while any other electron experiences only the
local channel — this is exactly the physical statement that the pseudopotential’s channel-dependence
represents the different degree to which the Pauli exclusion principle (orthogonality to the removed core
orbitals) affects electrons of different angular momentum. Because published ECP data sometimes tabulates
the local channel directly as ul rather than requiring the consumer to compute \(\Delta U\) from raw
per-channel values, qc-rs does not silently convert to a difference potential on input — it stores exactly
what the database provides and applies the subtraction (if needed) only at evaluation time.
Each radial channel is itself a sum of primitive Gaussian-type radial functions,
The radial-power convention is a documented trap, not a cosmetic detail. The stored integer
\(n_{A,lk}\) — called RADI_POWER in NWChem/PySCF/libecpint file conventions and EcpPrim::n in qc-mol — is
kept and passed through exactly as read from the basis file, with no -2 correction applied in qc-rs’s
own code, even though the formula above visibly has an \(r^{n-2}\) dependence. The -2 is already baked into
the file convention itself (verified against PySCF’s ECPscalar); silently “fixing” it by subtracting 2
again inside qc-rs would double-apply the correction and produce a wrong radial power. This is precisely
the kind of convention mismatch the basis-and-AO chapter warns about more
generally: a stored integer’s meaning is defined by the file format that produced it, not by what a
textbook formula’s exponent happens to look like.
The full AO matrix element is a straightforward one-electron expectation value over every ECP center:
Since \(U_A^{\text{ECP}}\)’s projector structure is a genuinely semilocal operator (it is not simply \(1/r\) or a Gaussian multiplying the identity — it depends on the angular momentum of the electron relative to center \(A\), which is not diagonal in an arbitrary AO basis centered elsewhere), it falls entirely outside libcint’s operator algebra, which is why qc-ecp needs its own kernel rather than reusing the production libcint one/two-electron integral drivers the rest of qc-rs’s Hamiltonian assembly is built on.
Primitive normalization#
The ECP radial primitive’s normalization, verified against PySCF’s ECPscalar, is
matching a unit radial self-overlap for the primitive — structurally the same normalization idea as
ordinary Gaussian AO primitives (derived in the basis-and-AO chapter), just
specialized to the ECP radial-power convention above rather than the ordinary GTO angular-momentum
exponent. Because the full AO matrix reuses qc-grid’s GaussianShell and spherical-value evaluator
(rather than a bespoke ECP-only AO representation), normalization, general contraction, and AO ordering all
match libcint’s own conventions automatically — the ECP matrix is built directly in libcint AO order, with
no separate reordering pass needed downstream.
Evaluation: analytic angular factors, semi-numerical radial quadrature#
The semilocal projector’s angular dependence has a genuine closed-form analytic treatment — real spherical-harmonic angular factors, Gaunt-coefficient-factorized (the same Gaunt/Clebsch-Gordan machinery that couples angular momenta when multiplying spherical harmonics) — but the radial integral over the product of two (possibly widely separated, off-center) Gaussian AO primitives and the ECP’s own radial Gaussian expansion has no similarly clean closed form for the general off-center case. qc-rs’s production path is therefore explicitly semi-numerical: exact angular factors, numerical radial quadrature — the same general strategy PySCF/libecpint use, not a qc-rs-specific compromise.
The production driver, qc-ecp::grid_radial::ecp_matrix_grid, is a direct algorithmic port of PySCF’s
nr_ecp.c (independently reimplemented in Rust, reproducing its numbers): a nested Gauss-Chebyshev
radial grid with per-shell-pair adaptive refinement. Rather than fixing one radial point count for every
shell pair regardless of how sharply peaked the integrand is, the grid starts coarse (31 points, “LEVEL0”)
and doubles for a given shell-pair/channel block only until that block’s radial integral has converged
to a tight tolerance (\(10^{-12}\)), up to a master grid of 2047 points (“LEVEL_MAX”):
Algorithm 6 (Adaptive radial-refinement ECP matrix element)
Input: a shell-pair/channel block’s radial integrand (a product of Gaussian radial functions and the ECP’s own radial expansion), convergence tolerance \(\tau=10^{-12}\). Output: the converged radial contribution to that block of the ECP matrix.
Start at the coarsest refinement level \(L_0\) (31 master-grid points).
Evaluate the radial estimate \(E_L = w_{\text{scale},L}\sum_{i\in\text{subset}_L}w_i\,f(r_i)\), a strided subsample of a single master Gauss-Chebyshev grid on \((0,\infty)\), with a level-dependent rescaling \(w_{\text{scale},L}=2^{L_{\max}-L}\) so every level estimates the same integral.
If \(|E_L-E_{L-1}|<\tau\) (or \(L=L_0\), no prior estimate to compare), accept \(E_L\).
Otherwise double the point count (advance to \(L+1\), a denser strided subset of the same master grid — no new grid is built) and repeat.
This adaptivity is what keeps the method both robust and fast simultaneously: smooth, well-separated shell pairs (chemically the common case — most AO pairs in a large molecule involve at least one function far from a given ECP center) converge at the coarsest level and cost only \(\sim31\) radial points, while only the sharp, tightly-contracted, close-together pairs — the ones that would actually need high radial resolution — pay for the expensive 2047-point refinement. A fixed-grid approach would have to use the expensive grid everywhere to be safe for the hard cases, or risk silent inaccuracy on them; adaptive refinement gets the accuracy of the dense grid only where the physics actually demands it.
Why this exists instead of, and alongside, a closed-form analytic radial path. qc-rs also has an
analytic off-center radial path (matrix::ecp_matrix_analytic, via modified spherical Bessel functions and
the same Gaunt factorization) that is exact in principle but has a documented numerical failure mode for
tight heavy-element primitives: the closed-form expression splits into a prefactor that can itself diverge
(\(e^{k^2/4p}\to\infty\) for large exponents) multiplied by a term that vanishes (\(e^{-\zeta d^2}\to0\) for a
large separation-dependent exponent), and floating-point arithmetic evaluates \(\infty\times0\) as NaN
rather than recovering the finite product the two factors’ combination should give analytically. The
grid-based radial form sidesteps this because its integrand, \(e^{-\zeta(r-d)^2}\cdot M_\lambda(2\zeta dr)\)
(a modified spherical Bessel function argument built directly from the combined, already-finite exponent
rather than from two separately-exploding/vanishing factors), stays numerically bounded throughout — this
is why ecp_matrix_grid rather than the analytic path is qc-rs’s production default for the general case,
even though the analytic path remains available (and useful) for cross-validation.
The angular part carries over unchanged from the analytic path regardless of which radial method is used — it is validated once, independent of radial strategy, and reused: for the “Type 1” (single-center, same-atom AO pair) and “Type 2” (the AO pair’s centers differ from each other, or from the ECP center, or both) angular structures, both a Cartesian-monomial-based path and the spherical-harmonic/Gaunt path are implemented and cross-validated against each other to \(\sim10^{-12}\)–\(10^{-13}\), with the faster one selected as the default for each case (Cartesian for Type 1 at high angular momentum, Gaunt for Type 2 where its per-center caching wins).
Verified example: a heavy element with an ECP basis#
import qc
m = qc.chk.new(atom="I 0 0 0; I 0 0 2.666", ao="lanl2dz", unit="angstrom").scf(ref="r").run()
m.scf.energy, m.scf.converged
# (-22.307234220033017, True)
m.scf.energy_components
# {'core': -56.253929996038075, 'coulomb': 28.188845752921235, 'exchange': -3.968212743262761}
I₂ has \(Z=53\) per atom (106 electrons all-electron), but LANL2DZ’s ECP removes the bulk of each iodine’s
core, leaving only the valence electrons (\(Z^{\text{eff}}\) per atom, far fewer than 53) to appear
explicitly in the SCF. The resulting total electronic energy, \(\sim-22.3\ E_h\), is far smaller in magnitude
than a genuine all-electron iodine dimer’s would be — exactly the expected signature of an ECP calculation:
the deep, chemically inert core energy has been folded into the fixed \(\hat U_A^{\text{ECP}}\) operator and
never appears as an explicit energy contribution at all. The core component above already includes both
the reduced nuclear attraction and the ECP operator’s own contribution — qc-rs does not report a
separate “ECP energy” term, because \(\hat U_A^{\text{ECP}}\) is architecturally just another piece of the
valence-only core Hamiltonian \(T+V_{ne}[Z^{\text{eff}}]+\hat U^{\text{ECP}}\), not a bolt-on correction
computed and reported separately.
Exercise 13
The radial-power convention keeps
RADI_POWERexactly as read from the basis file, with no-2correction applied in qc-rs’s own code, even though the radial formula visibly contains \(r^{n-2}\). Explain in one sentence why “helpfully” subtracting 2 again inside qc-rs, to make the code’s exponent match the textbook formula literally, would actually be a bug rather than a clarification.The adaptive radial grid doubles the point count only for shell-pair/channel blocks that have not yet converged, rather than using the finest (2047-point) grid uniformly. What specific class of shell pairs would make a uniform coarse grid dangerous, and what would make a uniform fine grid wasteful?
The closed-form analytic ECP radial integral can literally evaluate to
NaNfor tight heavy-element primitives, even though the true mathematical value is finite. Identify the two colliding limits (one diverging, one vanishing) responsible, and explain why reorganizing the same physics into the grid-based integrand avoids the problem rather than merely working around it with clamping or a special case.
Solution to Exercise 13
The published NWChem/PySCF/libecpint file convention for
RADI_POWERalready has the-2baked into what the integer means — the stored value, read literally, is exactly the exponent the radial formula needs. Subtracting 2 again inside qc-rs would apply the correction twice, silently producing the wrong radial power for every primitive in every ECP loaded this way — a systematic, silent numerical error that would only show up as a subtly wrong ECP energy, not a crash, making it a genuinely dangerous “fix” rather than a harmless simplification.A uniform coarse grid would be dangerous for sharp, tightly-contracted, close-together shell pairs (large exponents, small separation) — exactly the pairs whose radial integrand is most strongly peaked and hardest to integrate accurately, so a 31-point grid could silently under-resolve them and produce a converged-looking but numerically wrong ECP matrix element. A uniform fine (2047-point) grid would be wasteful because the overwhelming majority of shell pairs in a reasonably sized molecule are smooth and well-separated (at least one function far from the given ECP center) and converge at the coarsest level already — paying the expensive grid cost for every pair regardless would multiply the total ECP evaluation cost by roughly \(2047/31\approx66\times\) for no accuracy benefit on the easy majority.
The analytic radial integral splits into a prefactor \(e^{k^2/4p}\) that diverges for large combined exponents \(p\) and momentum-like parameter \(k\), multiplied by a factor \(e^{-\zeta d^2}\) that vanishes for a large separation-dependent exponent — the true product is finite (a genuine analytic cancellation), but IEEE floating-point arithmetic evaluates \(\infty\times0\) as
NaNrather than recovering that cancellation, because the two factors are computed separately before being multiplied. The grid-based integrand instead builds the combined exponent argument directly, as a single numerically bounded expression (\(e^{-\zeta(r-d)^2}\) inside the quadrature, never split into a diverging and a vanishing piece computed independently) — this is not a workaround bolted onto the same unstable formula, it is a genuinely different (numerically stable) way of organizing the identical underlying physics, which is why it needs no clamping or special-casing to stay finite.
The next chapter, on implicit solvation, covers a second Hamiltonian modification qc-rs supports — but one that, unlike the ECP’s fixed operator, is genuinely density-dependent and must therefore participate in the SCF self-consistency loop and, per the linear-response chapter, in the response kernel as well.