Handover to the documentation chat — libecpint / ECP support¶
Source chat: basissetdev. Branch: basissetdev @ commit add8163.
Date: 2026-05-09.
Status: Phase 14a-d shipped end-to-end. Phase 14e/f deferred and
captured below as known gaps.
This document tells the documentation chat what to write up for end-users of vibe-qc’s effective-core-potential (ECP) support. The underlying machinery is now functional; what’s missing is exposure in the user-facing docs (tutorials, API reference, CHANGELOG entry).
Headline for the documentation chat¶
vibe-qc 0.7.x ships with libecpint 1.0.7 vendored. ECP integrals, reduced-Z nuclear-attraction, and valence-electron-count accounting are wired through every molecular SCF driver (RHF, UHF, RKS, UKS). The Karlsruhe def2-ECP family, Stuttgart-Köln MDF (ecp10mdf, ecp28mdf, ecp46mdf, ecp60mdf, ecp78mdf), and Hay-Wadt LANL2DZ ECPs are bundled and ready to use. The user supplies an explicit
ecp_centerslist per heavy atom plus theecp_libraryname; auto-population from inline-ECP basis files is on the v0.8 roadmap.
The banner now lists libecpint:
linked: libint 2.13.1 · libxc 7.0.0 · spglib 2.7.0 · libecpint 1.0.7
Public API surface (already exported)¶
import vibeqc as vq
# Version + library probe
vq.libecpint_version() # "libecpint 1.0.7 (vendored, MAX_L=5)"
vq.library_versions()["libecpint"] # same
# Per-atom ECP descriptor (manual path)
ec = vq.ECPCenter()
ec.Z = 78 # atomic number of the centre
ec.xyz = [0.0, 0.0, 0.0] # Cartesian position (bohr)
# AO matrix V_ECP_{μν} = ⟨χ_μ | V_ECP | χ_ν⟩ (spherical basis)
V_ecp = vq.compute_ecp_matrix(
basis, # vibeqc.BasisSet
ecp_centers=[ec],
library_name="lanl2dz",
) # → numpy.ndarray (n_bf, n_bf)
# All four SCF entry points accept ecp_centers + ecp_library:
opts = vq.UHFOptions()
opts.ecp_centers = [ec]
opts.ecp_library = "lanl2dz"
result = vq.run_uhf(mol, basis, opts)
Auto-population helper (Phase 14e — recommended path)¶
The Phase-14e helper short-circuits the ECPCenter boilerplate
above. Given a molecule and a basis name, it parses the bundled
.ecp sidecar, picks the right libecpint XML library, and emits
(ecp_centers, library_name) ready to drop onto any SCF Options
class:
opts = vq.UHFOptions()
opts.ecp_centers, opts.ecp_library = vq.auto_ecp_centers(mol, "lanl2dz")
result = vq.run_uhf(mol, basis, opts)
Same numbers as the manual path: Pt UHF/LANL2DZ at −118.227 Ha,
Si at −3.475 Ha. Recommend this as the primary user-facing
recipe in tutorials; the manual ECPCenter form stays
documented but moves to the “advanced / custom XML library” tier.
The helper also exposes a few lower-level pieces for inspection / custom workflows:
vq.sidecar_path_for(basis_name) # Path or None
vq.parse_sidecar_path(path) # → list[EcpHeader(symbol, Z, lmax, ncore)]
vq.library_for(basis_name, ncore) # → "lanl2dz" / "ecp28mdf" / ...
# None for non-standard customs
When the helper can’t auto-resolve, it raises a clear error with the Phase-14g pointer:
ValueErrorif the molecule’s atoms span more than one libecpint XML library (e.g. Rb + Cs in dhf-tzvp — Rb wants ecp28mdf, Cs wants ecp46mdf, current SCF API takes one library string);NotImplementedErrorfor non-standard ncore values (vDZP’s per-element customs — needs Phase 14g’sset_ecp_basis(...)direct-primitive feed).
The XML library is bundled at python/vibeqc/ecp_library/xml/:
ecp10mdf.xml ecp28mdf.xml ecp46mdf.xml ecp60mdf.xml ecp78mdf.xml
lanl2dz.xml
$VIBEQC_ECP_SHARE_DIR overrides the bundled path if needed.
Worked example (drop into a tutorial)¶
Pt atom UHF with LANL2DZ — Phase-14e auto-population, the recommended recipe:
import vibeqc as vq
# Pt 5d⁹6s¹ ³D ground state, multiplicity = 3
mol = vq.Molecule([vq.Atom(78, [0.0, 0.0, 0.0])], multiplicity=3)
basis = vq.BasisSet(mol, "lanl2dz") # 22 basis functions (valence only)
opts = vq.UHFOptions()
opts.ecp_centers, opts.ecp_library = vq.auto_ecp_centers(mol, "lanl2dz")
opts.max_iter = 200 # heavy-atom UHF needs ~125 iters
result = vq.run_uhf(mol, basis, opts)
print(f"Pt UHF/LANL2DZ: {result.energy:.4f} Ha "
f"({result.n_iter} iters, converged={result.converged})")
# → Pt UHF/LANL2DZ: -118.2268 Ha (125 iters, converged=True)
The pre-Phase-14e manual recipe (kept for reference / advanced users):
opts.ecp_centers = [vq.ECPCenter()]
opts.ecp_centers[0].Z = 78
opts.ecp_centers[0].xyz = [0.0, 0.0, 0.0]
opts.ecp_library = "lanl2dz"
Si atom UHF with LANL2DZ (smaller, faster):
mol = vq.Molecule([vq.Atom(14, [0, 0, 0])], multiplicity=3)
basis = vq.BasisSet(mol, "lanl2dz") # 8 basis functions
opts = vq.UHFOptions()
opts.ecp_centers = [vq.ECPCenter()]
opts.ecp_centers[0].Z = 14
opts.ecp_centers[0].xyz = [0, 0, 0]
opts.ecp_library = "lanl2dz"
result = vq.run_uhf(mol, basis, opts)
# → Si UHF/LANL2DZ: -3.4748 Ha, 67 iters
What changed under the hood (for the changelog entry)¶
Phase 14a-c (already shipped before this work):
libecpint vendored under
third_party/libecpint/install/with pugixml + libcerf C++ deps also vendored. CMake linksECPINT::ecpint.compute_ecp_matrixC++ + Python API for V_ECP integrals.compute_ecp_one_electronreturnsECPHcore { V (with Z_eff nuclear attraction), V_ecp, E_nuc, total_ncore }. All four SCF drivers consume this.
Phase 14d shipped in commit add8163 (2026-05-09):
library_versions()now lists libecpint (user requirement “list it in our header”). Banner one-liner picks it up.ECP-aware basis loader. BSE-format
.g94files for vDZP, LANL2DZ, dhf-*, etc. bundle orbital basis blocks AND ECP definition blocks; libint2’s parser chokes on<Sym>-ECPheaders. New tooling splits each.g94at install time:scripts/basisset_dev/split_ecp_g94.py— state-machine splitter with lookahead, handles BSE conventions including all-caps Pople-era element symbols (NA,MG,SI).scripts/setup_basis_library.shinvokes it automatically.13 ECP-bearing files split: 6 dhf-* + 6 lanl* + vdzp.
Result: every BSE
.g94in the bundle now loads cleanly viavq.BasisSet(mol, name).
total_ncorevalence-electron accounting. SCF drivers now computen_elec_eff = mol.n_electrons() − ecp_h.total_ncoreand use it fornocc/n_α/n_β. Without this, the SCF tried to put the all-electron count into a basis sized for valence only and hit “canonical orthogonalization dropped too many basis directions”. Backward compat preserved whenecp_centersis empty (total_ncore = 0).
Known gaps the documentation chat should mention¶
Gap 1 — User must supply ecp_centers manually (Phase 14e)¶
The ECPCenter list is currently a user-supplied per-call argument.
The BasisSet knows from the .ecp sidecar which atoms have ECPs
and what ncore was assigned, but the SCF entries don’t read that
info — so the user has to mirror the basis’s ECP map in
opts.ecp_centers. Until Phase 14e lands, document this as the
expected workflow. Sample boilerplate:
# Boilerplate that the docs chat could turn into a helper:
def ecp_centers_for(mol, ecp_library_atoms):
"""Build ecp_centers covering every atom in `mol` whose Z is in
`ecp_library_atoms` (e.g. {78, 79, 53} for an Au-Pt-I cluster)."""
return [
_make_center(a.Z, a.xyz)
for a in mol.atoms()
if a.Z in ecp_library_atoms
]
Gap 2 — Choosing the right ecp_library¶
The bundled XML libraries cover specific element / core-size pairings:
Library |
Replaces |
Element range |
|---|---|---|
ecp10mdf |
10 e⁻ |
K – Cu (Stuttgart-Köln 10-core MDF) |
ecp28mdf |
28 e⁻ |
Rb – Ag (Stuttgart-Köln 28-core MDF) |
ecp46mdf |
46 e⁻ |
Cs – La, lanthanides |
ecp60mdf |
60 e⁻ |
Hf – Au |
ecp78mdf |
78 e⁻ |
Hg – Rn |
lanl2dz |
varies |
Hay-Wadt LANL2DZ family (per-element) |
For def2-TZVP / def2-TZVPP / def2-QZVP (Karlsruhe), use
ecp_library matching the element’s row:
Rb–Cd →
"ecp28mdf"Cs–Hg →
"ecp46mdf"(Cs, Ba, La),"ecp60mdf"(Hf–Hg)Tl–Rn →
"ecp78mdf"
For LANL2DZ / LANL08 orbital bases, ecp_library="lanl2dz"
covers every element the orbital basis covers.
For vDZP (Grimme 2023): the inline ECPs are non-standard
per-element core sizes (e.g. B with ncore=3). Until Phase 14e
arrives, vDZP cannot be used end-to-end through vq.run_uhf —
the orbital basis loads, but no XML library matches the inline
definitions. Users should fall back to def2-TZVP+ECP for now.
For dhf- and x2c-**: the basis sets need a relativistic Hamiltonian (X2C / DKH2 / ZORA) to be physically meaningful. The basis files load, but vibe-qc has no relativistic Hamiltonian yet. Document as “files ship, infrastructure pending”.
Gap 3 — CRYSTAL-format ECPs (5th-period pob)¶
vibeqc.basis_crystal.parse_crystal_atom_basis raises
NotImplementedError when a CRYSTAL-format file uses the
200+Z ECP convention (i.e. for Rb–I or Cs–Po pob basis sets).
Pure-Python pob fifth-period support requires extending the
parser to emit ECPCenter lists alongside the orbital basis.
This is the natural next item for basissetdev to deliver
post-handover.
Test artefacts¶
Two test files in tests/basisset_dev/ cover the work end-to-end:
test_basis_library_load.py— parametrised across every.g94inbasis_library/basis/(366 cases on the current bundle). Two cases per file:BasisSet(mol, name)succeeds, and a single-atom RHF/UHF on the lightest covered element produces a finite negative energy. ECP-bearing files now load cleanly (Phase 14d); the SCF half xfails them with reason “needs Phase 14e”.test_ecp_scf.py— NEW. 4 focused integration cases for the manual-ecp_centerspath: Si and Pt UHF on LANL2DZ, plus regression guards for non-ECP backward compat andncoresubtraction propagation.
Final tally on this work: 360 passed, 1 skipped (Phase-14e placeholder), 18 xfailed in 484 s. The 18 xfails partition into:
13 ECP-bearing × test_single_atom_scf (Phase 14e)
3 high-l (cc-pV7Z and friends) — needs libint rebuild with higher
LIBINT_MAX_AM2 SAP atomic-density helpers (not orbital bases by design)
Test execution policy¶
Run the full parametrised load test on planetx via vq, not on
the laptop. The 484-s run with 130+ atomic SCFs — particularly
the heavy-atom + LANL2DZ cases — was a contributor to the laptop
crash on 2026-05-09 overnight. Standard recipe per
reference_vq_queue.md:
# From the laptop, in the basissetdev worktree
JOBID=$(vq submit -d ./tests/basisset_dev -- \
python -m pytest test_basis_library_load.py --noconftest -q)
# Poll, then
vq fetch "$JOBID" -o ./out
A laptop-tier subset (architecture tests + small-basis SCF only)
should be a future deliverable so pytest tests/basisset_dev/ is
safe to run locally without crashing the host.
What this handover does NOT cover¶
Tutorial-style worked examples — the docs chat should write these. Suggested topics:
“Your first ECP calculation” (Pt or Au atom UHF/LANL2DZ).
“Choosing an ECP library for your element” (table from Gap 2).
“When to use ECPs vs. all-electron” (heavy-element cost argument; Karlsruhe def2 family is the default workflow).
“Why my SCF didn’t converge” troubleshooting (ECP heavy-atom UHF often needs
max_iter ≥ 200; small-gap systems benefit fromlevel_shiftordamping).
API-reference rendering —
compute_ecp_matrix,compute_ecp_one_electron,ECPCenter,ECPHcore,*Options.ecp_centers,*Options.ecp_library. Ensure Sphinx / mkdocs picks them up. Docstrings exist on the Python side; the C++ headers carry the canonical descriptions.CHANGELOG entry for the next minor release. Suggested bullet:
libecpint integration completed — basis-set loader handles BSE-format
.g94files with ECP blocks, SCF drivers compute valence-only electron counts, andlibrary_versions()/ banner credit libecpint. Karlsruhe def2-ECP, Stuttgart-Köln MDF, and Hay-Wadt LANL2DZ ECP libraries bundled out of the box. Auto-population ofecp_centersfrom inline-ECP basis files lands in v0.8 (Phase 14e).
Cross-references¶
REQUIREMENTS-PERIODIC.md — the periodic chat needs ECPs for 5th-period pob (R6 / fifth-period blocker).
ROADMAP_BASIS_LIBRARY.md — every 📂 (data-only) entry in the dashboard for the LANL family, vDZP, and the dhf-* family is partially unblocked by this work.
REVIEW_BASIS_SETS_2026-05-08.md — the literature review that motivated the libecpint priority.
Memory files:
reference_vq_queue.md(where heavy ECP testing should run),feedback_aux_basis_routing.md(AutoAux ↔ DF chat boundary, separate from libecpint).
Open hand-off question for the documentation chat¶
The ECPCenter API requires the user to know which ecp_library
name maps to which element. Most user-facing chat-codebases
encapsulate this in a helper function (ORCA’s keyword: ECP{def2}
expands automatically). Vibe-qc’s API is intentionally explicit
right now — should the docs chat:
Document the explicit mapping (current state) and let the user opt in to a helper, OR
Wait for Phase 14e and document the auto-population then?
Recommendation from basissetdev: option (1). The explicit
mapping is short (the table above), it’s already correct, and it
gives users a clear picture of what’s happening under the hood.
Phase 14e will be additive.