Effective core potentials (ECPs)¶
vibe-qc ships libecpint 1.0.7 vendored as a runtime dependency. ECP integrals, reduced-Z nuclear-attraction, and valence-electron-count accounting are wired through every molecular SCF driver (RHF, UHF, RKS, UKS). Heavy-element chemistry, including the d-block, the lanthanides, and the actinides, is reachable without constructing the all-electron basis.
The banner lists libecpint as a linked dependency:
linked: libint 2.13.1 · libxc 7.0.0 · spglib 2.7.0 · libecpint 1.0.7
What ships¶
Six ECP libraries bundled inside libecpint, MIT-licensed (see
docs/license.md):
Library |
Family |
Elements in the bundled XML |
Citation family |
|---|---|---|---|
|
Stuttgart-Köln MDF, 10-electron core |
K-Kr |
Andrae, Häußermann, Dolg, Stoll, Preuß, Theor. Chim. Acta 77, 123 (1990) and follow-ups |
|
Stuttgart-Köln MDF, 28-electron core |
Rb-Xe |
(ditto, per-element refs) |
|
Stuttgart-Köln MDF, 46-electron core |
Cs, Ba |
(ditto) |
|
Stuttgart-Köln MDF, 60-electron core |
Hf-Rn, Ac-U |
(ditto) |
|
Stuttgart-Köln MDF, 78-electron core |
Fr, Ra |
(ditto) |
|
Hay-Wadt LANL |
Na-La, Hf-Bi, U-Pu |
Hay, Wadt, J. Chem. Phys. 82, 270 + 299 + 284 (1985) |
These are the elements actually present in each bundled file, and they are
narrower than the family names imply: ecp46mdf carries two elements, not
every 46-core one. No bundled XML covers Ce-Lu; the lanthanides are
reached through a basis’ own .ecp sidecar (pob-TZVP-rev2 carries La-Lu),
never through ecp_library. The same numbers appear again under
“The bundled XML libraries, for explicit pairing only” below.
The accompanying valence-only orbital basis sets (the lanl2dz family,
the dhf-* sets, vDZP, the def2-m* composite bases, pob-TZVP-rev2 and the
16-member cc-pVnZ-PP family) are bundled together with their own .ecp
sidecars; see the next section.
Automatic attachment, decided per element¶
Every bundled basis that replaces core electrons ships its ECP as a
.ecp sidecar next to its .g94 file: the def2 family from Rb to Rn
(def2-SV(P) through def2-QZVPPD, with the def2-ECP), the LANL and dhf
families, vDZP, the 3c composite bases def2-mSVP / def2-mTZVP /
def2-mTZVPP, pob-TZVP-rev2, and the Peterson/Figgen PP sets
({,aug-}cc-p{V,wCV}{D,T,Q,5}Z-PP, Cu-Kr / Y-Xe / Hf-Rn, carrying the
Stuttgart-Köln MCDHF potentials with 10-, 28- and 60-electron cores). For molecular run_job calls and the direct molecular SCF
wrappers, the sidecar’s own primitives are attached inline for every
atom that has a block there, and nothing is attached for an atom that has
none. Thus run_job(mol, basis="lanl2dz", method="rhf") runs the
valence-electron ECP Hamiltonian on sulfur, while water in LANL2DZ is the
all-electron D95V calculation it always was. A basis that is valence-only
somewhere but ships no ECP there (the lanthanides in the diffuse def2-*D
sets) is refused with a message naming the elements rather than run
all-electron in a valence basis. The cc-pVnZ-PP sets were in that list
until their orbital files shipped (2026-09-08); they now resolve from
their own sidecars.
The decision is vibeqc.basis_registry.ecp_requirements(basis, atomic_numbers):
the sidecar first, then the family rules in
python/vibeqc/basis_library/registry.toml, and nothing else. Basis
names no longer decide anything, so x2c-TZVPall (all-electron on every
element) runs, and pob-TZVP-rev2 on silver attaches its ECP although its
name carries no marker.
Two explicit recipes remain when you want to override the selection:
Inline primitives (
ecp_primitive_blocks,ecp_primitive_centers,ecp_effective_charges,ecp_total_ncore) built from any sidecar withvibeqc.ecp_metadata.inline_ecp_data_for(mol, basis_name), or from a CRYSTAL record withvibeqc.basis_crystal.crystal_ecp_to_libecpint_arrays. This is what the automatic attachment sets.Manual
ECPCenterrecipe (ecp_centers+ecp_library): oneECPCenterper heavy atom and the name of a bundled libecpint XML library. Use it to pair an all-electron orbital basis with a Stuttgart or LANL potential deliberately, as the Zn²⁺/6-31G example below does.
vq.auto_ecp_centers(...) still exists as the legacy XML-library helper
behind recipe 2. It pairs a library by core count, so it cannot represent
vDZP’s custom cores, refuses a molecule spanning two core sizes, and for
vDZP’s silver picks a different potential from the one the sidecar
carries (85 mHa on AgH). New scripts should rely on the automatic
attachment instead.
Manual recipe (always available)¶
import vibeqc as vq
# Pt atom at the origin.
mol = vq.Molecule([vq.Atom(78, [0.0, 0.0, 0.0])])
basis = vq.BasisSet(mol, "lanl2dz") # valence-only orbital basis
# One ECPCenter per heavy atom.
ec = vq.ECPCenter()
ec.Z = 78 # atomic number of the centre
ec.xyz = [0.0, 0.0, 0.0] # bohr (Cartesian)
# Multiplicity lives on the Molecule; rebuild it with the right
# spin state if you're not already starting from one.
mol = vq.Molecule([vq.Atom(78, [0.0, 0.0, 0.0])], multiplicity=3)
basis = vq.BasisSet(mol, "lanl2dz")
# Wire the ECP onto the SCF options.
opts = vq.UHFOptions()
opts.ecp_centers = [ec]
opts.ecp_library = "lanl2dz" # which ECP XML library
result = vq.run_uhf(mol, basis, opts)
print(result.energy) # -118.227 Ha (22 BFs, 125 SCF iters)
The same ecp_centers + ecp_library flags are accepted by
run_rhf, run_rks, and run_uks. Multiple heavy atoms →
build one ECPCenter per atom and pass them all in:
# Pt-Pt dimer, 10-bohr separation. Singlet (paired) lives on
# the Molecule, not on the SCF options.
mol = vq.Molecule(
[
vq.Atom(78, [0.0, 0.0, -5.0]),
vq.Atom(78, [0.0, 0.0, +5.0]),
],
multiplicity=1,
)
basis = vq.BasisSet(mol, "lanl2dz")
opts = vq.UHFOptions()
opts.ecp_centers = [
vq.ECPCenter(Z=78, xyz=[0.0, 0.0, -5.0]),
vq.ECPCenter(Z=78, xyz=[0.0, 0.0, +5.0]),
]
opts.ecp_library = "lanl2dz"
Electron count and charge convention¶
Molecule.charge is always the physical ionic charge, and
Molecule.n_electrons() is always the full physical electron
count, including the core electrons the ECP will replace. The
SCF subtracts the replaced cores itself: the per-element core
sizes come from the ECP library (ECPHcore.total_ncore on the
C++ side), and only the remaining valence electrons fill
orbitals in the valence-only basis.
Worked example, [ZnH]+ with ecp10mdf:
mol = vq.Molecule(
[vq.Atom(30, [0.0, 0.0, 0.0]), vq.Atom(1, [0.0, 0.0, 3.0])],
charge=1, # the physical ionic charge, nothing more
multiplicity=1,
)
# Z_total = 31, charge +1 -> n_electrons() = 30 (physical)
# ecp10mdf replaces 10 core electrons on Zn (3s 3p 3d 4s stay
# in valence) -> 20 valence electrons
# closed shell -> 10 doubly occupied orbitals
Things that follow from the convention:
Never fold the core count into
charge. An input that passescharge = n_core + ionic_charge(a convention some codes and some pre-v0.12 vibe-qc scripts used) now describes a different, over-stripped ion: the SCF would remove the core a second time. The drivers raise if the cores outnumber the electrons, with a reminder thatn_electrons()must be the full physical count.Parity and multiplicity arithmetic work on either count. Every shipped core (
ecpNNmdf,lanl2dz) replaces an even number of electrons, so the physical and valence counts have the same parity;run_rhf’s even-electron check and the UHF/UKSn_alpha/n_betasplit behave as expected.Gradients require matching ECP provenance. When
GradientOptionsmirrors the SCF options’ XML-library or inline-primitive ECP input (see § Gradients below), the gradient drivers rebuild the energy-weighted density from exactly the orbitals the SCF occupied. Before the current wrapper guards, leaving these fields unset caused the bare-Z bug flagged by the 2026-05-18 audit. Public gradient wrappers now require a verified high-level SCF result and refuse a missing, different, or mixed ECP route before native derivative work.vq.ecp_effective_charges(mol, ecp_centers, library_name)returns the per-atomZ_eff = Z - n_corevector if you need the accounting explicitly (atoms without an ECP keep bare Z).Electrostatic properties use the same charges. The valence density integrates to the valence count, so the nuclear term of the dipole moment,
sum_A Z_A (R_A - O), and the reference charge in the Mulliken, Löwdin and Hirshfeld populations,q_A = Z_A - pop_A, must useZ_effon ECP atoms (Dolg and Cao, Chem. Rev. 112, 403 (2012), Sec. 5: the cores are point chargesQ = Z - n_corein every term of the valence-only Hamiltonian). Until GitLab #642 (2026-09-03) these used bareZ: H2S/LANL2DZ RHF reported 0.148 D with the wrong sign, the dipole moved by exactly-n_core = -10au per bohr of origin shift, and the Mulliken charges summed to +10 instead of 0; the corrected dipole is 1.99496 D and origin-free, and the FD Hessian’s dipole derivatives (hence IR intensities) satisfy the translational sum rulesum_A d mu / d R_A = 0.run_job, the FD Hessian, the ASE calculator and the population writers pass the charges automatically from the options the SCF ran with. A direct caller ofvq.properties.dipole_moment/mulliken_charges/loewdin_charges/hirshfeld_chargeson an ECP system passesnuclear_charges=vibeqc.ecp_metadata.effective_nuclear_charges(mol, opts); the default is bareZ, correct only all-electron. Hirshfeld partitions with all-electron free-atom weights even on ECP systems, so its per-atom split there is approximate although the charges now sum to the molecular charge.
Phase-14e auto-helper (vq.auto_ecp_centers)¶
The Phase 14e helper short-circuits the boilerplate above.
Given a molecule and a basis name, it parses the bundled
.ecp sidecar, picks the matching libecpint XML library, and
returns ready-to-drop (ecp_centers, library_name):
import vibeqc as vq
mol = vq.Molecule(
[
vq.Atom(78, [0.0, 0.0, -5.0]),
vq.Atom(78, [0.0, 0.0, +5.0]),
],
multiplicity=1, # singlet (paired)
)
basis = vq.BasisSet(mol, "lanl2dz")
opts = vq.UHFOptions()
opts.ecp_centers, opts.ecp_library = vq.auto_ecp_centers(mol, "lanl2dz")
result = vq.run_uhf(mol, basis, opts)
# Same -118.227 Ha as the manual recipe, but no per-atom ECPCenter
# wiring needed. Tutorial / sample-script-friendly.
The helper is a legacy surface since 2026-09: tutorials and example scripts need no ECP wiring at all, because the SCF wrappers attach the sidecar themselves, and the helper cannot represent vDZP’s custom cores or a molecule spanning two core sizes. Where it applies, its numerics match the manual recipe: Pt/lanl2dz UHF = −118.227 Ha (singlet) at 22 BFs in 125 iters; Si/lanl2dz RHF = −3.475 Ha.
Important
Status (shipped on main). vq.auto_ecp_centers is exported
from vibeqc.__init__ and works out of the box on a pip install
of vibe-qc. The basissetdev paper-writing branch still tracks
basis-set-side work (the BSE-fetched basis sets, ongoing
re-optimisations) but the ECP auto-helper itself has landed in
main since v0.8.0. The manual recipe below remains useful when
you want to override the Stuttgart-MDF default mapping per
element row.
When you need it¶
An ECP is required, and attached automatically, on every atom whose block in the chosen basis is valence-only:
lanl2dz,lanl2tz,lanl08*: Na and heavier.dhf-{svp,sv(p),tzvp,tzvpp,qzvp,qzvpp}: Rb and heavier (the Dirac-Fock small-core potentials).vdzp: its custom cores on B-F, Al-Ar, Ga-Kr, and Rb-Rn.def2-sv(p),def2-svp,def2-tzvp,def2-tzvpp,def2-qzvp,def2-qzvppand their diffuse-dvariants: Rb-Rn (the diffuse sets skip the lanthanides), with the def2-ECP.def2-msvp,def2-mtzvp,def2-mtzvpp: Rb-Rn, with the def2-ECP.pob-tzvp-rev2: Rb-I, Cs-Po, La-Lu, with the Stuttgart-Cologne potentials of its CRYSTAL records.{,aug-}cc-p{V,wCV}{D,T,Q,5}Z-PP: Cu-Kr, Y-Xe, Hf-Rn, with the Stuttgart-Köln MCDHF potentials (10-, 28- and 60-electron cores). All 16 sets carry the same potential; they differ only in the orbital blocks.
An ECP is not needed, and none is attached, when the block is
all-electron: every basis on H-Kr except vDZP, all of x2c-*, ano-rcc*,
sarc*, sapporo-dkh3-* and cologne-dkh2 on every element, and the
LANL / dhf families on light atoms.
A request that needs an ECP vibe-qc does not ship is refused: the diffuse def2-*D sets carry no lanthanide block. (The cc-pVnZ-PP orbital sets were the other example here until they shipped on 2026-09-08.)
The bundled XML libraries, for explicit pairing only¶
The six libecpint XML libraries are consulted only when a script sets
ecp_centers + ecp_library itself. Their element coverage is narrower
than the names suggest, and their numbers are rounded copies of the
published sets, which is why a bundled basis’ own sidecar is preferred:
Library |
Elements in the bundled XML |
Core |
|---|---|---|
|
K-Kr |
10 |
|
Rb-Xe |
28 |
|
Cs, Ba |
46 |
|
Hf-Rn, Ac-U |
60 |
|
Fr, Ra |
78 |
|
Na-La, Hf-Bi, U-Pu |
varies |
The def2-ECP that the def2 family pairs with is Wood-Boring for Y-Cd and
Hf-Hg and Dirac-Fock elsewhere; it ships as the .ecp sidecar of every
def2 orbital file (and of the def2-m* composite bases), not as an XML
library. AgH in def2-TZVP reproduces PySCF’s def2-TZVP + def2-ECP to the
microhartree (tests/test_ecp_correlated.py).
Python API surface¶
import vibeqc as vq
# Version 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
ec.xyz = [0.0, 0.0, 0.0]
# 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)
# Auto-helper (shipped on main; see § Phase-14e above)
ecp_centers, library_name = vq.auto_ecp_centers(mol, "lanl2dz")
# Drive any SCF with ECPs:
opts = vq.UHFOptions()
opts.ecp_centers = [ec]
opts.ecp_library = "lanl2dz"
result = vq.run_uhf(mol, basis, opts)
The same ecp_centers + ecp_library API works on every
molecular SCF driver: run_rhf, run_uhf, run_rks,
run_uks.
Periodic SCF with ECPs¶
The native direct drivers vq.run_rks_periodic and
vq.run_rhf_periodic_gamma apply a periodic ECP: PeriodicKSOptions and
PeriodicRHFOptions carry ecp_primitive_blocks, ecp_home_centers,
ecp_effective_charges and ecp_total_ncore, H_core uses Z_eff nuclear
attraction + the lattice-summed V_ECP, the electron count is reduced to
the valence count, and the ionic repulsion uses Z_eff. Build the fields
from the bundled pob-TZVP-rev2 records with
vibeqc.basis_crystal.build_periodic_ecp_data(system, atoms) where
atoms comes from vibeqc.periodic_runner._bundled_pob_source_atoms;
the heavy records ship in basis_library/sources/pob-TZVP-rev2/, so this
needs no network. Ag fcc / PBE / pob-TZVP-rev2 on this route agrees with
PySCF.pbc to within 1 mHa per atom (tests/test_periodic_ecp.py).
run_periodic_job applies a periodic ECP on its default route: the
k-point GDF drivers (jk_method="gdf", RHF / RKS / UHF / UKS, 3D cells)
build V_ne from a Z_eff nuclear frame, Bloch-sum the lattice-summed V_ECP
into every Hcore(k), use the Z_eff Ewald repulsion and fill the valence
count, so
run_periodic_job(system, basis, method="RKS", functional="pbe",
kpoints=[4, 4, 4], smearing_temperature=0.005, output="ag_pbe")
on a pob-TZVP-rev2 Ag cell is a 19-valence-electron-per-atom calculation.
The .system manifest and the ionic energy record it: result.e_nuclear
is the Ewald energy of the Z_eff charges. method="RHF" / "UHF" take the
same route; until 2026-09 PeriodicRHFOptions had no ECP fields and every
periodic HF request on an ECP basis died on the runner’s assignment
(#88). The UHF / UKS drivers split alpha and beta from the valence count
(AgCl in pob-TZVP-rev2: 18 / 18 electrons, not 32 / 32). With the default
mesh (kpoints=None) an ECP-bearing cell is routed to the same k-point
drivers at a one-point mesh; the run_pbc_gdf_* Gamma fast paths and the
legacy Gamma driver read no ECP field and refuse ECP options when called
directly. The runner resolves the ECP from the bundled pob-TZVP-rev2
records for that family and from the basis’ .ecp sidecar for every other
basis, so BasisSet(cell, "lanl2dz") or def2-TZVP on a silver cell carry
their ECP on this route too (LANL2DZ ships no fitting set; pass
aux_basis= explicitly). Every other periodic route
refuses an ECP-bearing cell before any SCF work, naming the elements:
RIJCOSX, the Ewald open-shell drivers, ROHF / ROKS on GDF, slabs, BIPOLE,
and the external-XC GPW / GAPW / AICCM adapters. Until
2026-09 the runner set the ECP fields on the options and those drivers
silently ignored them (Ag fcc in pob-TZVP-rev2 was dispatched with 188
electrons into a 19-valence-electron basis); the refusal is what replaced
that.
Gradients, forces, geometry optimization¶
Analytic nuclear gradients are ECP-aware (2026-05-18 audit
remediation). The gradient must differentiate the same
Hamiltonian the SCF solved, so GradientOptions mirrors the
SCF options’ ECP fields:
go = vq.GradientOptions()
go.ecp_centers = opts.ecp_centers # same list as the SCF call
go.ecp_library = opts.ecp_library
grad = vq.compute_gradient(mol, basis, result, go)
When ecp_centers is set, the nuclear-repulsion and
nuclear-attraction derivative pieces switch to the effective
charges Z_eff = Z - n_core and a dV_ECP/dR term (libecpint
first derivatives) is added. Historically, an empty ecp_centers list let
an XML-ECP reference reach the bare-Z all-electron derivative, which was
wrong at the 0.1 Ha/bohr scale (regression-pinned in
tests/test_ecp_gradient.py). Current public wrappers compare the exact
(Z, xyz) centre multiset and normalized library recorded on the SCF result.
They also refuse results from the low-level arbitrary-Hcore SCF entry points,
whose ECP provenance is deliberately unknown.
Inline-primitive ECPs¶
ECPs reach the SCF by two routes, and the gradient must
follow whichever one ran. Bases whose per-element cores have no
matching libecpint XML library – vDZP, def2-mSVP, and
CRYSTAL/pob data – are attached as inline primitive blocks
(see the auto-attach section above), which leaves ecp_centers
empty. Mirror those fields instead:
go = vq.GradientOptions()
go.ecp_primitive_blocks = opts.ecp_primitive_blocks
go.ecp_primitive_centers = opts.ecp_primitive_centers
go.ecp_effective_charges = opts.ecp_effective_charges # per ATOM
go.ecp_total_ncore = opts.ecp_total_ncore
grad = vq.compute_gradient(mol, basis, result, go)
The XML-library and inline-primitive routes are mutually exclusive; setting
both is an error. ecp_effective_charges has one entry per atom (bare Z
on atoms carrying no ECP), while ecp_primitive_blocks and
ecp_primitive_centers have one entry per ECP centre. Public gradient
wrappers compare each complete primitive block together with its paired
centre, the effective-charge vector in atom order, and ecp_total_ncore
against the verified SCF result.
Leaving these unset on an inline-ECP SCF used to trigger the same bare-Z
fallback described above: measured at 0.16 Ha/bohr on CH4/vDZP, ~1.9x the
true force (#574, pinned in tests/test_ecp_gradient.py). It now fails the
provenance check instead.
What wires this for you¶
One helper builds the mirror, and every internal consumer goes through it:
from vibeqc.gradient_options import gradient_options_from_scf
go = gradient_options_from_scf(opts) # JK backend + ECP fields, both routes
grad = vq.compute_gradient(mol, basis, result, go)
The ASE calculator (
vibeqc.ase.VibeQC) uses it foratoms.get_forces()andoptimize=True.The FD Hessian (
compute_hessian_fd) uses it whenever you pass nogradient_options(#576). That covers the molecular runner’shessian=Truefrequency / thermochemistry path andrun_irc’s transition-state Hessian, which pass none. An explicitgradient_optionsis honoured for its JK-backend fields, but its ECP fields are re-synchronised to the SCF options at every displaced geometry.compute_hessian_fdalso moves the ECP centres with the displaced atom. Both routes pin the potential to absolute coordinates (ecp_centers[i].xyz,ecp_primitive_centers[i]), and the SCF attaches a centre to a nucleus only within 1e-6 bohr (libecpint’s own assignment tolerance is 1e-4 bohr). Displace the nucleus without the centre and the atom silently reverts to bareZwhileV_ECPkeeps acting: pre-#576, [ZnH]+/6-31G/ecp10mdf diverged to -7e10 Ha at the first 0.005 bohr Zn displacement, and the all-electron atoms’ Hessian columns were the bare-Z ones. ECP options whose centres sit on no atom of the molecule are now refused up front with aValueErrornaming the centre.
Every other molecular driver that evaluates the SCF away from the geometry the centres were built for follows the same rule (#643):
run_dimer/run_ircpath forces (the shared image evaluator) move the centres onto each visited geometry, run the SCF through the ECP-aware drivers instead of the all-electron warm-start seam, and mirror the ECP fields onto the gradient when you pass nogradient_options; your options come back describing the start geometry. A top-levelgrid_optionsoverride is refused on ECP RKS/UKS images (set the grid on the KS options instead).The ASE calculator (
vibeqc.ase.VibeQC) moves the centres onto the current atoms at everycalculate(). The centre-to-atom map is fixed at the first geometry where the centres sit on the atoms; afterwards the*_optionsyou passed describe the last geometry evaluated, not the one you built them for. On ECP systems the calculator’shessianproperty uses the FD route, because the analytic kernels take no ECP options.The native optimisers (
optimize_molecule,optimize_molecule_brent, the runner’soptimizer_backend="native"/"brent") follow the centres per step and restore your options on return;run_jobattaches the ECP for the start geometry before any optimiser runs and moves the centres onto the optimised geometry before the final single point and the Hessian.Centres that sit on no atom of the start geometry are refused up front with a
ValueErrornaming the centre, on every one of these paths.
run_neb still refuses molecular ECP paths (its warm-start image loop
is all-electron by construction).
Post-HF and multireference derivative routes (MP2 / CC optimisations, CASSCF, CASPT2 and NEVPT2 gradients and Hessians) remain unsupported with ECPs: their derivative boundaries cannot yet preserve and verify the exact ECP operator, so they fail before SCF or trajectory work. Their energies are a different matter; see the next section.
Phase-14f/g: CRYSTAL INPUT ECP-block parser + inline application¶
Phase 14f shipped on main a parser for CRYSTAL INPUT-format
ECP blocks so basis sets distributed in CRYSTAL’s ECP format (the
5th-period pob bases: Rb-I, Cs-Po) parse without raising:
parsed = vq.basis_crystal.parse_crystal_atom_basis(crystal_input_block)
parsed.ecp # CrystalECP dataclass
parsed.ecp.terms # list of CrystalECPTerm
Phase 14g wires those parsed inline ECP terms through libecpint’s
set_ecp_basis(…) inline-primitive API (no XML library needed):
from vibeqc.basis_crystal import (
build_periodic_ecp_data,
crystal_ecp_to_libecpint_arrays,
)
# Convert CRYSTAL ECP → libecpint flat arrays.
arrays = crystal_ecp_to_libecpint_arrays(parsed.ecp)
# arrays.exponents, .coefficients, .ams, .ns -- ready for
# vq.compute_ecp_matrix_from_primitives(basis, center_xyz, [block])
# Or build periodic ECP data for run_periodic_job in one call:
ecp_blocks, ecp_centers, eff_z, ncore = build_periodic_ecp_data(
system, atom_list
)
This is the path run_periodic_job uses internally for pob-TZVP-REV2
heavy elements. Applications that need ECPs from non-XML sources
(vDZP custom cores, mixed-row dhf-* archives) can use the same
ECPPrimitiveBlock + compute_ecp_matrix_from_primitives building
blocks directly.
Citations¶
For published work using ECPs:
libecpint software: R. A. Shaw, J. G. Hill, J. Chem. Phys. 147, 074108 (2017); R. A. Shaw, J. Chem. Phys. 159, 014103 (2023).
Stuttgart-Köln MDF family: Andrae, Häußermann, Dolg, Stoll, Preuß, Theor. Chim. Acta 77, 123 (1990) and per-element follow-ups in the libecpint repository documentation.
Hay-Wadt LANL family: Hay, Wadt, J. Chem. Phys. 82, 270 (1985); 299 (1985); 284 (1985).
The libecpint upstream project documents the per-element citations in its source repository.
See also¶
basis_sets.md, orbital basis selection, including which orbital bases are designed to be paired with an ECP.density_fitting.md, DF / RIJCOSX Fock build, fully ECP-aware.-
full ECP licensing inventory.