ferric
A quantum chemistry engine written in Rust, with Python bindings and a TOML-driven command line. It computes Hartree–Fock and DFT energies and gradients, the MP2 family, coupled cluster, RPA and GW, and constrained DFT, with libint2 for the integrals.
pip install ferric # Linux x86_64, Python 3.10–3.13
Start here
Get started → Install the wheel and compute a checked number in under a minute. Then read the sharp bits that catch new users.
How-to guides → Task recipes: charged and open-shell molecules, optimization, QM/MM. Choosing a method maps a chemistry question to a method.
Methods → What each method family is, how to run it, how accurate it is, what it costs and what to cite.
Reference → Capabilities and validation · Input file · Python API · Examples · Rust API
| If you are | Start with |
|---|---|
| A chemist who wants numbers | Your first calculation, then Choosing a method |
| Coming from PySCF | For PySCF users |
| Working on drug-discovery workflows | End-to-end applications and QM/MM |
| A method developer | Electronic response, Architecture, Rust API |
| An automated agent | For automated agents |
Implemented ≠ validated
Working code is not a checked number. These pages describe what exists; how far each capability's numbers have been checked against an independent reference differs a lot between methods. Each one is graded individually in Capabilities and validation. Read it before relying on a result.
The idea
ferric is organized around electronic response: how the density reacts to
a perturbation. That object appears as the polarizability \( \alpha \), the
dielectric function \( \varepsilon \) and the susceptibility \( \chi \), and
three of the method families here are different ways of getting it right where
standard methods get it wrong:
- Attenuated MP2: MP2 builds dispersion from an uncoupled polarizability, which overbinds some systems (π-stacking is the classic case). Attenuating the correlation operator removes the long-range part where that error lives.
- PDEP-RPA / GW: the dielectric matrix is the density–density response. PDEP keeps only its dominant eigenmodes, a low-rank representation of the screening used by RPA correlation and the GW interaction.
- Constrained DFT: a constraint couples to the density and reads its response, building charge-localized diabatic states and their electron-transfer couplings.
Electronic response develops the argument and says which parts of it are demonstrated and which remain a design premise.
Source, license, citation
github.com/mgoldey/ferric, dual-licensed MIT / Apache-2.0. To cite ferric and the methods you used, see References and citing.
Installation
There are two ways in. Most people want the first.
| You want to | Do this | Time |
|---|---|---|
| Run calculations | Install the prebuilt wheel | about a minute |
| Change ferric's Rust code, or use MPI | Build from source | about 10 minutes, mostly compiling ferric |
Fastest: the prebuilt wheel
Wheels are published to PyPI for Linux x86_64 (manylinux_2_28),
CPython 3.10–3.13, and for no other platform:
| Platform | Wheel | Source build |
|---|---|---|
| Linux x86_64, CPython 3.10–3.13 | yes | yes (the only tested platform) |
| Linux aarch64 | no | not tested |
| macOS (x86_64, arm64) | not yet (wanted; no wheel is built) | not tested |
| Windows | no | not supported: build.rs hardcodes Unix paths and links Unix static archives |
libint2 and libxc are statically linked into the extension,
and OpenBLAS ships inside the wheel as a bundled shared library, so nothing
needs compiling. The wheel's libint2 carries second derivatives, so analytic
Hessians work from a plain pip install; see
What the libint2 build carries.
pip install ferric # or: uv pip install ferric
The only releases so far are pre-releases (0.1.0rc*). pip and uv both select
them when no final release exists, so the plain command above works. Pin a
version (ferric==0.1.0rc5) if you need a reproducible environment.
Which build am I running shows how to check.
The wheel gives you two things:
- the Python module:
import ferric - a
ferriccommand onPATHthat runs TOML input files (the same CLI ascargo run --bin ferric)
Check it works
python -c "import ferric; print(ferric.__file__)"
Then run your first calculation. It takes under a second.
What the wheel does not contain
The wheel holds the compiled library and nothing else. The examples/ input
files and the testdata/ molecules live in the
git repository. Pages that use them say so. To have them locally:
git clone https://github.com/mgoldey/ferric
cd ferric
ferric examples/water-rhf.toml
The CLI resolves [molecule] xyz = "..." relative to the current directory,
so run example files from the repository root.
The large-libint wheel (GitHub releases)
Starting with 0.1.0rc8, each GitHub release
also carries a second wheel for each CPython. It has the same version and
commit as the PyPI wheel, but it is built against conda-forge's libint2
2.13.1 (the build scripts/install-libint.sh installs), which it bundles as a
shared library. Its file name carries the wheel build tag 1largeam:
ferric-0.1.0rc8-1largeam-cp311-cp311-manylinux_2_28_x86_64.whl
Compared with the PyPI wheel it adds l = 7 shells, analytic 3- and 2-centre second derivatives, multipoles above order 2 and G12 (F12) integrals; see What the libint2 build carries. It does not extend analytic Hessians: 4-centre second derivatives stop at f functions in both wheels. RI-J/RI-K Hessians already work with the PyPI wheel, so they are no reason to switch. Nor is it a strict superset: its one-electron integrals reach l = 7 / 6 / 3 (energy / 1st / 2nd derivative), while the PyPI wheel's reach 6 / 6 / 6, so one-electron second derivatives of g, h or i functions are available only in the PyPI wheel.
Install the PyPI wheel first, so its dependencies are present, then swap in
the file for your Python from the release (check it against the release's
SHA256SUMS):
pip install ferric==0.1.0rc8
pip install --force-reinstall --no-deps \
https://github.com/mgoldey/ferric/releases/download/v0.1.0rc8/ferric-0.1.0rc8-1largeam-cp311-cp311-manylinux_2_28_x86_64.whl
Both wheels are the distribution ferric at the same version, so pip list
shows no difference and only one can be installed at a time:
pip install ferric==0.1.0rc8then reports "already satisfied" and keeps the large wheel.pip install -U ferricat the next release replaces it with that release's PyPI wheel; swap in the large one again from the new release.- With both files on one
--find-linksindex, pip picks the build-tagged (large) wheel.
To see which one is loaded:
python -c "import ferric; i = ferric.build_info(); print(i['libint_version'], i['libint_linkage'], i['libint_max_am'])"
The large wheel prints 2.13.1 shared and 'eri3': [7, 7, 4]; the PyPI wheel
prints 2.7.2 static and 'eri3': [6, 6]. The libint: line of
ferric --version reports the same limits.
The large wheel contains a libint2 binary (LGPL-3.0-only) built by
conda-forge. Its source is the
loriab/libint v0.1
generated tarball, built by the
conda-forge libint feedstock;
each release's notes link both and give the tarball's sha256. The licence
texts ship with the release as libint2-LICENSE.txt and
libint2-COPYING.LESSER.txt, covered by its SHA256SUMS.
Building from source
Build from source if you are changing ferric, need the MPI build, or are on a
platform without a wheel (only Linux x86_64 is tested; see the table above).
ferric links libint2, a C++ integral library;
scripts/install-libint.sh installs a prebuilt copy in seconds.
Prerequisites
- Rust 1.75+: install via rustup
- libint2 2.13.1: the conda-forge Linux x86-64 build, which includes
second-derivative integrals;
scripts/install-libint.shdownloads it (checksum pinned) and needscurl,python3,zstdandpatchelf - libxc, OpenBLAS, LAPACK, Eigen3 and Boost headers
- Python 3.10+ and maturin: optional, for the Python bindings
Steps (Ubuntu 22.04+)
The apt list matches what CI installs (.github/workflows/ci.yml).
# 1. System dependencies
sudo apt-get install -y build-essential cmake g++ gfortran wget git \
libeigen3-dev libopenblas-dev liblapack-dev pkg-config \
libxc-dev libboost-dev patchelf zstd \
python3-dev python3-pip python3-venv
# 2. Get ferric and install libint2 (seconds)
git clone https://github.com/mgoldey/ferric
cd ferric
scripts/install-libint.sh ~/.local/libint2-2.13.1
export LIBINT2_PREFIX=~/.local/libint2-2.13.1
# 3. Build ferric
cargo build --release
# 4. Check it: seconds, not minutes
OPENBLAS_NUM_THREADS=1 ./target/release/ferric examples/water-rhf.toml
The last line should end with converged = true and
energy = -74.9631468000 Hartree, as shown in the
first calculation.
LIBINT2_PREFIX is read by crates/ferric-integrals/build.rs; unset, it
defaults to the installer's ~/.local/libint2-2.13.1 when that exists, and to
~/.local otherwise, so the export above is optional for the default path. The install script records the library's absolute path
in every binary that links it, so moving or deleting that directory breaks
the build's executables until they are rebuilt. A prefix holding a static
libint2.a (a from-source libint2 build) also works, but a library generated
without second derivatives has no analytic Hessians; frequencies then use
finite differences of the analytic gradient.
The workspace has three binaries (ferric, ferric-cli and ferric-batch),
so cargo run needs to be told which one: cargo run --release --bin ferric -- input.toml.
Running the test suite
OPENBLAS_NUM_THREADS=1 cargo test --workspace
.cargo/config.toml sets OPENBLAS_NUM_THREADS=1 for cargo-invoked
processes only when the variable is unset (its [env] entry has no
force = true, so an exported value wins). The prefix above makes this command
use 1 regardless of your shell. Do not raise it above 1; see
Threading.
The Python binding tests are pytest, not cargo; see
CONTRIBUTING.md.
Debug vs release
- Debug (
cargo build): fast to compile, slow to run. Use it while iterating on Rust code. It catchesdebug_assert!violations and integer overflow that release builds silently allow. - Release (
cargo build --release): slow to compile, fast to run. Use it for anything you will wait on: real molecules, benchmarks, and any RPA/GW/CC job.
Python bindings from source
uv sync
uv run maturin develop --release
uv run python -c "import ferric; print(ferric.__file__)"
Use uv run maturin develop, not a bare maturin develop. A bare one can
install into a different interpreter from the one uv run python loads, and the
stale build keeps getting imported.
Building from the sdist
Each release publishes an sdist next to the wheels, so
pip download ferric==<version> --no-binary :all: succeeds. The sdist holds
the Rust workspace crates the extension depends on, Cargo.lock and the
vendored libecpint. It does not hold libint2, so it is not a
self-contained build: pip install from it needs everything in
Prerequisites already present, with LIBINT2_PREFIX set to a
libint2 install. The libint2 source is generated and too large to bundle (the
export below is 183 MiB against PyPI's 100 MiB file limit).
Two ways to get a libint2 for an sdist build:
- Linux x86-64:
scripts/install-libint.sh(conda-forge 2.13.1). The script is not in the sdist; take it from the repository. - Any platform with CMake and Eigen3: build the export the wheels use,
libint-2.7.2-ferric-small-1.tgzfrom thelibint-2.7.2-ferric-small-1release (verify its sha256 againstLIBINT_SHA256in.github/workflows/wheels.yml). Configure it withcmake .. -DCMAKE_INSTALL_PREFIX=$PREFIX -DCMAKE_PREFIX_PATH=$PREFIX -DCMAKE_POSITION_INDEPENDENT_CODE=ON -DCMAKE_BUILD_TYPE=Release, thenmakeandmake install, and setLIBINT2_PREFIX=$PREFIX. The build takes about 30 minutes. It has the wheel's integral classes (see the table below), so F12 methods still need the 2.13.1 build. The recipe is tested only as part of the Linux x86-64 wheel build.
On a platform with no wheel, plain pip install ferric falls back to this
sdist and fails at the libint2 step unless those prerequisites are present.
An sdist build carries no git metadata, so
the build identity reports commit: "unknown".
What the libint2 build carries
Integral classes, derivative orders and angular-momentum limits are fixed when libint2's source is generated, so no build flag changes them. Highest angular momentum for energy / 1st / 2nd derivatives (– = not generated):
| Integrals | Source build with scripts/install-libint.sh (conda-forge 2.13.1) | PyPI wheel (ferric's libint2 2.7.2 export) | Needed for |
|---|---|---|---|
| 4-centre ERI | 7 / 6 / 3 | 6 / 6 / 3 | SCF, gradients, analytic Hessians |
| One-electron (overlap, kinetic, nuclear) | 7 / 6 / 3 | 6 / 6 / 6 | the same |
| 3- and 2-centre ERI (auxiliary shell) | 7 / 7 / 4 | 6 / 6 / – | RI-MP2, RPA, GW and their gradients |
| G12 geminal | 4 / – / – | – | F12 / geminal integrals |
The 3-centre limit is for the auxiliary shell; libint caps the two orbital shells of a 3-centre integral at its default limit for that derivative order (the wheel: l ≤ 6 for energies, l ≤ 4 for first derivatives; 2.13.1: 6 / 6 / 3).
With either build, analytic Hessians cover orbital bases up to f functions; a
basis with g or higher functions uses finite differences of the analytic
gradient. The wheel has no G12 class, so F12 methods need a source build.
scripts/generate-libint-small.sh regenerates the wheel's export. The upstream
mpqc4 tarball (libint2 2.7.2) has no second derivatives and no G12 class: built
against it, ferric uses finite-difference Hessians and the G12 tests skip with
an explicit message.
Which build am I running
python -c "import ferric; print(ferric.__version__); print(ferric.build_info()); print(ferric.__build__)"
ferric --version
The wheel and a conda-forge 2.13.1 build share a version and commit; they
differ in the libint2 limits in the table above, which ferric --version (the
libint: line) and ferric.build_info()["libint_max_am"] report.
ferric.build_info() and ferric --version report the version, the full git
commit, whether tracked files differed from that commit (dirty), the cargo
profile and the libint2 version. ferric.__build__ is the compact stamp
{"git_sha": ..., "dirty": ...}. All of it is fixed when the extension is
compiled, so it describes the loaded .so even if that is a symlink into
another checkout.
- A released wheel carries the plain version (
0.1.0), the tagged commit anddirty: False. To release, push avX.Y.Ztag; there is nothing to commit, and the build fails rather than ship from a dirty or unidentified tree. - A build from a checkout carries a
.devNversion (0.1.0.dev0) and the commit it was built from, withdirty: Trueif tracked files had uncommitted changes. Untracked files do not count. - A build with no git metadata (an sdist or source tarball) reports
commit: "unknown", andferric.__build__isNone.
MPI
MPI is a source build only. The wheels workflow publishes no MPI wheel to PyPI.
sudo apt-get install -y libopenmpi-dev openmpi-bin libclang-dev
cargo build --release --workspace --features mpi
mpirun -np 4 -x OPENBLAS_NUM_THREADS=1 ./target/release/ferric examples/water-rhf.toml
libclang-dev is needed by mpi-sys's bindgen step. MPICH and Intel MPI are
not supported: their libmpi.so.12 ABI differs from OpenMPI 4.x's
libmpi.so.40.
The CLI is the MPI entry point; the Python API is not
The CLI is SPMD by design: mpirun -np N ferric input.toml runs one rank per
process.
mpirun -np N python script.py is not supported. The bindings expose no
rank or world-size accessor, so if rank == 0 cannot be written. Every rank
runs the whole script, prints N times, and races on the same output files.
GPU (CUDA)
The CUDA backend is a source build only and is off by default even when built in.
cargo build --release -p ferric-cli --features ferric-cli/gpu
FERRIC_GPU=auto ./target/release/ferric examples/water-ccsd.toml
Building needs no CUDA toolkit: the driver and cuBLAS libraries are loaded at run time. Running needs an NVIDIA driver and the CUDA 12 runtime libraries (libcuda.so.1, libcublas.so.12) on the loader path; without them mode = "auto" prints a notice and runs on the CPU, and mode = "on" stops the ferric binary with the reason. Library and Python callers that set FERRIC_GPU=on with no usable device get the same notice and a CPU run, not an error; check ferric.gpu_status(). The CUDA 12 libraries support compute capability 5.0 and above; CUDA 13 does not support Pascal (compute capability 6.x), so a Pascal card needs the CUDA 12 libraries.
What runs on the device, under mode = "auto" or "on":
- Dense f64 contractions issued through
einsum!(the coupled-cluster and MP2-family drivers) above a size threshold. - The closed-shell RI-MP2 energy, with
B_ovresident on the card: f64 by default ([precision] mode = "f64"), or with[precision] mode = "mixed"B_ovstored as f32 and accumulated in f64 (see the validation page for its measured error). - The unrestricted RI-MP2 energy (open-shell reference, amplitude-free path), with one resident
B_ovper spin: f64 by default, or withprecision = "mixed"each spin'sB_ovstored as f32 and the αα, ββ and αβ blocks accumulated in f64 by the same kernel. - Mixed applies to the RI-MP2 energy under the Coulomb, erfc and terfc operators, closed-shell or unrestricted (plain RI-MP2, attenuated RI-MP2, SCS-MP2 with two terfc operators, the MP2 half of att-MP2+VV10); the erf operator, composite fitted operators, SR-MP2 in RS-MP2+RPA, OO-MP2 and kappa-regularised runs use the f64 device kernel.
- The density-fitted Coulomb matrix (RI-J): the raw three-index tensor is stored on the card once per geometry (packed lower triangle, f64) and each J build moves only the density weights and the result.
- The density-fitted exchange matrix (DF-K, occupied-orbital path): the dressed three-index tensor is stored on the card once per geometry and each K build moves only the occupied coefficients and the result, in f64.
- The Kohn-Sham exchange-correlation grid (closed- and open-shell, LDA, GGA and meta-GGA), only under
mode = "on"(or a preset that sets it):autokeeps the XC grid on the CPU, because the device path was measured slower than the CPU. Measured on a GTX 1080 (Pascal, sm_61), PBE/cc-pVDZ SCF wall time: benzene 2.0 s on the CPU, 3.6 s with the device XC grid in f64 and 5.5-6.8 s in mixed; n-octane 8.5-8.9 s, 18-23 s and 17 s; each device XC build took 2.2-3x the host time (6-core host,RAYON_NUM_THREADS=3). Withmode = "on"the screened per-batch basis-function values are stored on the card once per grid, and each Fock build runs the two per-batch matrix products (densityΦ = D·χand potentialV = A·χᵀ) on the device in f64; namingxc-vxcinmixed_kernels(withprecision = "mixed") forms the potential product from f32 operands with f64 accumulation while the SCF density is still changing (opt-in; it is not in the default mixed set). Grid construction, screening, the libxc functional evaluation, the density reductions and the fixed-order V accumulation stay on the CPU, so the result is reproducible and independent of the thread count. The VV10 nonlocal grid and XC gradients run on the CPU.
RI-J and DF-K run on the CPU when their tensor does not fit the device pool (or, for DF-K, is spilled or recomputed on the host) and under multi-rank MPI; the notice names the reason. When one SCF has both and the pool cannot hold both, DF-K gets the device. The XC grid runs on the CPU when its basis-function blocks do not fit what is left of the device pool, or after any device error (the notice names the reason; the failed pass is recomputed on the CPU). Integrals, SCF diagonalisation, DIIS and grid construction run on the CPU. Device results are reproducible run to run on one device and agree with the CPU to the accuracy of a different summation order, not bit for bit.
The shortest way to use the device is one line in the input, gpu = "on" (f64) or gpu = "mixed", or ferric --gpu mixed input.toml on the command line (the flag overrides the input file and FERRIC_GPU_PRESET). The [gpu] table in the input reference lists the keys and what each preset turns on.
Threading
The ferric binary and import ferric pin OpenBLAS to one thread when
OPENBLAS_NUM_THREADS is unset, and honour the variable when it is set
(cargo sets it to 1 through .cargo/config.toml, but only when it is unset,
so an exported value reaches cargo-launched processes). Leave it unset or at 1.
ferric uses rayon for its parallelism; a threaded OpenBLAS on top of that
oversubscribes the machine, and its LU routines can crash when called from
rayon workers. For throughput
across many independent jobs, prefer many single-threaded processes over one
multi-threaded job.
Your first calculation
This page takes you from an installed ferric to one checked number, then shows
where to go next. It assumes you have run pip install ferric
(Installation). No git clone is needed until the last
section.
We compute the Hartree–Fock energy of water in the minimal STO-3G basis. It takes well under a second and has a known answer, so you can tell immediately whether your installation is right.
1. From Python
Save this as water.py:
import ferric
# XYZ format: atom count, a comment line, then symbol x y z in Ångström.
water = ferric.Molecule.from_xyz_string("""3
water
O 0.000000 0.000000 0.117790
H 0.000000 0.755453 -0.471161
H 0.000000 -0.755453 -0.471161
""", 0, 1) # charge 0, spin multiplicity 1 (singlet)
basis = ferric.BasisSet.bundled("sto-3g")
rhf = ferric.run_rhf(water, basis)
print(rhf.converged, f"{rhf.energy:.10f}")
Run it:
OPENBLAS_NUM_THREADS=1 python water.py
You should see:
True -74.9631468000
Two things to notice:
- Always read
converged. Whenrun_rhfruns out of iterations it still returns an energy; it sets the flag and does not raise. A number withconverged == Falseis not a result. See Sharp bits. - Energies are in Hartree. Geometries go in as Ångström; see Sharp bits for which accessors return Bohr.
2. The same thing from the command line
The wheel also installs a ferric command that reads a TOML input file. Write
the geometry to water.xyz:
printf '3\nwater\nO 0.000000 0.000000 0.117790\nH 0.000000 0.755453 -0.471161\nH 0.000000 -0.755453 -0.471161\n' > water.xyz
and write water-rhf.toml next to it:
[molecule]
xyz = "water.xyz" # resolved relative to the directory you run ferric from
[basis]
name = "sto-3g"
[method]
kind = "rhf"
OPENBLAS_NUM_THREADS=1 ferric water-rhf.toml
The output ends with:
RHF/sto-3g on water.xyz
nbasis = 7
iterations = 8
converged = true
energy = -74.9631468000 Hartree
Same molecule, same number. The CLI rejects any key it does not recognise, so a typo is an error rather than a silently ignored setting. Every key is listed in the input reference.
3. Add correlation
To go beyond Hartree–Fock, change the method and the basis. RI-MP2 needs an auxiliary (fitting) basis alongside the orbital basis, and STO-3G is too small for a meaningful correlation energy, so this example moves to cc-pVDZ:
bs = ferric.BasisSet.bundled("cc-pvdz")
aux = ferric.BasisSet.bundled("cc-pvdz-ri")
mp2 = ferric.run_rimp2(water, bs, aux)
print(f"RI-MP2 total energy: {mp2.total_energy:.10f} Ha")
RI-MP2 total energy: -76.2308014541 Ha
In TOML the same calculation changes the [basis] name and the method kind,
and adds an [mp2] section naming the auxiliary basis:
[basis]
name = "cc-pvdz"
[method]
kind = "rimp2"
[mp2]
auxbasis = "cc-pvdz-ri"
If auxbasis is omitted the CLI uses cc-pvdz-ri, but name it explicitly: the
auxiliary basis should match the orbital basis. This is
examples/water-rimp2.toml.
4. Where to next
| You want to | Go to |
|---|---|
| Pick a method for a chemistry question | Choosing a method |
| See what every method supports (open shell? gradients? CLI?) | Capabilities and validation |
| Charged or open-shell molecules, geometry optimization | Recipes |
| The whole Python surface | Python bindings |
| Coming from PySCF | For PySCF users |
| Know what to trust | Capabilities and validation |
Running the bundled examples
The repository has one input file per method under examples/, indexed in
Examples. They refer to molecules under
testdata/, which the wheel does not include, so run them from a clone:
git clone https://github.com/mgoldey/ferric && cd ferric
OPENBLAS_NUM_THREADS=1 ferric examples/water-rimp2.toml
OPENBLAS_NUM_THREADS=1 ferric examples/water-attmp2.toml # attenuated MP2, ω = 0.420 Å⁻¹
If you built from source rather than installing the wheel, replace ferric with
cargo run --release --bin ferric --.
Sharp bits
Behaviour that differs from what you might expect. This page follows the pattern of JAX's "Sharp Bits": each entry says what surprises people, why ferric does it that way, and what to do.
Leave OPENBLAS_NUM_THREADS unset
What happens: the ferric CLI and import ferric pin OpenBLAS to one
thread when OPENBLAS_NUM_THREADS is unset, and honour the variable when it is
set. (.cargo/config.toml sets it to 1 for cargo-launched processes, also
only when it is unset.) A value above 1 is therefore a deliberate choice, and
its effect depends on the workload: it can speed up one calculation and slow
another down several-fold.
Why: ferric's parallelism comes from rayon. OpenBLAS threads run inside
rayon's worker threads, so the thread count is roughly BLAS threads × rayon
threads. Measured on benzene with the ferric CLI, single runs on a heavily
loaded machine with 6 physical cores (12 hardware threads), so read these as
order-of-magnitude:
OPENBLAS_NUM_THREADS | unset | 1 | 6 | 12 |
|---|---|---|---|---|
RI-JK RHF (benzene-rhf-def2-rijk.toml) | 54 s | 31 s | 9.7 s | timed out at 1800 s |
PDEP-RPA (benzene-pdep-rpa.toml) | 5.9 s | 7.2 s | 22.9 s | 30.7 s |
"unset" and 1 run the same configuration, so the gap between those two columns is the machine's load, not a setting. B3LYP timings were noisy, with no clear trend. None of these runs crashed.
Three effects matter beyond speed:
- Oversubscription can stall a run. Once BLAS threads × rayon threads exceed the physical cores, a job can slow by orders of magnitude, as the 12-thread RI-JK run shows.
- Results change slightly. The BLAS thread count changes the order of floating-point summation. The RI-JK energy at 6 threads differs from the 1-thread energy by 6e-6 Ha; expect differences at the 1e-6 to 1e-5 Ha level.
- LU routines can crash. Threaded OpenBLAS LU factorization (
dgetrf) can overflow the 2 MB stacks of rayon's worker threads. ferric calls LU solves in BSE, PCM, COSMO, MBD and orbital-rotation steps, among others.
Do: leave OPENBLAS_NUM_THREADS unset, and ferric runs with one BLAS
thread. If you set it higher for a particular job, never above the number of
physical cores, and check the timing and the energy against a one-thread run
of the same input. For throughput across many molecules, run many
single-threaded processes.
converged is a flag, and it does not mean "ground state"
What happens: run_rhf, run_uhf and run_rohf return a result object
even when they hit max_iter; they do not raise. (run_dft, run_ksdft and the
correlated drivers behave differently: they raise if their reference SCF does
not converge.)
Why: a non-converged state is still useful for diagnosis, and raising would discard it.
Do: check result.converged in Python, or converged = true in CLI
output, before using any number, and treat False as "no result". True means
the SCF reached a stationary point, not necessarily the lowest one; open-shell
molecules especially can land on an excited solution. Comparing UHF with ROHF, or
a different starting guess, is a cheap check. See
Python bindings.
energy_conv is a sanity bound, not a target
What happens: setting [scf] energy_conv very tight (say 1e-10) can make
an SCF run tens of iterations past a settled density, or never converge.
Why: convergence is driven by the density criterion (density_conv). The
energy change between iterations has a noise floor, and a tight energy bound can
sit below it, so it is met only by chance. For a closed-shell run with exact J
and K the floor comes from the incremental Fock update and its integral
screening (integral_thresh). ferric estimates it as
0.0125 * integral_thresh * nbf^2.25 + 0.5 * eps * |E| * sqrt(nbf) Ha. The
estimate against the measured RMS of the energy change once the density has
settled:
| system | nbf | estimated floor | measured RMS |
|---|---|---|---|
| water / cc-pVDZ | 24 | 1.6e-11 | 4.5e-12 |
| benzene / cc-pVDZ | 114 | 5.3e-10 | 1.4e-10 |
| alkane C48 / STO-3G | 338 | 6.1e-9 | 2.2e-9 |
| 71-atom drug-like molecule / STO-3G | 239 | 2.8e-9 | 1.9e-9 to 2.7e-9 |
The estimate is good to a factor of a few and errs high for large systems; it
was fitted on systems up to about 340 basis functions, and beyond that it is an
extrapolation. With density-fitted J (and in the open-shell solvers, which
rebuild J and K every iteration) only rounding remains, about
0.5 * eps * |E| * sqrt(nbf): measured 3e-13 for benzene/cc-pVDZ with RI-JK and
1.3e-12 for alkane C20/6-31G with RI-JK. The CLI default is loose
(1e-3) by design.
What ferric tells you: when you set energy_conv below 0.3 times the
estimated floor, the CLI prints warning: [scf] energy_conv ... is below the estimated noise floor of dE ... to stderr and the Python run_rhf, run_uhf
and run_rohf emit a RuntimeWarning, once, before the SCF starts. A default
energy_conv is never warned about. Nothing else changes: the threshold is not
clamped and the SCF stops exactly where it otherwise would. A run that ends
without converging on the dE criterion alone repeats the statement in
RhfResult.convergence_summary. No estimate is made for functionals (grid
noise), the LinK and COSX exchange builders, PCM / COSMO, polarizable embedding
or non-default screening: those runs get no warning, which is not a statement
that the tolerance is reachable. The |E| in the rounding term is estimated
from the nuclear charges (ECP cores removed, about 5 % for all-electron
molecules, 3.7x high for an iodine ECP case); the point-charge or field energy
of an embedding is not included.
A symptom of the same floor: with a large point-charge field (point_charges= on run_rhf's closed-shell direct-J/K path) the number of SCF iterations can differ between two orderings of the same charges, because a change of a few ulps in the one-electron potential decides which iteration the dE test happens to pass. On rc7 a 6458-charge field took 64 iterations nearest-first and 111 in file order at energy_conv=1e-10. The potential itself changes only by roundoff (3e-15 to 1e-14 relative) and the converged energy by about 1e-12 Ha. ferric turns the incremental update off when energy_conv < 1e4 * integral_thresh or density_conv < 1e3 * integral_thresh (1e-8 and 1e-9 at the default integral_thresh), where all measured orders took 20 iterations; at the default energy_conv and density_conv every measured order took the same number (15 iterations on the field and orders tried; the tests do not assert iteration counts, so this is a measurement, not a guarantee for other fields). run_uhf and run_rohf use a separate open-shell incremental path and run_dft's default RI-J does not use the direct incremental path: for those the order changes the potential by roundoff too, but iteration counts were not measured. ferric does not reorder the charges.
Do: tighten density_conv, not energy_conv.
Density fitting is on in run_dft but off in run_rhf
What happens: comparing a ferric DFT energy with a code that uses exact Coulomb gives a discrepancy that grows with molecule size.
Why: run_dft, run_ksdft and the CLI's ksdft density-fit Coulomb
(RI-J) with def2-universal-jkfit by default, and, for functionals with exact
exchange (hybrids and range-separated hybrids), exchange (RI-K) with the same
basis. run_rhf builds exact four-centre J and K unless you pass df_j_aux/df_k_aux. The
difference is the fitting error that density fitting always carries. Against exact J at PBE/STO-3G it is 0.28 kcal/mol for water, 1.16 for
benzene and 9.5 for a 71-atom drug molecule.
Do:
- To compare with an exact-Coulomb code (ORCA with
NORI, PySCF withoutdensity_fit()), passdf_j_aux="exact"torun_dftorrun_ksdft, anddf_k_aux="exact"too for a hybrid."","none","off"and"conventional"mean the same. In the CLI, set[scf] df_j_aux = ""anddf_k_aux = "". The CLI'sSCF J/Klog line then readsRI-JK viawith a blank name; the run uses exact J and K. - Or fit on both sides:
density_fit(auxbasis="def2-universal-jkfit")in PySCF, orrun_rhf(..., df_j_aux="def2-universal-jkfit", df_k_aux="def2-universal-jkfit")in ferric, so J and K are both fitted on both sides.run_rhftakes a basis name or""here, not"exact". run_qmmm(KS methods),run_gwandrun_u_gwwithxc,run_tddftwith a functional,run_tdhf_static_polarizability,run_double_hybridandrun_rs_mp2_rpaalways density-fit their reference SCF withdef2-universal-jkfitand have no opt-out.
Units differ by interface and by accessor
| Quantity | Where | Unit |
|---|---|---|
Molecule geometry input (from_xyz, from_xyz_string, .xyz files) | Python, CLI | Ångström |
| Coordinates inside the Rust library | Rust | Bohr |
Attenuation ω: [mp2] omega (att-rimp2, rs-mp2-rpa), [mp2] mp2v_omega (mp2-v); omega= of run_attenuated_rimp2, run_rs_mp2_rpa, run_mp2_v, and terf_omega= of run_rs_mp2_rpa | CLI TOML, Python | Å⁻¹ |
Double-hybrid ω: [dft] omega (wb97x-l-v) | CLI TOML | Bohr⁻¹ |
tune_omega (bracket and result); omega= of compute_eri3_mo and compute_metric_2c | Python | Bohr⁻¹ |
| Range-separation ω | Rust configs | Bohr⁻¹ |
QmmmSystem(...) coordinates | Python | Ångström |
QmmmSystem.point_charges() | Python | Bohr |
QmmmSystem.link_atom_positions() | Python | Ångström |
| Energies | everywhere | Hartree |
Do: check the docstring of any coordinate accessor before converting it. Converting both QM/MM accessors "to be safe" makes one of them wrong by a factor of 1.89.
Energies take a BasisSet; geometry changes take a basis name
What happens: run_rhf(mol, ferric.BasisSet.bundled("sto-3g")) works, but
run_optimize wants the string "sto-3g".
Why: entry points that move atoms (optimization, frequencies, saddle search) build the basis themselves for each geometry, so they take its name.
Do: check the signature in Python bindings.
Not everything is on the CLI
The CLI runs the method.kinds in the
capability matrix, with task = energy,
optimize or frequencies. It also has [qmmm], [cosmo], [pcm],
[external_potential], [scf] stability_descent, [mp2] att_operator and
[dft] grid_prune. These capabilities are Python-only:
| Capability | Python | What the CLI has instead |
|---|---|---|
| Transition-state search, IRC | run_saddle, run_irc | no task for either |
| QM/MM MM forces, full-system gradient and optimization, smeared charges, Thole polarization, an MM force field | run_qmmm, run_optimize_qmmm, QmmmSystem, MmTopology | [qmmm] embeds the QM region in fixed point charges from a PQR file, with link atoms and boundary schemes |
| Constrained DFT and electron-transfer couplings | run_cdft, CdftConstraint, cdft_coupling | nothing |
Examples need the repository
examples/*.toml point at molecules under testdata/. Neither ships in the
wheel. Clone the repository and run
from its root; see Installation.
[memory] budget_gb does not cap the whole process
What happens: a job's resident memory can exceed budget_gb, and a job
whose budget is too small usually still runs, slower, rather than failing.
Benzene PBE/def2-SVP with budget_gb = 0.002 finishes with the same SCF
iterations, a peak RSS of 157 MB, and 11.2 s of wall time instead of 3.5 s.
Why: the CLI turns budget_gb (or, when unset, 0.8 × available RAM) into
one process-wide ledger. The large, size-dependent allocations reserve their
bytes from it before allocating: three-index RI tensors, the DFT grid's AO
cache, MO-transformed RI blocks, and the large tensors of the MP2, RPA, GW
and CC methods. Two such allocations alive at the same time therefore cannot
each claim the whole budget. Nothing else is charged: basis-sized matrices
(Fock, density, DIIS history), integral engines, BLAS and per-thread scratch,
allocator overhead. The spilled RI-JK path sizes its dressing and DF-K read-back
widths to the budget. Below a floor of about 200 aux rows for the dressing (raw
stream, one unpacked block and one GEMM result) and two DF-K chunks for the
read-back, a budget overshoots, with a warning naming the phase. The read-back is
sized for 8 workers; a run with more can overshoot by the extra per-worker
scratch, and says so.
When a charged allocation does not fit, the result depends on the allocation:
- The SCF's three-index tensor is stored whole when
n_aux × n_bf² × 8bytes fit. When only the symmetric half (n_aux × n_bf(n_bf+1)/2 × 8bytes, plus 64 unpacked aux rows of scratch) fits, it is kept in memory as that packed triangle and the RI-J build reads the packed rows directly. When even that does not fit, it is either spilled to a file in$TMPDIR(/tmpby default) and reread on every iteration, or recomputed on every iteration without writing anything.autochooses between those two from a rate measured on the machine;[scf] jk_storageforcesmemory,diskordirect. Holding the K dressing to a small budget takes several passes over the raw tensor, and a run making many says so when a pass costs IO or integrals. The dressing and K are designed to be bitwise the same spilled or in core, and tests confirm that atOPENBLAS_NUM_THREADS=1on one OpenBLAS kernel (Haswell). Threaded BLAS moves bits, and other kernels are untested. J is a separate matter: which storage raw gets can follow the shared ledger, so J can differ by a few ulp between runs that charge the ledger differently. - A dressed tensor that fits the budget but not beside the resident raw tensor
spills instead of stopping the job, unless
jk_storage = "memory". - A DF-guess pre-stage that runs out of memory is abandoned with a warning, and
the SCF starts from its usual guess. That changes iterations and can select a
different SCF solution, a larger effect than any bit-level one. An explicit
jk_storage = "memory"that does not fit, and any memory failure under MPI, stop the job instead. A spill directory that is full also stops it, anddf_incrementshas no fallback. - The DFT grid AO cache is recomputed at every Fock build instead of stored.
The energy is bit-identical. Under an explicit budget, XC takes its memory after
the RI-J/K tensors, so it never pushes them out. An auto-detected budget first
gives up 0.8 of the AO blocks XC expects to keep, so the raw tensor can drop to a
packed or spilled tier while XC stays resident. If what is left cannot hold its per-build working
set either, that set runs uncharged, a warning gives its size, and the run log
records it. The job can exceed its budget by more than that figure, because
other scratch is not charged either: danuglipron/def2-SVP warned of 0.46 GB and
peaked 0.92 GB over a 15.3 GB budget. The working set scales with the thread
count. The VV10 NLC cache has no fallback and is charged before RI-J/K, so a
budget that cannot hold it alone stops the job before any integrals are built.
It also leaves the MINAO guess's free-atom solves less room. If one runs out, that
element's block of the guess is left zero, so the SCF starts from a partial MINAO
guess rather than from hcore. If that was every element in the molecule, the density
is zero and the first Fock matrix is hcore. A warning names the element and says
which, the run log records it,
and the SCF may take more iterations. Very near that limit, repeat runs of one
input have differed in whether this happens.
jk_storage = "memory"with a VV10 functional is held to the budget less the cache, so it refuses budgets that cannot hold the cache and the tensors together. - An allocation with no fallback stops the job with an error naming it. For
some methods this check comes after the SCF. Benzene RI-MP2/def2-SVP with
budget_gb = 0.001completes the SCF, then exits withRI-MP2 MO-side blocks ... requires 0.01 GB; budget is 0.00 GB.
Some stages print a warning when resident memory passes 110% of the budget. The warning never stops the run.
From Python, memory_budget_gb= sets the same per-allocation limits, but no
shared ledger is installed. Each check compares its own allocation with the
whole budget, not with what the other allocations have left. Two checks
instead subtract the process's current RSS and allow 90% of the remainder: the
KS-DFT grid AO cache (store or recompute) and the UKS Newton/TRAH fxc
kernel's second grid cache. The fxc check subtracts RSS in the CLI
too.
Do: leave room below the machine's real limit for the uncharged part. On
a shared machine, also cap the process externally so a runaway job dies in
its own cgroup, for example with scripts/ferric-limited -- ferric input.toml
(defaults MemoryMax=12G, MemoryHigh=10G, no swap) or
systemd-run --user --scope -p MemoryMax=12G -- ferric input.toml.
GPU offload is opt-in and may not help
The CUDA backend is not in the PyPI wheel; it needs a source build with the gpu feature (see GPU (CUDA)), and even then it is off by default (FERRIC_GPU=auto or [gpu] mode turns it on). Consumer GeForce cards run FP64 at 1/32 of their FP32 rate or less, so offloading f64 contractions may be slower than the CPU once transfer time is counted; whether it helps depends on the card and the problem, and has to be measured on your own hardware.
The Kohn-Sham XC grid runs on the device only under mode = "on" (or gpu = "on" / "mixed"), never under auto: on a GTX 1080 every device XC build took 2.2-3x the CPU time and whole PBE SCFs ran about 2x slower, so auto keeps it on the CPU rather than slowing every DFT run on a GPU box.
Device memory is a separate pool ([gpu] memory_gb); the host [memory] budget_gb is not reduced by it. A contraction that does not fit the device pool runs on the CPU and is counted, not refused. FERRIC_GPU_TRACE=1 prints each fallback.
[precision] mode = "mixed" (or its deprecated alias [gpu] precision = "mixed") changes the numbers as well as the speed: the audit line, ferric.gpu_status()["precision"] and each result's precision provenance record (run_end.extra.precision in the run log, result.precision in Python) say which precision a run used, and the per-kernel error map is on the validation page. The one shipped mixed kernel is rimp2-energy (f32-resident B_ov; closed-shell and unrestricted RI-MP2 under the Coulomb, erfc and terfc operators); its measured energy errors are 3e-11 to 7e-9 Eh closed-shell Coulomb (total, with the OS and SS parts up to 5e-9 Eh each), 5e-10 to 1.4e-8 Eh unrestricted Coulomb (total, with the αα, ββ and αβ blocks up to 1.1e-8 Eh each) and 7e-11 to 6e-9 Eh for erfc and terfc on the systems listed there, graded as a measured error map, not Proven; a mixed run prints precision mixed in its [ferric] gpu: line, and its energies are not interchangeable with f64 results in a convergence or reference comparison. A tiny problem still pays the device upload when the mode is on, whatever the precision. gpu = "mixed" (or [gpu] preset = "mixed") is mode = "on" plus precision = "mixed", so it carries the same change in the numbers; a mode or precision setting that contradicts the preset is an error, not an override. A mixed_kernels list given next to a preset is kept as given instead of the preset's set (it may narrow it); a name this build does not ship is refused, and the opt-in xc-vxc and dfk-occ are admitted only when the list names them and mode resolves to "on" (so gpu = "mixed" with the list works, auto-mixed with it is an error).
Implemented is not validated
A method listed on these pages exists and runs. How closely its numbers have been checked against an independent reference differs a lot from method to method. Look up the grade in Capabilities and validation before relying on a number.
ferric recipes
Copy-paste workflows that end in a number. Each gives its expected output and rough runtime, so you can tell "still running" from "silently wrong".
Outputs for recipes 1 and 3 come from running them as written (2026-09-23); recipe 2's output is from an earlier run. MEASURED marks numbers that came out of the program.
Prerequisite: a working ferric. The fast path is the prebuilt wheel
(about a minute). A source build takes about 10 minutes, mostly compiling ferric:
pip install ferric
See Installation. Build from source only if you are changing ferric itself or need MPI.
Which recipes need a git clone: the wheel contains the compiled library and
the ferric command, nothing else. examples/, testdata/ and scripts/
live in the repository
(what the wheel does not contain).
Run those recipes from the repository root:
| Recipe | Needs a clone? | Extra dependencies |
|---|---|---|
| 1. Single point | only for examples/water-rhf.toml; your own .xyz + TOML works anywhere | — |
| 2. Ions and radicals | no | — |
| 3. Optimize | only for examples/h2-lda-opt.toml | — |
On a shared machine, run anything real under a memory cap. ferric's
memory budget charges only the large, size-dependent tensors, so a job's
resident memory can exceed it. Only a cgroup puts a hard ceiling on the
process; without one, an overshoot can trigger the system-wide OOM killer,
which may kill unrelated processes. Details are in
Sharp bits
and For agents.
scripts/ferric-limited (repository, Linux with systemd) wraps a command in a
systemd-run --user scope:
scripts/ferric-limited --max=8G --high=7G -- ferric input.toml
1. Single-point energy from a TOML file
The first success that confirms your install works. From the repository root:
ferric examples/water-rhf.toml
MEASURED (about 2 s including start-up):
RHF/sto-3g on testdata/molecules/water.xyz
nbasis = 7
iterations = 9
converged = true
energy = -74.9631468000 Hartree
From a source checkout, the same run is
cargo run --release --bin ferric -- examples/water-rhf.toml.
For your own molecule you need an .xyz (Å) and a TOML file:
[molecule]
xyz = "mymol.xyz" # relative to the directory you run ferric from
[basis]
name = "def2-svp"
[method]
kind = "rhf"
For every method.kind, see Capabilities and validation. For
every TOML key, see the input reference.
2. Charged and open-shell species
Read this before running any ion, radical or metal center. charge and
multiplicity are [molecule] keys. Most files in examples/ leave them at
their defaults (0 and 1). The open-shell exceptions are examples/h_uhf.toml
and examples/oh-ugw.toml.
[molecule]
xyz = "tBu.xyz"
charge = 1 # cation
multiplicity = 1 # closed-shell singlet
[basis]
name = "def2-svp"
[method]
kind = "ksdft"
[dft]
functional = "B3LYP"
Relax the geometry with xtb first. A correct formula doesn't guarantee a
physical structure, and the parity check below can't tell the difference.
MEASURED: a hand-built C10H17+ with a 0.902 Å C–H contact stalled the SCF.
After xtb mol.xyz --opt --chrg 1 --uhf 0 --gfn 2 it converged, 545 kcal/mol
lower. See Golden paths Step 1b.
MEASURED on the tert-butyl cation C4H9+ (13 atoms), B3LYP/def2-SVP:
converged = true
energy = -157.4301362681 Hartree
Expect seconds to about a minute at this size.
The error you will hit first
If the electron count and the multiplicity disagree, you get this error:
error: inconsistent charge/multiplicity: 35 electrons with multiplicity 1
implies n_alpha = (35 + 1 - 1) / 2 = 35/2, which is not a non-negative
integer... An odd electron count needs an even multiplicity (2, 4, ...)
and vice versa
It means your geometry, your charge or your multiplicity is wrong. ferric
can handle ions and open shells. The message prints the arithmetic and the
rule, so check the atom count in your .xyz against the first line of the
file, then the charge and multiplicity you set.
A radical (open-shell doublet) uses the same keys:
[molecule]
xyz = "radical.xyz"
charge = 0
multiplicity = 2 # one unpaired electron -> UHF/UKS
From Python, multiplicity belongs to the molecule, not to the run_* call:
ferric.Molecule.from_xyz("x.xyz", charge=0, multiplicity=2).
3. Optimize a geometry
ferric examples/h2-lda-opt.toml
MEASURED (about 2 s): converged = true, steps = 2,
final E = -1.1212649781 Hartree.
The pattern is task = "optimize" next to any kind that has analytic
gradients:
[method]
kind = "ksdft"
task = "optimize"
[dft]
functional = "B3LYP"
[optimize]
max_steps = 30
Capabilities and validation lists which methods have analytic
gradients. Harmonic frequencies use task = "frequencies" (the analytic Hessian
where it applies, otherwise finite differences of the analytic gradient; see examples/water-frequencies.toml).
Runtime depends on the system, so measure before you plan. A whole optimization at drug-like size has not been timed on this page. For scale, two single-point measurements:
- 32 atoms, PBE/def2-SVP: 96 s.
- A 27-atom delocalised cation, PBE/6-31G: 4.9 min, because it needed 173 SCF iterations. At B3LYP/def2-SVP the same molecule was killed for memory after 22 min (Golden paths, Step 2).
An optimization multiplies the single-point cost by the number of steps. Measure one species before you queue many.
4. Comparing against another code
Three causes account for most spurious "ferric disagrees" reports:
Density fitting. ferric's KS-DFT (kind = "ksdft", run_dft) fits the
Coulomb term by default, and the fitting error grows with system size. MEASURED
at PBE/STO-3G against exact Coulomb: water 0.28, benzene 1.16, and a 71-atom
drug molecule 9.5 kcal/mol. Compare like with like. run_dft(..., df_j_aux="exact") turns fitting off, or you can turn it on in the other code.
Grid. ferric's KS-DFT default grid is (75, 110) (radial, angular), flat,
with no pruning. PySCF's default grid.level=3 is roughly (75, 302). The
difference is worth ~1e-5 Ha on water and grows with atom count, because
grid error scales with the number of atoms, not the basis size. A small basis
on a big molecule can be worse than a big basis on a small one. Match grids
before you conclude anything.
Convergence criteria. Setting energy_conv alone can leave the density
loosely converged. Variational quantities (E_HF) don't mind. Anything that
depends linearly on the MO coefficients (correlation energies, properties) does.
density_conv is the criterion that converges the density, so
tighten it when you care about those quantities. Keep energy_conv loose: with
density fitting, a very tight energy_conv may be unreachable
(Golden paths, Step 2).
[scf]
density_conv = 1e-9
Choosing a method
Start from what you want to compute. Each row names a method that ferric
implements for that task, where to run it (CLI method.kind or Python only),
how far it is validated, and a shipped example to copy. Grades, and the anchors behind them, are on
Capabilities and validation. Cost is given as formal scaling
with system size N, not as timings.
This page only recommends what the code and its tests support. Where ferric has no validated option for a task, the row says so.
By task
| Task | Recommended | Where | Grade | Scaling | Example |
|---|---|---|---|---|---|
| Geometry optimization | KS-DFT (e.g. PBE, B3LYP) or RHF, task = "optimize" | CLI, Python (run_optimize is RHF) | Proven | N⁴ | h2-lda-opt.toml, h2_opt.toml |
| Harmonic frequencies | KS-DFT or RHF/UHF/ROHF, task = "frequencies" (analytic Hessian for RHF/UHF/ROHF with exact J/K, and for RHF, UHF and ROHF with RI-J and/or RI-K; the analytic moving-grid Hessian for closed-shell KS LDA/GGA, pure or global hybrid; both under the default hessian = "auto"; otherwise finite differences of the analytic gradient, 6N gradients) | CLI, Python run_frequencies | energies Proven; check the printed Hessian asymmetry | 6N × N⁴ | water-frequencies.toml |
| Transition state | run_saddle (P-RFO), then run_irc to confirm which minima it connects | Python only, closed shell | see Capabilities and validation | ~2(6N+1) gradients + steps | — |
| Conformer or reaction energies, routine | KS-DFT + D3(BJ) | CLI ksdft + [dft] dispersion = "d3bj", Python run_dft(dispersion=...) | DFT Proven; D3(BJ) (with or without ATM) matches simple-dftd3 to <1e-12 Ha | N⁴ (DFT) | water-pbe-d3bj.toml |
| Correlated energies, small to medium | RI-MP2 | CLI rimp2, Python run_rimp2 | Proven | N⁵ | water-rimp2.toml |
| Correlated energies, benchmark quality, small | CCSD(T) (closed shell) | Python run_ccsd_t only; CCSD also CLI ccsd | CCSD Proven; (T) matches PySCF ~1e-6 Ha on H2O/cc-pVDZ | N⁶ / N⁷ | water-ccsd.toml (H2) |
| Non-covalent interaction energies | Attenuated MP2 in the basis its parameters were fitted for: SCS-MP2(2terfc) or MP2-V in aug-cc-pVTZ, erfc att-MP2 in aug-cc-pVDZ (the basis of the 2012 paper); or KS-DFT + D3(BJ) | CLI scs-mp2-2terfc, mp2-v, att-rimp2, ksdft | att-MP2, 2terfc and MP2-V Proven (narrow) as implementations; the MP2-V parameterization is unbenchmarked | N⁵ | water-scs-mp2-2terfc.toml, water-mp2v.toml, water-attmp2.toml |
| Long-range correlation from response | RS-MP2 + LR-RPA, or PDEP-RPA | CLI rs-mp2-rpa, pdep-rpa | RS-MP2+RPA Smoke; PDEP-RPA Proven | N⁵ (MP2 part); N⁴ (RI-RPA) | water-rs-mp2-rpa.toml, water-pdep-rpa.toml |
| Ionization potentials / electron affinities | G0W0@PBE (or @HF); U-GW for open shells | CLI gw, Python run_gw, run_u_gw | Smoke, about ±0.3 eV | — | water-g0w0-pbe.toml, oh-ugw.toml |
| Excitation energies | BSE-TDA on G0W0@HF; or TDA/TDDFT (CIS/TDHF on an HF reference) | CLI bse-tda, tda, tddft; Python run_bse_tda, run_tddft | BSE-TDA Smoke; TDA/TDDFT Proven (narrow, closed shell) | — | water-bse-tda.toml, water-tda.toml |
| Static polarizability | PDEP-RPA properties, or RPAx@KS (static α only) | CLI pdep-rpa with compute_polarizability, tdhf-static-polarizability | PDEP-RPA Proven (energy); RPAx Smoke | N⁴ (RI) | h2o-pdep-rpa-props.toml, water-tdhf-static-alpha.toml |
| \( C_6 \) dispersion coefficients | PDEP-RPA dynamic α (c6_source = "pdep") or TS/MBD, in an augmented basis. Not RPAx: its \( C_6 \) is ~63% low | CLI pdep-rpa + [rpa] compute_c6 | which source is better is not established | N⁴ (RI) | water-c6-pdep.toml, argon-c6-rpa-pbe.toml |
| Implicit solvation | IEF-PCM (CLI [pcm], Python solvent=) or COSMO (CLI [cosmo]) | see SCF and DFT | both cross-checked against PySCF on water | SCF cost | — |
| Embedding in a protein or solvent | QM/MM | Python run_qmmm; CLI [qmmm] (point charges) | see QM/MM | SCF cost | water-qmmm.toml |
| Electron-transfer coupling | constrained DFT + Wu–Van Voorhis \( H_{ab} \) | Python run_cdft, cdft_coupling (no CLI) | no external reference | SCF cost × outer λ loop | — |
| Atomic charges, ESP | Löwdin, Hirshfeld, CHELPG, RESP; ESP at nuclei or on the surface | Python | see Capabilities and validation | SCF cost | — |
Notes on the table:
- "Proven" for an attenuated MP2 method means the implementation reproduces
its reference, not that the method is accurate on your system. Its
parameters are fitted to interaction-energy benchmarks in one basis (the
aug-cc-pVTZ sets without counterpoise, with frozen core); in another basis,
with counterpoise correction, or with
frozen_core = 0(the default), you are extrapolating. See The MP2 family. - KS-DFT uses density fitting by default (
def2-universal-jkfit), and the fitting error grows with system size. Turn it off or match it before comparing with an exact-Coulomb code; see SCF and DFT. - CC results are sensitive to the aux basis: at CC accuracy the RI error depends on the auxiliary set. See Coupled cluster.
Pairing an orbital basis with its auxiliary basis
Every correlated method needs an RI auxiliary basis ([mp2] auxbasis or
[rpa] auxbasis; the auxbasis argument in Python). Use the set built for
your orbital basis. The bundled pairs are:
| Orbital basis | RI (correlation) aux | JK aux (for df_j_aux / df_k_aux) |
|---|---|---|
| cc-pVDZ / TZ / QZ | cc-pvdz-rifit (alias cc-pvdz-ri), cc-pvtz-rifit, cc-pvqz-rifit | def2-universal-jkfit; cc-pvtz-jkfit / cc-pvqz-jkfit (H, B-F, Al-Cl, Ga-Br only) |
| aug-cc-pVDZ / TZ / QZ | aug-cc-pvdz-rifit, aug-cc-pvtz-rifit, aug-cc-pvqz-rifit | aug-cc-pvdz-jkfit, aug-cc-pvtz-jkfit, aug-cc-pvqz-jkfit (H, B-Ne, Al-Ar, Ga-Kr; Ne/Ar/Kr extrapolated), else def2-universal-jkfit |
| cc-pVXZ-PP, aug-cc-pVXZ-PP | <orbital>-pp-rifit (H-Ar, without Li and Na in the aug sets; Sc-Ni at TZ and QZ only; Y-Pd; Hf-Pt. K-Ni at DZ, K and Ca at TZ/QZ, Cu-Sr, Ag-Lu and Au-Rn error) | def2-universal-jkfit |
| def2-SVP(D), TZVP(P)(D), QZVP(P)(D) | def2-<x>-rifit for each | def2-universal-jkfit |
Each RIFIT set in the table also loads under ORCA's <orbital>/c name (for
example def2-tzvp/c), each JKFIT set under <orbital>/jk (for example
aug-cc-pvtz/jk), and def2-universal-jkfit as def2/jk. STO-3G, the
Pople sets and the pcseg family have no matching RI set; pair them with a
def2 or cc RIFIT set of similar quality. Any other orbital set goes in
through [basis] path as a Gaussian-94 file (orbital basis only).
def2-universal-jkfit is the default JK set for KS-DFT.
When nothing here fits
- Analytic Hessians cover RHF, UHF and ROHF (exact J/K, or RI-J / RI-K with no external potential; an ECP is fine; basis up to f functions; RHF and UHF with exact J/K also allow an external potential, ROHF allows none on any route), and closed-shell KS LDA/GGA (pure or global hybrid, moving-grid Hessian). Other KS-DFT (UKS, ROKS, meta-GGA, range-separated hybrids) and correlated methods' frequencies are by finite differences of the analytic gradient.
- Open-shell coupled cluster and open-shell TS search are not available
(the one open-shell CC-type method, LinLCCD(hh), is library-only). Open-shell
KS-DFT is available from the CLI:
ksdftwithmultiplicity > 1runs UKS, anduhf/rohfwith[dft] functionalrun UKS/ROKS. See Capabilities and validation: open shells in the CLI. - TDDFT with the XC kernel exists only as a library-only spike; the CLI and Python TDDFT omit it. See RPA, GW and excited states.
For PySCF users
ferric's Python API will look familiar if you use PySCF, but it is built differently. This page maps the PySCF calls you already know onto ferric's, then lists the differences that change numbers or cause errors.
The table only lists pairs where both sides exist. If a PySCF feature has no row, assume ferric does not have it; check the full function reference to be sure. The PySCF names were checked against PySCF 2.12.1.
Rosetta table
Assume import ferric and, on the PySCF side, the matching from pyscf import ...
(gto, scf, dft, mp, cc, tdscf, gw, lo, df, qmmm, solvent,
geomopt, hessian).
Molecule and basis
| PySCF | ferric |
|---|---|
mol = gto.M(atom=..., basis="cc-pvdz", charge=0, spin=2) | mol = ferric.Molecule.from_xyz_string(xyz, charge=0, multiplicity=3) and bs = ferric.BasisSet.bundled("cc-pvdz") |
mol.energy_nuc() | mol.nuclear_repulsion() |
mol.nelectron | mol.nelec() |
mol.natm | mol.natoms() |
mol.atom_coords() (Bohr) | mol.coords_bohr() |
mol.atom_coords(unit="Angstrom") | mol.coords() |
mol.elements | mol.symbols() |
mol.atom_charges() | mol.atomic_numbers() (true Z, not reduced by an ECP) |
ghost atom "ghost-H" or "X-H" | @H in the XYZ text |
SCF and DFT
| PySCF | ferric |
|---|---|
mf = scf.RHF(mol).run() | r = ferric.run_rhf(mol, bs) |
scf.UHF(mol).run() | ferric.run_uhf(mol, bs) (spin from mol) |
scf.ROHF(mol).run() | ferric.run_rohf(mol, bs) |
scf.RHF(mol).density_fit(auxbasis="def2-universal-jkfit") | ferric.run_rhf(mol, bs, df_j_aux="def2-universal-jkfit", df_k_aux="def2-universal-jkfit") |
mf = dft.RKS(mol); mf.xc = "b3lyp"; mf.run() | ferric.run_dft(mol, bs, functional="b3lyp") (closed-shell only) |
mf.disp = "d3bj" | ferric.run_dft(..., dispersion="d3bj"), or ferric.d3bj_energy(mol, "b3lyp") alone |
mf.e_tot | r.energy (RHF/UHF/ROHF) or r.total_energy (DFT) |
mf.converged | r.converged |
mf.mo_energy | r.orbital_energies() (RHF), r.orbital_energies_alpha() / _beta() (UHF/ROHF) |
mf.mo_coeff | r.mo_coefficients() (RHF only) |
mf.make_rdm1() | r.density() (RHF, DFT), r.density_alpha() / r.density_beta() (UHF/ROHF) |
mf.level_shift = 0.2 | run_rhf(..., level_shift=0.2) |
mf = solvent.PCM(scf.RHF(mol)); mf.with_solvent.eps = 78.4 | ferric.run_rhf(mol, bs, solvent=78.4) or solvent="water" |
qmmm.mm_charge(mf, coords, charges) | run_rhf(..., point_charges=[(q, x, y, z), ...]), coordinates in Bohr |
Geometry and vibrations
| PySCF | ferric |
|---|---|
geomopt.geometric_solver.optimize(mf) | ferric.run_optimize(mol, "cc-pvdz") (RHF; basis by name) |
mf.Hessian().kernel() then thermo.harmonic_analysis(mol, h) | ferric.run_frequencies(mol, "cc-pvdz") |
Correlation, response and properties
| PySCF | ferric |
|---|---|
mp.MP2(mf).density_fit(auxbasis="cc-pvdz-ri").run() | ferric.run_rimp2(mol, bs, ferric.BasisSet.bundled("cc-pvdz-ri")) |
cc.CCSD(mf).density_fit(auxbasis=...).run() | ferric.run_ccsd(mol, bs, aux) |
mycc.ccsd_t() | ferric.run_ccsd_t(mol, bs, aux).t_correction |
tdscf.TDA(mf).run() | ferric.run_tddft(mol, bs, aux, method="tda") |
tdscf.TDHF(mf).run() / tdscf.TDDFT(mf).run() | ferric.run_tddft(mol, bs, aux, method="casida") |
gw.GW(mf).kernel() | ferric.run_gw(mol, bs, aux) |
lo.Boys(mol, mo).kernel() | ferric.boys_localize(mol, bs, mo).c_loc() |
mf.mulliken_pop() | ferric.mulliken_charges(mol, bs, r) |
df.incore.aux_e2(mol, auxbasis) | ferric.compute_eri3(mol, bs, aux) |
df.incore.fill_2c2e(mol, auxbasis) | ferric.compute_metric_2c(mol, bs, aux) |
Integrals and building blocks
With ints = ferric.AoIntegrals(mol, bs) (see Building blocks).
AO matrices follow libint2's basis-function conventions, not PySCF's, so raw AO matrices differ between the two codes; energies and other invariant quantities agree.
| PySCF | ferric |
|---|---|
mol.intor("int1e_ovlp"), "int1e_kin", "int1e_nuc" | ints.overlap(), ints.kinetic(), ints.nuclear() |
mf.get_hcore() | ints.hcore() |
mol.intor("int1e_r") | ints.dipole() |
mol.intor("int2e") | ints.eri() |
mf.get_jk(mol, dm) | ints.jk(dm) |
scf.RHF(mol).density_fit(auxbasis=...).get_jk(mol, dm) | ints.df_jk(aux).jk(dm) |
ao2mo.general(mol, (c1, c2, c3, c4), compact=False) | ints.ao2mo(c1, c2, c3, c4) (4-index, not pair-packed) |
lib.einsum(spec, a, b) | ferric.einsum(spec, a, b) (two operands) |
How ferric differs
Stateless calls, not mutable method objects. PySCF builds an mf
object, lets you set attributes on it, then runs it. ferric has one function
per calculation. Settings are keyword arguments, and the call returns a result
object. There is nothing to reconfigure and rerun; call the function again.
Correlated drivers run their own reference SCF, so you pass them the molecule
and basis, not a converged mf.
Spin is multiplicity, and it lives on the molecule. PySCF's spin is
2S; ferric's multiplicity is 2S+1. It is set when the molecule is built
(from_xyz_string(..., multiplicity=3)). run_uhf and run_rohf have no spin
keyword.
The auxiliary basis is always explicit. PySCF picks an auxiliary basis for
you when you call .density_fit() without one. ferric's MP2, CC, RPA, GW and
TDDFT drivers take the RI basis as a required argument. Only SCF-level
fitting has a default: run_dft and run_ksdft fit J (and K for hybrids)
with def2-universal-jkfit unless you pass df_j_aux/df_k_aux. These fit
their reference SCF with def2-universal-jkfit and take no override:
run_qmmm with a KS method, run_gw and run_u_gw with xc, run_tddft
with a functional, run_tdhf_static_polarizability, run_double_hybrid, and
run_rs_mp2_rpa (its HF reference).
Density fitting is on in run_dft and off in run_rhf. run_dft uses
RI-J by default, and RI-K for hybrids. dft.RKS in PySCF uses exact Coulomb
unless you call .density_fit(). run_rhf builds exact four-centre J and K
unless you pass df_j_aux/df_k_aux. The difference between a fitted and an
exact energy is the fitting error, not a bug; the ferric source records it at
PBE/STO-3G against conventional J as 0.28 (water), 1.16 (benzene) and
9.5 kcal/mol (a 71-atom drug molecule). Pass df_j_aux="exact" to run_dft
when you compare against exact-Coulomb PySCF.
The DFT grid is smaller and not pruned. ferric's default is 75 radial ×
110 Lebedev angular points on every atom, with no pruning. PySCF's default
(grids.level = 3) uses 302 angular points for H through Ne and 434 from Na
on, with 50 radial points for H and 75 for C–Ne, and prunes with
nwchem_prune. Expect grid-level differences in DFT energies.
Correlated methods use RI integrals throughout. ferric's CCSD and CCSD(T)
build their integrals from the auxiliary basis you pass. The fair PySCF
comparison is cc.CCSD(mf).density_fit(...), not plain cc.CCSD(mf), which
uses exact four-index integrals.
Range-separation ω is in Å⁻¹ in the run_* drivers. omega on
run_attenuated_rimp2 and run_rs_mp2_rpa is in Å⁻¹ (default 0.420). PySCF,
and ferric's low-level compute_eri3_mo and compute_metric_2c, take Bohr⁻¹.
Point charges are in Bohr; QmmmSystem coordinates are in Å.
point_charges= on run_rhf and the other SCF drivers takes Bohr. PySCF's
qmmm.mm_charge defaults to mol.unit, which is Ångström unless you changed
it. ferric.QmmmSystem takes Ångström coordinates, and its point_charges()
accessor returns Bohr, ready to pass to run_rhf.
Only some drivers raise on an unconverged SCF. PySCF's kernel() returns the energy
either way. ferric's run_rhf, run_uhf, run_rohf and run_qmmm do the
same, so check .converged. run_dft and every correlated driver raise
instead when their SCF does not converge.
TDDFT refuses some functionals. ferric's run_tddft includes the f_xc
response and matches PySCF TDA/TDDFT to 1e-3 eV for HF, LDA, PBE and
B3LYP on the tested systems. It refuses meta-GGA, VV10 and range-separated functionals, which
PySCF accepts, and it runs closed-shell references only.
GW defaults to a Hartree–Fock reference. PySCF's gw.GW(mf) runs on
whatever mf you pass, usually a DFT one. ferric.run_gw runs its own HF
reference unless you pass xc="pbe" or another functional.
The PCM defaults differ. ferric's solvent= is IEF-PCM with 110
tesserae per atomic sphere (pcm_lebedev_order=110). PySCF's PCM defaults
to C-PCM with Lebedev order 29 (302 points per sphere) and SWIG
discretization. The named solvent "water" is ε = 78.4 in ferric;
PySCF's default ε is 78.3553.
AO matrices do not match element by element. ferric's AO basis-function
conventions come from libint2 and are not the same as PySCF's (libcint).
Measured on CO/cc-pVDZ: RHF total energies agree to 3e-12 Ha and orbital
energies to 8e-9 Ha, while the two AO density matrices differ element by
element by up to 1.5. Compare invariant quantities (energies, orbital
energies, charges), not raw AO matrices. Axis order differs too:
compute_eri3 returns (naux, n_bf, n_bf), with the auxiliary index first.
Basis sets come from a fixed bundled list. BasisSet.bundled(name) knows
64 sets plus aliases (see Bundled basis sets).
BasisSet.from_bse_json(path) loads a Basis Set Exchange JSON file; there is
no per-element basis dictionary.
The Pople sets use Gaussian's d convention, not PySCF's default. PySCF
builds every molecule spherical (5D) unless you pass cart=True, and the flag
covers the whole molecule. ferric types each shell:
| Basis | ferric | Matching PySCF call |
|---|---|---|
| 6-31G, 6-31G*, 6-31G**, 6-31+G*, 6-31+G**, 6-31++G** | Cartesian d (6D) | gto.M(..., cart=True) |
| 6-311G**, 6-311++G** | spherical d (5D) on every element | the default (cart=False) |
| every other bundled set | spherical, as the Basis Set Exchange types it | the default |
The 6D/5D difference is not small: water/6-31G* differs by about 1.4 mHa
between the two. For the 6-311G family ferric departs from the Basis Set
Exchange, which marks the Na-Ar d shells Cartesian; ferric makes them
spherical like every other element, so a molecule such as CH3Cl is 5D
throughout and matches PySCF's default. The f shells the 6-31G* family
carries on Sc-Zn are spherical (7F), as in Gaussian; cart=True would make
them Cartesian too, so PySCF's single flag cannot reproduce 6-31G* on Sc-Zn.
Results come back as numpy arrays or lists, not live objects. Matrices are
numpy.ndarray. Per-atom quantities such as charges are Python lists.
QM/MM
Run a quantum region inside a classical environment: a ligand in a protein pocket, a reacting site in an enzyme, a solute in explicit solvent. The QM atoms get a real wavefunction; everything else becomes point charges that polarize it.
What you get: QM region selection (by index, by radius, or by whole residue), link atoms across covalent cuts with four boundary-charge schemes, Gaussian-smeared charges, Thole polarizable embedding, an AMBER-form MM crate, analytic QM and MM forces, a full-structure gradient across the cut, geometry optimization.
What is missing: no AMBER prmtop reader (go through OpenMM), and no
periodic boundary conditions.
Before you start: relax the geometry with xtb
DFT on an unrelaxed structure wastes hours and often fails.
Measured on a 27-atom carbocation built by hand:
hand-built C7-H24 = 0.902 Å SCF: Stalled@37, never converged
xtb GFN2 C7-H24 = 1.084 Å SCF: converged
545 kcal/mol lower
An SCF has no reason to converge on a structure with a 0.9 Å C–H bond. xtb fixes it in seconds; DFT spends hours failing on it.
xtb input.xyz --opt --gfn 2 --chrg 1 # seconds
ferric single_point.toml # then the expensive step
Use it for the initial geometry of anything you did not get from a crystal structure or a previous optimization: hand-built ligands, docked poses, edited residues, anything from a SMILES string.
Build note: xtb built with
-O3miscompiles gradients on this platform. Build with-Doptimization=2.
The smallest working example
import ferric
# The full structure: every atom, QM and MM alike.
symbols = ["O", "H", "H", "O", "H", "H"] # two waters
coords = [[0.0, 0.0, 0.0], [0.76, 0.59, 0.0], [-0.76, 0.59, 0.0],
[0.0, 0.0, 3.0], [0.76, 0.59, 3.0], [-0.76, 0.59, 3.0]]
charges = [-0.834, 0.417, 0.417, -0.834, 0.417, 0.417] # MM partial charges (e)
# QM = the first water; the second becomes point charges.
sys = ferric.QmmmSystem(symbols, coords, charges, qm_indices=[0, 1, 2])
res = ferric.run_qmmm(sys, "cc-pvdz", method="rhf")
print(res.energy)
charges are ignored for atoms that end up in the QM region, so you can pass a
full force-field charge set and let the selection decide.
Selecting the QM region
Three ways:
# 1. Explicit indices — full control.
ferric.QmmmSystem(symbols, coords, charges, qm_indices=[0, 1, 2])
# 2. Everything within a radius of some seed atoms.
ferric.QmmmSystem(symbols, coords, charges,
qm_seeds=[0], qm_radius_angstrom=5.0)
# 3. Whole residues — a residue joins if any of its atoms is in range.
# Prefer this for proteins: a radius cut alone will slice through a
# residue and leave you with a chemically meaningless fragment.
ferric.QmmmSystem(symbols, coords, charges,
qm_seeds=[0], qm_radius_angstrom=5.0,
residue_ids=residue_ids)
residue_ids together with qm_indices is an error: an explicit index list
and a residue expansion are contradictory instructions.
Cutting a covalent bond
If the QM/MM boundary crosses a bond, you need a link atom and a scheme for what to do with the host's charge:
sys = (ferric.QmmmSystem(symbols, coords, charges, qm_indices=qm)
.with_link_atoms(bonds) # bonds crossing the cut
.with_boundary_charges(bonds, "rcd")) # keep | delete-host | rc | rcd
| scheme | what it does |
|---|---|
keep | leave the host charge in place |
delete-host | Z1 — delete it |
rc | Lin–Truhlar redistributed charge |
rcd | redistributed charge and dipole |
The parser is strict: an unknown string is an error.
Energies, forces, and optimization
res = ferric.run_qmmm(sys, "cc-pvdz", method="uhf")
# KS-DFT: method="rks" or "uks" and xc, e.g. method="rks", xc="PBE".
# xc without rks/uks (or rks/uks without xc) raises ValueError.
res.energy
res.qm_gradient() # dE/dR on the QM atoms
res.mm_forces() # forces on the MM sites
res.full_gradient() # across the cut, link rows folded back onto the frontier
opt = ferric.run_optimize_qmmm(sys, "cc-pvdz", method="rhf", move_mm="none")
move_mm chooses which MM atoms relax: "none" (the default, where only QM
atoms move), ("within", radius_angstrom), ("residues", [ids]) (the system
must have been built with residue_ids=), or "all". Any value other than
"none" requires mm_topology=: without a force field, nothing holds the
moving MM atoms' geometry together.
Polarizable embedding
Thole polarizable sites, if fixed charges are not enough:
sys = ferric.QmmmSystem(symbols, coords, charges,
qm_indices=qm,
polarizabilities_angstrom3=alphas)
res = ferric.run_qmmm(sys, "cc-pvdz", method="rhf", thole_a=2.1304)
res.e_pol # the polarization energy
res.induced_dipoles()
What is validated, and how far
Checked against pyscf.qmmm.mm_charge: the energy shift agrees to <1e-8 Ha,
MM forces to 3e-10. An empty or all-zero MM region is bit-identical to a gas
phase calculation.
The QM–MM Lennard-Jones term across a covalent cut is checked against OpenMM's own exclusion and 1-4 handling (ethanol cut at C–C, realistic LJ): energy and gradient agree to ~1e-18 Ha. With no bond path between the regions it is bit-identical to the plain every-pair sum.
Not validated: periodic boundary conditions (absent).
From the CLI
A [qmmm] section runs QM/MM from a TOML file, with no Python needed. The QM
region becomes the molecule that is solved, and the MM region becomes the
external potential it is solved in. A complete, runnable input is
examples/water-qmmm.toml (repository, not the wheel):
[molecule]
xyz = "testdata/molecules/water.xyz" # required by the parser, not read with [qmmm]
charge = 0 # apply to the QM region
multiplicity = 1
[basis]
name = "sto-3g"
[method]
kind = "rhf"
task = "energy"
[qmmm]
pqr = "testdata/molecules/water_na.pqr"
qm_indices = [0, 1, 2] # zero-based; or: qm_seeds = [0], qm_radius_angstrom = 1.5
# link_bonds = [[0, 3]] # required when the cut crosses a covalent bond
# boundary_scheme = "delete-host" # default; also "keep", "rc", "rcd"
Measured on that file (water QM, one Na⁺ 4 Å away as MM, STO-3G): vacuum
−74.9629466809, embedded −74.9653197421 Ha (−1.489 kcal/mol). That matches
ferric.run_rhf(point_charges=...) to all ten printed digits.
The geometry and the MM charges both come from the PQR. An xyz has no
partial charges, and an MM region without charges contributes nothing. [molecule] xyz must still be present, because the parser
requires it, but it is not read when [qmmm] is present. That matters when
you compute a vacuum reference: delete [qmmm] and ferric falls back to the
xyz. If the xyz holds a different geometry, you are comparing two different
molecules. In the example above, the xyz is an optimized water that gives
−74.9631468000, which is 0.13 kcal/mol of error in the difference. Compare at
one geometry.
boundary_scheme defaults to "delete-host", not "keep". Keeping the
host charge across a covalent cut puts a bare point charge inside the link
atom's bond length (measured 0.443 Å on an ethane C–C cut). A geometry
optimization in that field diverges instead of failing with an error. "keep"
is still available and is the right choice when the cut isn't covalent. An
unknown scheme name is an error.
[qmmm] and [external_potential] together are refused. The MM region
is an external potential, so combining them would count a contribution twice.
Scope of the CLI section: it reads only a PQR, because it needs charges and geometry together, and does electrostatic embedding only. Polarizable sites, smeared charges, the MM force field and MM relaxation are Python-only.
Known limits
- No AMBER
prmtopreader. Go through OpenMM, or build the arrays. - No periodic boundary conditions.
- The pocket field is fixed unless you ask otherwise.
move_mm="none"is the default inrun_optimize_qmmm; the MM sites do not relax with the QM region until you widen it. - QM/MM dispersion is the force field's Lennard-Jones only. D3/D4/XDM/VV10
are QM-atom-pairwise and never see the MM atoms. With
mm_topology=set, the MM force field supplies Lennard-Jones between every QM real atom and every MM atom (link atoms and boundary charges carry none). Without it there is no QM–MM van der Waals term at all. - QM–MM Lennard-Jones follows the force field's exclusion rules across the
cut. Pairs 1-2 or 1-3 apart through the topology's bond list (the cut bond
included) get no LJ, and 1-4 pairs are scaled by the topology's
scale_lj_14(AMBER default 0.5). The MM–MM terms use the same bond graph, so the cut bond has to be in the topology's bonds. QM–MM electrostatics do not use these exclusions orscale_coul_14. The embedding puts every MM charge in the QM Hamiltonian, and the boundary scheme (keep/delete-host/rc/rcd) handles the charges next to the cut. - The MM crate is AMBER-form (harmonic bonds and angles, periodic torsions, Lennard-Jones, Coulomb), validated against OpenMM.
Export: cube files and NPZ bundles
ferric writes volumetric data as Gaussian cube files and per-molecule
property bundles as compressed NumPy .npz archives. The Python functions
evaluate basis functions, the density and orbitals at arbitrary points; the
CLI [rpa] section writes the NPZ bundle and PDEP eigenpotential cubes. The
array schema is in crates/ferric-export/theory.md.
Units
| Quantity | Unit |
|---|---|
| Points, origins, spacings, padding, cube grids | Bohr |
| Density | e/Bohr³ |
| Orbitals and AO values | Bohr^(-3/2) |
NPZ coords | Bohr |
| NPZ energies, ESP, field | Hartree, Hartree/e, a.u. |
| NPZ polarizabilities | Bohr³ |
| NPZ charges | e |
These differ from Molecule.from_xyz_string, which reads Å, and from the
periodic and QM/MM Python entry points, which take Å. The functions below
take Bohr throughout.
Cube files from Python
write_cube writes the total density or one restricted MO on an axis-aligned
grid. Without origin and shape the grid is the atoms' bounding box plus
padding. It returns the (origin, shape) used. what="mo" needs
mo_index (0-based, energy order) and a restricted RHF or RKS result.
Orbital signs are the SCF's arbitrary phase.
import ferric
water = ferric.Molecule.from_xyz_string("""3
water
O 0.000000 0.000000 0.117790
H 0.000000 0.755453 -0.471161
H 0.000000 -0.755453 -0.471161
""")
bs = ferric.BasisSet.bundled("sto-3g")
rhf = ferric.run_rhf(water, bs)
origin, shape = ferric.write_cube("density.cube", water, bs, rhf, spacing=0.3)
print(shape)
origin, shape = ferric.write_cube("homo.cube", water, bs, rhf,
what="mo", mo_index=4, spacing=0.3)
print(shape)
[28, 38, 32]
[28, 38, 32]
Values at points
eval_ao_on_points, eval_density_on_points and eval_mo_on_points take an
(N, 3) array of points in Bohr.
| Function | Returns |
|---|---|
eval_ao_on_points(mol, bs, points) | (N, nbasis) AO values, PySCF eval_gto layout |
eval_density_on_points(mol, bs, result, points) | list of N total densities (α + β) |
eval_mo_on_points(mol, bs, result, mo_indices, points) | (N, len(mo_indices)) restricted MO values |
result is an RhfResult or DftResult. eval_mo_on_points refuses
open-shell results. All three check the allocation against the memory budget
before evaluating.
import numpy as np
pts = np.array([[0.0, 0.0, 0.0], [0.0, 0.0, 1.0]]) # Bohr
print(np.round(ferric.eval_density_on_points(water, bs, rhf, pts), 4))
print(ferric.eval_ao_on_points(water, bs, pts).shape)
print(ferric.eval_mo_on_points(water, bs, rhf, [4], pts).shape)
[10.2392 0.8103]
(2, 7)
(2, 1)
NPZ bundles from the CLI
A pdep-rpa run with [rpa] export_npz = "<path>" writes the bundle. The
compute_* keys then default to true; the full key list and defaults are
in Input file (TOML).
[molecule]
xyz = "testdata/molecules/water.xyz"
[basis]
name = "cc-pvdz"
[method]
kind = "pdep-rpa"
task = "energy"
[rpa]
auxbasis = "cc-pvdz-ri"
n_quad = 20
trunc_thresh = 1e-4
export_npz = "h2o_pdep_rpa.npz"
compute_esp = true
compute_polarizability = true
A requested property that cannot be computed fails the run unless
allow_partial_npz = true. Read the archive with numpy.load:
import numpy as np
d = np.load("h2o_pdep_rpa.npz")
print(d.files)
Keys written for water/cc-pVDZ with the input above (N atoms, nao = 24):
| Keys | Shape |
|---|---|
coords, atomic_numbers | (N, 3), (N,) |
mo_coeffs, density_matrix | (nao, nao) |
orbital_energies | (nao,) |
pdep_eigenvectors | (naux, M), M retained eigenvalues |
orbital_centers, orbital_spreads | (nmo, 3), (nmo,) |
density_second_moment, dipole | (3, 3), (3,) |
esp_atoms, electric_field | (N,), (N, 3) |
esp_surface, esp_points | (npts,), (npts, 3); compute_esp_surface = true only |
alpha_tensor, alpha_ct | (3, 3) |
alpha_atomic | (N, 3, 3) |
hirshfeld_charges, lowdin_charges, mulliken_charges, chelpg_charges, resp_charges | (N,) |
c6_freqs, c6_weights | (n_quad,) |
alpha_atomic_dynamic | (N, n_quad, 3, 3) |
alpha_ct_dynamic | (n_quad, 3, 3); c6_source = "pdep" only |
c6_iso, c6_aniso | (N, N), (N, N, 3, 3) |
c6_molecular_iso | (1,) |
c6_partition, c6_source | byte strings; decode with .tobytes().decode() |
mo_coeffs, orbital_energies, orbital_centers and orbital_spreads are
written for restricted references only. c6_iso and c6_aniso are per-atom pair tensors under a partition
convention. They do not sum to the molecular C6; read c6_molecular_iso.
alpha_ct + alpha_atomic.sum(0) == alpha_tensor. The definitions of
alpha_atomic and alpha_ct are in the compute_alpha_atomic row of the
input reference.
PDEP eigenpotential cubes
Setting export_eigpot_prefix in [rpa] writes <prefix>_eigpot_NNN.cube
for the leading export_eigpot_count eigenpotentials (default 10; the count
is capped at the number retained). cube_spacing (default 0.2 Bohr) and
cube_margin (default 4.0 Bohr) set the grid.
examples/benzene-pdep-rpa-export.toml is a complete input.
Golden paths: ferric in applications
End-to-end workflows for real questions, written so an agent can execute them without reading the rest of the repo. Each one states what it answers, what it cannot, and how to tell it went wrong.
Read For agents first if you are automated. It lists the failure modes that cost the most time. Individual steps live in Recipes.
Golden path A — Reaction energetics of a cation cascade
Answers: which intermediate is the bottleneck, and which branch point controls product selectivity.
Worked example: terpene synthase carbocation cascades (geranyl → linalyl → α-terpinyl → product). The same shape applies to any closed-shell cation mechanism.
Step 0 — gate before you spend hours
pip install ferric
git clone https://github.com/mgoldey/ferric && cd ferric # examples/, testdata/, scripts/
ferric examples/water-rhf.toml # expect converged = true, energy = -74.9631468000
Don't skip this, and don't build from source first. The wheel installs in
about a minute, and a source build takes about 10 minutes. If the install is broken,
every later failure will look like a chemistry problem. The clone is needed
because the wheel doesn't ship examples/, testdata/ or scripts/.
Run the study itself under scripts/ferric-limited --max=8G --high=7G --.
These can be multi-hour jobs, and an OOM kill leaves a truncated log with no
error message.
Step 1 — build the species, and check parity
Generate starting geometries however you like; RDKit ETKDG + MMFF is fine. Use force fields for geometry seeds only, never for a reported energy.
Then, before any QM, verify electron parity for every species:
n_elec = sum(ATOMIC_NUMBER[sym] for sym in symbols) - charge
assert (n_elec % 2 == 0) == (multiplicity % 2 == 1), (
f"{name}: {n_elec} electrons cannot have multiplicity {multiplicity}"
)
A cation is charge=1, multiplicity=1. Getting this wrong is the most
common first failure. ferric catches it with a clear message, but checking in
your own loop fails faster and names the species.
Check geometry as well as parity. A structure can have a correct formula and still be physically impossible. Measured: a hand-built C10H17+ passed parity (76 electrons, singlet) while carrying a 0.902 A C-H contact, shorter than a real bond, and a hydrogen 1.030 A from a second carbon. The SCF stalled at 37 iterations, which is the correct behaviour for overlapping nuclei.
# formula check and geometry check fail on different mistakes; keep both
assert (n_elec % 2 == 0) == (multiplicity % 2 == 1) # bad stoichiometry
assert min_interatomic_distance(xyz) > 0.9 # gross overlap only (your own helper, Angstrom)
# The failure above PASSES that screen (closest contact 0.902 A): its H sat
# ~1 A from two carbons at once. Also reject any H bonded to two heavy atoms:
assert max_heavy_neighbours_of_hydrogen(xyz, cutoff=1.3) <= 1
Step 1b: relax with xtb before any DFT
xtb is the middle tier of the funnel. GFN2-xTB fixes a bad structure in seconds:
LD_LIBRARY_PATH=~/.local/lib/x86_64-linux-gnu \
xtb mol.xyz --opt --chrg 1 --uhf 0 --gfn 2
# -> xtbopt.xyz
A local install needs LD_LIBRARY_PATH. Without it, xtb fails with
libxtb.so.6: cannot open shared object file; the install itself is fine.
Measured on the same molecule, method and basis:
hand-built geometry : closest contact 0.902 A -> SCF Stalled @37, E = -389.5110690
xtb-relaxed (54 its): closest contact 1.084 A -> SCF converged, E = -390.3794234
The relaxed structure is 0.868 Ha (545 kcal/mol) lower and converges; the hand-built one does not.
Step 2 — optimize each intermediate
[molecule]
xyz = "alpha_terpinyl.xyz"
charge = 1
multiplicity = 1
[basis]
name = "6-31g" # def2-SVP did not fit on a workstation here; see below
[method]
kind = "ksdft"
task = "optimize"
[dft]
functional = "PBE"
[scf]
max_iter = 400 # 173 iterations was needed; the default is 200
energy_conv = 1e-7 # a loose, reachable bound; see below
# density_conv defaults to 1e-6 and is the criterion that does the real work.
# Tighten it if you need more than the energy; see below.
[optimize]
max_steps = 60
These settings follow from the measurements below. Don't raise the basis or
tighten energy_conv without measuring one species first.
Delocalised cations need iterations, and a reachable energy_conv.
Measured on the same 27-atom cation, changing only these two knobs:
max_iter 100, energy_conv 1e-8 -> MaxIter @100, 29 min, 3.30 GB, no result
max_iter 400, energy_conv 1e-7 -> CONVERGED @173, 4.9 min, 1.81 GB
173 iterations is normal for a delocalised carbocation; the default cap of 200
is not sized for this. energy_conv = 1e-8 was unreachable: under density
fitting the energy change floors with naux, so a dE bound tighter than the RI
noise floor is never met and the run spends its whole iteration budget on it.
density_conv is the criterion that converges the density. Leave the dE bound
loose enough to be satisfiable.
The exit reason is diagnostic. Stalled means the SCF could not make
progress: suspect the geometry. MaxIter means it was progressing and ran out
of iterations: raise the cap.
Set density_conv as well as energy_conv. E_HF is variational and
tolerates a loose density; anything depending linearly on the MO coefficients
does not. A correlation energy computed from an unconverged density can look
reproducible and still shift when anything upstream changes, because the
density was never converged.
Checkpoint every species as it finishes. A crash at species 6 of 8 should cost one species, not the whole run.
The method and basis decide whether the study is feasible. Measured on the same 27-atom cation, single point (both the basis and the functional differ between the two rows, so this does not isolate the cost of the basis):
def2-SVP / B3LYP : >22 min, 6.04 GB, SIGKILLed before converging
6-31G / PBE : 4.9 min, 1.81 GB, CONVERGED
6-31G/PBE used a third of the memory and finished. Don't plan a def2-SVP cascade on a workstation without measuring one species first.
Treat the optimization time as an estimate. An optimization takes tens of gradient steps. Estimate: at ~5 min per 6-31G single point, 30–60 steps would take roughly 2.5–5 hours per species if each gradient step costs about as much as a single point. Neither a gradient step nor a full optimization of this system has been timed. Later steps start from the previous density and may converge faster than a cold start. Time one optimization before budgeting a cascade.
Use a release build. Measured: the same cation at B3LYP/def2-SVP ran for
more than 34 minutes on a debug binary, sharing the machine with one other
job, without converging a single point. The wheel and cargo build --release
are both release builds.
A 27-atom cation at def2-SVP needs ~6 GB. Budget the study with that in
mind. Running eight species of that size at once exhausts a workstation.
Always wrap the run in scripts/ferric-limited --max=8G --high=7G --.
For agents
explains why the in-process budget alone is not enough.
Size it before you start. Measured: the tert-butyl cation (C4H9+, 101 basis functions at def2-SVP) is a single point that takes seconds. A C10 cascade cation (C10H17+) has 225 basis functions. Estimated at N^3.5 scaling, that is about 16x the single-point cost. That is cheap for an energy, but an optimization multiplies it by the step count, so the optimizations of the C10 species dominate the cost of the study. Run one species end to end and time it before you queue eight.
Step 3 — confirm the ordering survives the functional
Run single points on the optimized geometries with at least two more
functionals, for example B3LYP and wB97X-V (both available;
wB97X-V resolves to libxc's HYB_GGA_XC_WB97X_V), at a basis that fitted in
Step 2.
If the functionals disagree on the ordering of intermediates, report the disagreement. Do not average them or pick the one that matches your hypothesis. A functional-dependent ordering is a finding about the system.
Step 4 — properties at the key intermediate
Hirshfeld and Löwdin charges, the ESP at the nuclei, and the static
polarizability of the π-stabilized cation. From Python these are
ferric.hirshfeld_charges, ferric.lowdin_charges and ferric.esp_at_atoms.
Cost warning: the polarizability comes from ferric-rpa and is an
RPA-level calculation. Budget it separately from the DFT run. It can cost more than the optimization before it.
Step 5 (optional) — barriers
Intermediates alone give you a thermodynamic profile. If you need barriers,
ferric has a Python-only transition-state toolchain. None of it is wired to
the CLI's method.task:
| Call | What it does | Scope |
|---|---|---|
ferric.run_saddle(mol, basis, xc=...) | P-RFO search for a first-order saddle point | Closed shell only (multiplicity 1), HF or KS. It raises if the start has no negative Hessian mode, so start from a guessed TS, not a minimum. The Hessian is built twice by central differences and Bofill-updated in between: 2(6N+1) + (steps+1) gradients. |
ferric.run_irc(mol, basis, mode=...) | Follows the reaction path downhill in both directions, from the saddle to the two minima it connects | Closed shell only. Pass SaddleResult.imaginary_mode as mode. MEASURED ~71 gradients per direction on NH3 inversion. |
ferric.run_frequencies(mol, basis, reference=..., xc=...) (CLI: task = "frequencies") | Harmonic frequencies from the analytic Hessian where it applies (RHF, UHF, ROHF, and closed-shell KS LDA/GGA through the moving-grid KS Hessian, all under the default hessian="auto"), otherwise finite differences of the analytic gradient (6N gradients). Negative entries are imaginary modes. .normal_modes gives the vectors. | RHF/UHF/ROHF and their KS variants. Check .asymmetry to judge whether the step size suited the system. dispersion="d3bj" or "mbd" (CLI: [dft] dispersion) adds the dispersion Hessian on RKS, UKS and ROKS. |
One imaginary frequency is necessary but not sufficient for a transition state:
a methyl rotor also gives one. Use run_irc to confirm that the saddle connects
the two intermediates you meant.
What this cannot tell you
State these in any write-up. They define the scope of the claim.
- Gas-phase cluster models. There's no enzyme environment unless you add QM/MM (QM/MM).
- Analytic Hessians for RHF, UHF and ROHF only. Their frequencies use the analytic Hessian (RHF and UHF also with an ECP or fixed external charges or field; ROHF with neither); KS-DFT (ROKS included), implicit solvent and polarizable embedding frequencies, and every TS search, use finite differences of analytic gradients (Step 5). That costs 6N gradients per Hessian, so a C10 cation Hessian is hundreds of gradient evaluations. Without Step 5 you have intermediate energies but no barriers, and kcat depends on barriers.
- No thermochemistry. There's no entropy, enthalpy or free-energy
correction anywhere, from the CLI or Python. You can compute a zero-point
energy yourself from the frequencies (½Σhν over the real modes). Python's
FrequencyResultdoesn't provide one. - Relative energies only. Absolute totals carry basis-set and functional errors far larger than the differences you are interpreting.
Reading the output
| Signal | Meaning |
|---|---|
converged = true | Trust the energy. Absence of this line is not success. |
converged = false | An energy is still printed. It is not a result. |
exit Some(Stalled) | The SCF could not progress. Suspect the geometry: check the minimum interatomic distance and relax with xtb. |
exit Some(MaxIter) | It was progressing and ran out of iterations. Raise max_iter (173 is normal for a delocalised cation) and check energy_conv is reachable under DF. |
exit Some(Diverged) | The energy climbed for several iterations in a row. Suspect the geometry or the charge/multiplicity before the solver. |
| charge/multiplicity error | Your .xyz atom count is wrong. Read the arithmetic it prints. |
| Disagreement with another code ~1e-5 Ha | Check the grid: ferric (75,110) vs PySCF ~(75,302). Scales with atom count. |
| Larger KS-DFT disagreement that grows with size | ferric density-fits Coulomb by default in KS-DFT. See Recipes §6. |
| A correlation energy that moves when nothing physical changed | density_conv was never set. |
Before publishing any number
Check the capability's grade in Capabilities and validation. A method being available from the CLI doesn't mean its numbers are production-grade. The grades are Proven, Proven (narrow), Smoke and Spike; a few kinds are not graded.
ferric for agents
Read this first if you are an automated agent driving ferric. It's a routing page: what exists, where each thing is documented, and which mistakes cost the most time. The linked pages hold the details.
Start here
Don't build from source unless you need to. The prebuilt wheel installs in about a minute. A source build takes about 10 minutes, mostly compiling ferric.
pip install ferric
git clone https://github.com/mgoldey/ferric && cd ferric # for examples/, testdata/, scripts/
ferric examples/water-rhf.toml
Expect converged = true and energy = -74.9631468000 Hartree. If you don't
get that, stop and fix the install (Installation). Nothing
else will work, and the failures will be confusing.
pip install puts a ferric command on PATH that runs the same CLI as the
source build. The wheel holds only the compiled library and that command. The
examples/, testdata/ and scripts/ directories need the clone.
Which wheel
| Channel | What | How |
|---|---|---|
| PyPI | Built by the Wheels workflow on a v* tag. So far only pre-releases (0.1.0rc*) exist, and pip and uv pick them when there's no final release. | pip install ferric |
| GitHub nightly | Built every night (53 10 * * * UTC) from main. It's a workflow-run artifact, not a PyPI upload. | Download it from the Wheels workflow run, then pip install ./<wheel> |
Use the nightly artifact when you need a fix that has landed on main but
hasn't been tagged, instead of building from source.
Build from source when you are changing ferric or need MPI. MPI builds are source-only (see Installation).
Capability → entry point
| You want | Use | Where it's documented |
|---|---|---|
| Which methods exist, with which tasks and validation grade | — | Capabilities and validation |
| An energy from a TOML file | CLI, method.kind | Recipes §1, input reference |
| An ion, radical or metal center | [molecule] charge, multiplicity (Python: on Molecule.from_xyz) | Recipes §2 |
| An optimized geometry | method.task = "optimize" | Recipes §3 |
| Harmonic frequencies | method.task = "frequencies", ferric.run_frequencies | Analytic Hessian for RHF/UHF/ROHF and closed-shell KS LDA/GGA (the moving-grid KS Hessian, chosen by the default hessian = "auto"), otherwise finite differences of analytic gradients; examples/water-frequencies.toml |
| A transition state and its reaction path | ferric.run_saddle, ferric.run_irc (Python only, closed shell) | Golden paths Step 5 |
| QM/MM embedding | ferric.QmmmSystem, ferric.run_qmmm, CLI [qmmm] | QM/MM |
| A machine-readable record of a run | <input>.ferric.jsonl, written by default | Run logs |
| Python instead of TOML | the ferric module | Python API |
Before you trust a number
Check the grade. Not every capability is equally validated. A method being available from the CLI doesn't mean its numbers are production-grade. Read its row in Capabilities and validation before quoting a value.
Check convergence, not the exit code. converged = true (CLI) or
.converged (Python) is the signal. A run that hit max_iter still prints an
energy. In the run log, read run_end.converged, and for correlated methods
read the result record (Run logs).
When comparing to another code, check density fitting and the grid. Recipes §4 covers both.
Failure modes that cost the most time
- Charge/multiplicity parity. An odd electron count needs an even
multiplicity. The error message prints the arithmetic and the rule, and it
means your
.xyzor your charge is wrong. ferric can handle ions. - Debug instead of release. A debug binary is far slower. Measured: a
27-atom cation didn't converge a single point in 34 minutes on one. If a job
seems hung, check which binary is running.
cargo run --release --bin ferricand the wheel are both release builds../target/debug/ferricis not. - MPI is a workspace feature, not a CLI one.
ferric-clihas nompifeature of its own, so-p ferric-cli --features mpifails. The working build iscargo build --release --workspace --features mpi. - Threading. Don't set
OPENBLAS_NUM_THREADSabove 1. The CLI andimport ferricpin OpenBLAS to one thread when the variable is unset and honour an explicit value, andcargosets it to 1 only when it is unset (an exported value wins). Multithreaded BLAS under ferric's rayon parallelism can crash (LU routines) or oversubscribe the machine, so the setting affects correctness as well as speed. - Under-converged density.
energy_convalone doesn't converge the density. Correlation energies and properties inherit the full first-order density error. E_HF doesn't, because it's variational. Setdensity_conv. - Serialize QM jobs. Two concurrent ferric runs slow each other by more than half. Measured: a pair of them pushed a single point that normally takes seconds past a 15-minute timeout. Each run wants all the cores and several GB. Run one at a time.
Memory: the budget predicts, the cgroup enforces
ferric works out a memory budget from [memory] budget_gb,
FERRIC_MEM_BUDGET_GB, or, if neither is set, 0.8 × available RAM. The CLI
prints that budget at startup and installs it as one shared, debited pool for
the whole process: the three-index RI tensors, the DFT grid's AO cache and the
large tensors of the MP2, RPA, GW and CC methods reserve their bytes from it,
so two allocations alive at the same time cannot each claim the whole budget.
From Python, memory_budget_gb= sets the same per-allocation limits but
installs no shared pool, so each check compares its own allocation with the
whole budget. In both, an allocation that does not fit is spilled to disk,
recomputed on demand, or refused with an error naming it, depending on the
allocation (see
Sharp bits).
The budget isn't a cap on process memory (RSS). It covers the dominant
tensors, not every allocation. Basis-sized matrices, integral engines (for
example libint2's C++-side pools), BLAS and per-thread scratch and allocator
overhead aren't charged, so treat the printed number as a floor on what the
process needs. Lowering FERRIC_MEM_BUDGET_GB changes how the charged work
is split into blocks. It doesn't shrink the uncharged part.
Size the budget from a gradient run. Measured on benzene/cc-pVDZ/PBE/RI-J: the SCF alone peaked at 0.616 GB, and the SCF plus gradient at 3.033 GB. The KS gradient holds the AO second derivatives on the grid. A budget sized from an energy run under-provisions a geometry optimization by roughly 4x, and the first optimization step is where it fails.
The cgroup is the only hard ceiling. On Linux with systemd:
scripts/ferric-limited --max=8G --high=7G -- ferric input.toml
An unbounded overshoot triggers the system-wide OOM killer, which can kill
unrelated processes. Inside a cgroup, only the job dies. dmesg | grep -i oom
confirms a kill. A truncated log doesn't prove one. As a rule of thumb, plan on
~6 GB for 27 atoms at def2-SVP.
Telling a running job from a dead one
- Look for evidence of work, not a process name.
pgrep -falso matches your owngrep, abash -cwrapper, or a defunct entry at 0% CPU. Sort by CPU instead:ps -eo pid,stat,pcpu,rss,etimes,comm --sort=-pcpu | head. - A log that has stopped growing doesn't mean the job is dead. stdout is buffered, so a long QM run can legitimately print nothing for tens of minutes. The run log (Run logs) is flushed record by record, so watch it instead.
Terminatedusually means timeout(1), not the OOM killer. Check whether atimeoutwrapped the command before you look indmesg.- A grep filter that matches nothing looks the same as "the job produced nothing". Test the filter against known-good output first.
Scope
ferric is a quantum chemistry engine: energies, gradients, finite-difference Hessians and properties. It isn't a protein-engineering tool, a docking program or an MD code. The QM is the product.
Its analytic Hessian covers RHF, UHF, ROHF and closed-shell KS LDA/GGA (other frequencies are finite differences of the gradient), and it has no thermochemistry (entropy, enthalpy, free energy). Transition-state search and IRC are Python-only and closed-shell only (Golden paths Step 5). If a task asks for something ferric can't do, such as designing mutations or predicting ΔΔG, say so instead of approximating it with something adjacent.
Methods overview
What exists, grouped by family. Maturity varies a great deal between these. Check the grade and its anchors on Capabilities and validation before trusting a number. To go from a task to a method, start with Choosing a method.
| Family | Methods | Page |
|---|---|---|
| SCF and DFT | RHF, UHF, ROHF; KS-DFT (LDA, GGA, hybrid, range-separated, VV10, SCAN/r2SCAN/TPSS); D3(BJ) and MBD@rsSCS dispersion; IEF-PCM and COSMO solvation; gradients, optimization, frequencies (analytic Hessian for RHF/UHF/ROHF and closed-shell KS LDA/GGA; finite differences otherwise), TS search and IRC | SCF and DFT |
| MP2 | RI-MP2, attenuated (erfc, terfc), SCS, SCS-MP2(2terfc), MP2-V, RS-MP2 + LR-RPA, OO-RI-MP2, MP3, Laplace MP2 and SOS-MP2; local approximation of RI-MP2 | The MP2 family |
| Coupled cluster | CCD, CCSD, CCSD(T), LinLCCD (hh, drivers-only, full; exact or local); double hybrids B2PLYP, DSD-PBEP86, ωB97X-L-V | Coupled cluster |
| CAS-CI | full CI in an active space on a closed-shell RHF reference; lowest root; Rust library only | CAS-CI |
| Response | PDEP-RPA, dRPA by the Riccati solve (exact or local), G0W0, COHSEX, evGW0, evGW, BSE-TDA, TDA/TDDFT, polarizabilities and \( C_6 \) | RPA, GW and excited states |
| Electron transfer | cDFT, \( H_{ab} \) couplings (Python and Rust, no CLI) | Constrained DFT |
| Periodic systems | Gamma-point and k-point RHF/UHF/ROHF, RKS/UKS/ROKS, MP2, dRPA; analytic forces and stress; dense AFT or RS-GDF J/K | Periodic systems |
QM/MM embedding is on its own page: QM/MM.
Exact and local correlation
A correlated calculation is two choices: the method (what is computed:
RI-MP2, dRPA, LinLCCD) and the approximation (whether, and how, its
amplitudes are truncated). ferric keeps them apart. method.kind (and the
Python function) names the method, and the method is computed exactly. A
local approximation is asked for explicitly, with the CLI's
[local] section or the Python local= and
eps= keywords, and only rimp2, drpa and linlccd have one.
The one local scheme is the amplitude threshold: in the Boys-localized
basis, a pair amplitude whose localized integral is at or below eps is
dropped (single threshold, Wang et al. 2023). Three facts govern its use:
epsis part of the model. It has no default and must be written, and every printout and run-log record of a local run carries it, with the fraction of amplitudes kept ("local": nullmarks an exact run). Report it with any number you quote.eps = 0is the exact method. It keeps every amplitude and reproduces the exact energy (RI-MP2 to ≤ 1e-9 Ha; dRPA to the canonical plasmon formula; LinLCCD of the same variant), which is how each local path is anchored.- The error is one-sided and grows faster than linearly in
eps: dropped amplitudes give less correlation energy, measured with the same sign at every point, and each decade ofepsraises the error by more than a decade (the fraction of amplitudes dropped grows with it). MP2 has Hylleraas stationarity; dRPA does not, so its error is first order in the dropped amplitudes and the one-sidedness is measured, not guaranteed.
Riccati, plasmon and PDEP are not approximations of each other: they are
algorithms for the same exact dRPA. The drCCD Riccati solve at eps = 0,
the plasmon formula and full-rank PDEP agree to ≤ 2.6e-14 Ha (H2/STO-3G and
water/6-31G, PDEP at 64 quadrature points). Which one to use is a cost
question: see RPA, GW and excited states.
Formal scaling with system size N, for orientation (these are the textbook exponents, not measured timings): SCF and DFT N⁴ for exact four-centre integrals, lower with density fitting and screening; RI-MP2 and its variants N⁵; CCSD N⁶; (T) N⁷.
Infrastructure
Shared machinery underneath all of the above:
- Screening: Schwarz bounds on every four-centre path, CSB/CSAM (Thompson & Ochsenfeld 2017) as options, LinK exchange (Ochsenfeld, White & Head-Gordon 1998). QQR (Maurer, Lambrecht & Ochsenfeld 2012) is implemented and validated as a bound but not used in production.
- Spherical and Cartesian basis support, with BSE-JSON and Gaussian-94 parsers and a set of bundled orbital, RI, JK and ECP bases.
einsum!: a tensor-contraction macro that routes contractions through BLAS3 GEMMs, used throughout the CC and MP3 code.- Memory budgets: allocation ceilings that refuse an oversized job with a named breakdown instead of letting it be OOM-killed.
- Python bindings (pyo3) and a TOML-driven CLI. Not every method has both; Capabilities and validation says which.
Properties
ESP at nuclei and on the molecular surface, electric field, static and atom-partitioned polarizabilities, Mulliken, Löwdin, Hirshfeld, CHELPG and RESP charges, density matrices, and NPZ export of ML-ready features.
Negative results
Known limits and measured negatives are kept in one place, Capabilities and validation: known limits, so a correction has one row to change. Two matter when picking a method:
- TDHF/RPAx \( C_6 \) stays about 63% low regardless of gap. Use that kernel for static polarizabilities only.
- Local MP2: the integral-direct local MP2 (
rimp2with[local] integral_direct = true) is measured sub-quadratic (about N1.24 erfc, N1.4 Coulomb) on alkanes C20–C48; it is at about parity with exact RI-MP2 at C20 and about 6× faster at C32. Local MP2 withoutintegral_directmakes no scaling claim. See The MP2 family.
The AO-sparse Laplace SOS-MP2 truncation is not a negative: the radius it needs does not track the molecular diameter. See the MP2 page.
SCF and DFT
Ground-state self-consistent field methods, their nuclear gradients, and the
things built on them: geometry optimization, harmonic frequencies,
transition-state search, implicit solvation and dispersion correction. The
[scf] and [dft] keys are in Input file;
grades are on Capabilities and validation.
Hartree–Fock
What it is. RHF (closed shell), UHF and ROHF (open shell), with DIIS, Schwarz screening, and a choice of direct, LinK, density-fitted (RI-J / RI-K) or seminumerical (COSX) Fock builds (see Choosing how exchange is built below). The open-shell solvers add per-spin DIIS, a virtual-block level shift, augmented-Hessian Newton, and Maximum-Overlap-Method (MOM) orbital tracking for near-degenerate cases (Gilbert, Besley & Gill 2008). These exist because specific systems failed without them.
Run it. method.kind = "rhf" / "uhf" / "rohf" (examples/water-rhf.toml,
examples/h_uhf.toml); Python ferric.run_rhf, run_uhf, run_rohf. Spin
and charge are set on the molecule (Molecule.from_xyz(path, charge, multiplicity)), not on the solver call.
Defaults. Exact four-index J and K (no density fitting) unless
[scf] df_j_aux / df_k_aux are set. Convergence gates on the density:
density_conv = 1e-6 by default; energy_conv (default 1e-3) is a sanity
bound, not the convergence target; a value below the estimated noise floor of the
energy change draws a warning (see sharp bits).
Accuracy. Proven (rhf, uhf, rohf).
Kohn–Sham DFT
What it is. Kohn–Sham DFT through libxc: LDA, GGA, hybrid and range-separated hybrid functionals (for example PBE, B3LYP, ωB97X-V), VV10 nonlocal correlation, and the SCAN / r2SCAN / TPSS meta-GGAs (τ-dependent; no density Laplacian). The solvers cover RKS, UKS and ROKS.
Run it. method.kind = "ksdft" with [dft] functional = "PBE"
(examples/water-wb97xv.toml, examples/benzene-dfb3lyp.toml); Python
ferric.run_dft or run_ksdft. The two Python entry points are closed shell
(RKS) only. In the CLI, ksdft on a molecule with multiplicity > 1 runs UKS,
and kind = "uhf"/"rohf" with [dft] functional run UKS/ROKS. Open-shell
KS is also the reference inside pdep-rpa and gw ([rpa] xc with
multiplicity > 1), and in Python through
run_frequencies(reference="uhf", xc=...) and QM/MM
(run_qmmm(method="uks")); see Capabilities and validation.
Defaults.
- Functional: LDA if
[dft] functionalis omitted. - Grid: Becke partitioning of atom-centred Treutler–Ahlrichs radial ×
Lebedev angular grids, up to 75 × 110 points per atom, pruned by
default (
grid_prune = "nwchem-radial": NWChem angular regions, and 50 radial shells for H and He, 66 for Li–Ne, 75 for heavier atoms) with a 1e-14 weight cutoff (grid_weight_cut, ORCA'sWeightCut). That is 42-47% fewer points than the flat 75 × 110 grid on organic molecules, with energies within 2e-7 to 4e-6 Ha of it on PBE/def2-SVP (the flat grid's own error against a 99 × 590 grid is 4.5e-4 to 3.3e-3 Ha on C10–C32 alkanes). - The flat 75 × 110 grid is
grid_prune = "none"withgrid_weight_cut = 0."nwchem"prunes the angular grid only (about 23% fewer points,examples/water-pbe-pruned-grid.toml);"defgrid1"–"defgrid3"are ORCA 6.1's DEFGRIDs, with their own sizes: fewer radial shells than the default (H 30/35/44, Li–Ne 38/44/53, Na–Ar 46/54/63 against 50/66/75) but more angular points (peak orders 194/302/590 for heavier atoms and 110/194/434 for H and He, against 110).defgrid2, ORCA's default, moved PBE/def2-SVP energies by 1e-3 to 4e-3 Ha from the flat grid. Setting onlygrid_radial/grid_angularkeeps the default scheme at that size. Optimization, frequency (including the analytic moving-grid Hessian) and TDDFT runs use the grid the SCF ran on. - SCAN is the exception to "pruning costs nothing". On water/6-31G its
gradient error against a 99 × 590 grid is 2.1e-4 Ha/Bohr on the flat grid
and 5.6e-4 on the default, because the hydrogens lose radial shells.
r2SCAN (3.7e-4 flat, 3.6e-4 default) and PBE (unchanged) are not
affected. For SCAN, use
grid_prune = "nwchem"or a larger grid. - Density fitting: RI-J and RI-K are on by default for
ksdftandrun_dft(def2-universal-jkfit), whilerhfis exact by default. In Python,df_j_aux=""selects exact Coulomb. The fitting error grows with size: measured at PBE/STO-3G against exact J it was 0.28 kcal/mol for water, 1.16 for benzene and 9.5 for a 71-atom drug molecule. Compare with an exact-Coulomb code only after turning fitting off, or at matched fitting. - Meta-GGAs get an automatic 0.5 Ha virtual-block level shift (ramped to zero at convergence) when you set none, because τ amplifies grid noise and plain DIIS limit-cycles.
Accuracy. ksdft is Proven. SCAN/r2SCAN/TPSS energies are pinned against PySCF
(ferric-scf/tests/dft_scan.rs); the SCAN/r2SCAN closed-shell gradients against
PySCF with grid response (dft_gradient_mgga.rs). TPSS gradients have no
PySCF pin. Other meta-GGA variants
(deorbitalized, +VV10, hybrid meta-GGA) are refused, not approximated. The
\( V_{xc} \), \( f_{xc} \), VV10, Becke-weight and gradient formulas are
derived in DFT derivations.
Dispersion correction: D3(BJ)
What it is. Grimme's D3 with Becke–Johnson damping, a post-SCF pairwise
correction, implemented natively in Rust: the two-body term, plus the
Axilrod–Teller–Muto three-body term when its scale s9 is nonzero. s9
defaults to 0 (off), the reference default for a named functional
(simple-dftd3 without --atm).
Run it. [dft] dispersion = "d3bj" on method.kind = "ksdft"
(examples/water-pbe-d3bj.toml); "d3bj(b3lyp)" borrows another functional's
fitted damping parameters. Python: run_dft(..., dispersion="d3bj"), or
ferric.d3bj_energy(mol, functional, s9=0.0) for the correction alone;
s9=1.0 adds the three-body term. The CLI and run_dft(dispersion=...) use
s9 = 0.
Scope. The damping parameters are fitted per functional, so the key is
refused on any method other than ksdft. Tabulated functionals, each equal
to simple-dftd3 1.6.0's rational-damping entry: BLYP, BP86, B97-D, PBE, PBE0,
revPBE, B3LYP, TPSS, TPSS0, B2PLYP, BHLYP, SCAN, r2SCAN, ωB97X. M06-2X has no
D3(BJ) fit (only zero-damping D3(0)) and is an error. task = "optimize" works (the D3
gradient is implemented; ferric-d3/tests/gradient_vs_fd.rs), and so does
task = "frequencies" on RKS, UKS and ROKS: see
Frequencies with dispersion.
Accuracy. Energy and analytic gradient agree with simple-dftd3 1.6.0
with the three-body term off and on, on nine molecules from water to benzene
and CHBr3: 1.2e-13 Ha and 5.5e-13 Ha/Bohr on the totals, 2.6e-15 Ha on the
three-body part (ferric-d3/tests/vs_reference_dftd3_atm.rs). The three-body
term is 0.1% of the two-body energy at benzene and grows with size. Unlike
simple-dftd3, no real-space cutoff is applied (it drops triples beyond
40 Bohr), and the triple sum costs O(N³).
Dispersion correction: MBD@rsSCS
What it is. The many-body dispersion energy of Ambrosetti et al. 2014 (range-separated self-consistent screening), added post-SCF. Its per-atom inputs are Tkatchenko–Scheffler free-atom α, C6 and R_vdW scaled by Hirshfeld volume ratios v_A / v_A^free of the converged SCF density; the free-atom volumes come from live free-atom SCFs in the same basis and SCF settings, solved for the isolated atom: point charges, external fields, implicit solvent, polarizable sites and cDFT constraints of the molecular run are not applied to it. The model itself is described under Polarizabilities and dispersion coefficients.
Run it. [dft] dispersion = "mbd" on a Kohn–Sham SCF
(examples/water-pbe-mbd.toml) uses the β published for [dft] functional
(PBE 0.83, PBE0 0.85, HSE06 0.85); "mbd(pbe0)" uses another functional's β.
Any other functional is an error, not a default β. The printout gives
E(KS-DFT), E(MBD@rsSCS) with β and the functional, and the corrected
total; the JSON run log's run_end record carries the corrected total as
energy, with scf_energy and a dispersion object (model, params,
energy, beta, volume_ratios) in extra (D3(BJ) runs log the same
object without beta and volume_ratios). UKS/ROKS energy runs, which write
no run_end record, write a dispersion record with the same fields. Python:
run_dft(..., dispersion="mbd"), which also reports DftResult.volume_ratios.
Gradient. task = "optimize" on an RKS, UKS or ROKS reference and
run_dft(with_gradient=True) add the exact analytic MBD@rsSCS gradient: the
explicit dependence on the nuclear positions, and the dependence through the
Hirshfeld volumes, including how the SCF density itself responds to the
displacement. That response is the orbital relaxation, obtained from one
coupled-perturbed Kohn–Sham (Z-vector) solve per gradient; it costs about as
much as a few SCF iterations. On a UKS reference the solve is coupled across
the α and β orbital rotations (Coulomb couples the spins; exchange and the XC
kernel are spin-resolved), and the orthonormality of each spin's occupied
orbitals enters separately. On a ROKS reference the solve runs over the three
rotation blocks of the shared orbitals (closed→virtual, open→virtual,
closed→open) with the exact restricted-open-shell orbital Hessian, and the
closed and open orbitals are kept orthonormal as one set, which adds a
closed–open cross term to the orthonormality contribution. The volumes are
integrated on a lattice anchored to the molecular centroid, and its motion is
part of the gradient. Against finite differences of the full SCF + MBD
calculation at 6-31G the gradient agrees to 6e-9 Hartree/Bohr for H2O with
PBE, PBE0, HSE06 and PBE with RI-J, and to 9e-10 for NH3; the orbital
relaxation alone is 1.0e-5 for H2O (11.5% of the largest MBD component). For
UKS doublets and a triplet (NH2, OH, O2 at 6-31G with PBE, PBE0 and HSE06) it
agrees to ≤ 1.9e-9, and to 6e-9 for OH with PBE + RI-J and PBE0 + RI-JK,
against an orbital relaxation of 3e-6–1e-5. For ROKS doublets and triplets
(HCO, NH2, CH2, O2 at 6-31G) it agrees to ≤ 2.1e-11 with PBE and PBE0, ≤ 4.5e-10
with HSE06 and 7.5e-9 for HCO with PBE + RI-J, against an orbital relaxation
of 2.7e-6–9.8e-6. For a linear radical whose state breaks the cylindrical
symmetry (²Π: OH, NO, CH), a rotation of the electrons about the molecular
axis is an orbital rotation that leaves the energy unchanged, so the orbital
Hessian has a null mode; only the anisotropy of the XC grid gives it a small
eigenvalue of either sign. The UKS and ROKS Z-vectors detect it on linear
molecules and solve on its complement, which is exact for a property that is
symmetric about the axis. On OH, NO and CH (STO-3G and 6-31G, PBE and PBE0)
the gradient agrees with finite differences of the full pipeline to
7e-12–5e-11 Hartree/Bohr. A state with any other negative or near-null
orbital-Hessian direction is still refused (the UKS-PBE CH state is a saddle
point and is refused). The relaxation term needs an LDA, GGA or hybrid-GGA
functional without VV10, no implicit solvation, polarizable embedding or cDFT
constraints, and integer aufbau occupation (no MOM). With COSX exchange the
overlap fit must be off (cosx_overlap_fit = false); the UKS-PBE0 + COSX
gradient on NH2/STO-3G agrees with finite differences of the full pipeline to
2.3e-11. Other setups are refused rather than given an
approximate gradient. Python's run_dft is closed-shell only; open-shell
frequencies with MBD@rsSCS run through the CLI or run_frequencies.
Frequencies with dispersion
What it is. Harmonic frequencies on the dispersion-corrected surface
E(KS) + E(disp), on an RKS, UKS or ROKS reference. The Hessian is the central
difference of the corrected analytic gradient: at each of the 6N displaced
geometries the SCF of that reference is converged, the KS gradient and the dispersion gradient
are evaluated there, and their sum is differenced. For MBD@rsSCS the
dispersion gradient is the exact one above, with the Z-vector of the
reference solved (RKS, UKS or ROKS), so the response of the Hirshfeld
volumes to the displacement, through the SCF density, is in the Hessian.
There is no analytic Hessian on this path: [frequencies] hessian = "analytic"
is an error and "auto" runs finite differences.
Run it. [dft] dispersion with method.task = "frequencies" on a KS
SCF: ksdft, or rhf/uhf/rohf with [dft] functional. The printout gives the corrected energy, E(KS-DFT)
and the dispersion energy at the input geometry, and the JSON run log gets a
dispersion record with the same fields as an energy run. Python:
run_frequencies(mol, basis, xc="PBE", dispersion="d3bj"), which reports
.e_dispersion and the corrected .energy; reference="uhf" or "rohf"
selects UKS or ROKS.
Accuracy. For PBE/STO-3G water, the D3(BJ) part of the Hessian agrees with 4-point second differences of the D3(BJ) energy to 5.3e-9 Hartree/Bohr² (largest element 1.9e-5), and the resulting frequencies with those of the KS Hessian plus that independent D3 Hessian to 1e-5 cm⁻¹. The MBD@rsSCS part agrees along three fixed directions with second differences of the full SCF + MBD energy to 1.5e-6 Hartree/Bohr² (0.8% of the largest curvature, 2.0e-4). That figure is set by noise in the energy differences, not by the Hessian construction. A gradient without the orbital-relaxation term misses by 3.2e-5. Dispersion leaves the Hessian's asymmetry (7.6e-6 Hartree/Bohr² for PBE/6-31G water) and the projected translation/rotation modes (below 1e-4 cm⁻¹) where they are without it.
On open shells (PBE/STO-3G, bent NH2 doublet on UKS, HCO doublet on ROKS), the D3(BJ) part agrees with 4-point second differences of the D3(BJ) energy to 3.7e-9 and 6.5e-9 Hartree/Bohr² and the frequencies to 9.4e-6 and 3.0e-5 cm⁻¹; the D3 Hessian obeys the translational sum rule to 1e-11 and the rotational one (with the D3 gradient) to 3e-9. The MBD@rsSCS part agrees along three fixed directions with 5-point second differences of the full SCF + MBD energy to ≤ 4.8e-8 Hartree/Bohr² for UKS NH2 and CH2 (triplet) and ROKS HCO and CH2, against curvatures of 6e-5–2e-4; without the orbital-relaxation term the misses are 8.3e-7–1.6e-5. A correction that does nothing reproduces the plain UKS or ROKS finite-difference Hessian bit for bit.
The shifts are small for a single molecule. For PBE/6-31G water at its uncorrected PBE minimum (1594.35, 3500.27 and 3669.29 cm⁻¹), D3(BJ) shifts the three modes by +0.006, −0.077 and −0.095 cm⁻¹, and MBD@rsSCS by +0.107, −0.406 and −0.516 cm⁻¹.
Implicit solvation
Two independent implementations. Both are threaded uniformly through RHF, UHF, ROHF and the KS variants, and both leave the energy bit-identical to vacuum when unset.
| Model | What it is | How to run it | Anchor |
|---|---|---|---|
IEF-PCM (ferric-pcm) | Integral-equation PCM; modified Bondi radii (H 1.10 Å), atom-centred Lebedev spheres | CLI [pcm] solvent = "water" (or epsilon), energy runs of rhf, uhf, rohf, ksdft and pdep-rpa (examples/water-pcm.toml), and task = "optimize" for rhf, uhf, rohf and ksdft. Python run_rhf(..., solvent=78.4) or a solvent name; also run_pdep_rpa. | water / STO-3G, ε = 78.4: −3.813 vs PySCF IEF-PCM −3.8228 kcal/mol |
COSMO (ferric_scf::cosmo) | Conductor-like screening | CLI [cosmo] epsilon = 78.39 | water / cc-pVDZ, ε = 78.39: −5.955 vs PySCF COSMO −5.94 kcal/mol (ferric-scf/tests/cosmo_water.rs) |
Neither has cavitation, dispersion or repulsion terms. IEF-PCM has an
analytic nuclear gradient (ferric_pcm::pcm_gradient: the tessera-charge
potential term plus the S/D/area response term, switching-function
derivatives included; equations in crates/ferric-pcm/theory.md §8), added
to the RHF, UHF, ROHF and KS gradients whenever RhfConfig.pcm is set. It
matches PySCF IEF-PCM nuc_grad_method() to 7e-11 Ha/Bohr on PySCF's
302-point cavity and finite differences of the solvated SCF energy to
3e-7 Ha/Bohr. It needs the cavity built from the molecule (an injected
PcmConfig::cavity is refused). There is no PCM Hessian: frequencies with
[pcm] are refused. COSMO has no gradient term and no Hessian: every
analytic-gradient entry (the SCF gradient dispatchers, optimize_geometry*,
optimize_qmmm, finite-difference frequencies, and CLI task = "optimize" or
"frequencies" with [cosmo]) refuses a COSMO SCF with "COSMO has no analytic
gradient; use [pcm] ... or finite differences". COSMO is energy-only; use
[pcm] for geometry work. An unknown solvent name is an error, not a silent
vacuum.
Gradients, optimization, frequencies, transition states
Gradients. Analytic nuclear gradients for RHF, UHF, ROHF and KS-DFT (including meta-GGA, with grid response), checked against finite differences and, for DFT, against PySCF. RI-MP2 also has an analytic gradient, closed shell only (there is no unrestricted MP2 nuclear gradient).
Optimization. method.task = "optimize" for rhf, uhf, rohf,
ksdft, rimp2 and pdep-rpa (examples/h2_opt.toml,
examples/h2-lda-opt.toml); on an open-shell molecule only uhf, rohf and
ksdft. Python ferric.run_optimize is RHF.
Harmonic frequencies. method.task = "frequencies" for rhf, uhf,
rohf and ksdft (examples/water-frequencies.toml,
examples/nh2-rohf-frequencies.toml); Python
ferric.run_frequencies(mol, basis_name, reference="rhf", xc=...). Mass-weighted,
translations and rotations projected out. The Hessian is:
- Analytic for closed-shell RHF and for UHF of any multiplicity, with
exact four-centre J/K or with density-fitted J and/or K (
df_j_aux/df_k_aux, Coulomb operator, no external potential on the fitted routes, a well-conditioned fitting metric), no solvent or polarizable embedding, and a basis up to f functions (RHF and UHF also with an ECP, on the fitted routes too, and with exact J/K also with fixed external point or smeared charges, or a uniform field), and for ROHF with exact four-centre J/K or RI-J / RI-K, an ECP allowed, no external potential and a basis up to f functions: one SCF plus a coupled-perturbed HF solve per nuclear coordinate (for UHF, one solve coupling the α and β orbital rotations; for ROHF, one solve over the closed-virtual, open-virtual and closed-open rotations). This needs a libint2 with second derivatives (the buildscripts/install-libint.shinstalls). - Analytic on the moving grid for closed-shell Kohn-Sham (
ksdft, orrhfwith a functional): LDA and GGA functionals, pure or global hybrid, exact or density-fitted J and K, Becke weight response included, so it is the Hessian of the surface the KS gradient differentiates. One SCF plus a coupled-perturbed KS solve. - Central finite differences of the analytic gradient everywhere else
(UKS, ROKS, meta-GGA, VV10, range-separated hybrids, KS with an ECP or an
external potential, pruned grids,
[dft] dispersion, RI J/K with an external potential or an ill-conditioned fitting metric, solvent, polarizable embedding, g functions, ROHF with an external potential): 6N gradient evaluations. The displacement[frequencies] delta(default 5e-3 Bohr) is a real accuracy knob; the printed Hessian asymmetry, zero in exact arithmetic, is the check that it and the SCF thresholds suit the system.
[frequencies] hessian (Python hessian=) selects it: "auto" (default,
analytic where it applies, the moving-grid Hessian for closed-shell KS),
"analytic" (the Hartree-Fock analytic Hessian; an error where it does not
apply, including every KS reference), "analytic-moving-grid" (the
closed-shell KS one; an error where it does not apply) or "fd". The output
names the one that ran (Hessian = analytic, analytic-moving-grid or
finite-difference; Python .hessian_source).
The moving-grid KS Hessian agrees with "fd" up to the finite-difference
error: ≤ 5e-7 cm⁻¹ against Richardson-extrapolated FD at a tight SCF
(density_conv 1e-11) on water / STO-3G, for one SCF plus a
coupled-perturbed KS solve instead of 6N gradients. Against the default
"fd" (step 5e-3 Bohr, default SCF), which "auto" ran for KS before, the
wavenumbers move by ≤ 0.071 cm⁻¹ (water / STO-3G, LDA, PBE, B3LYP). Under
"auto", open-shell KS, meta-GGA, VV10, range-separated hybrids, ECPs,
external potentials, pruned grids, [dft] dispersion, COSX, implicit solvent,
fractional occupations, MOM, and systems whose response AO table (basis
functions × grid points × 4 for a GGA, 8 bytes each) exceeds 2 GiB (on the
default 75×110 grid, a GGA above about 8,100 basis functions × atoms, e.g.
about 30 atoms at def2-SVP) run finite differences; "analytic-moving-grid"
refuses them before the SCF.
The choice is made before the SCF. An error the analytic path raises after the
SCF (for ROHF: the stored spin Fock matrices differing from those rebuilt from
the final densities by more than 1e-4 Ha, which a loose density_conv can
cause; a response solve that does not converge; non-positive orbital curvature;
a first-order rotation above 10 rad/Bohr) is returned as an error under
"auto" too, never retried with finite differences; the same holds for the
moving-grid KS Hessian (a non-converged SCF included).
Transition states and IRC (Python only, closed shell).
ferric.run_saddle(mol, basis_name, xc=...) searches for a first-order saddle
by P-RFO, using two finite-difference Hessians with Bofill updates between
them; it costs 2(6N + 1) + (n_steps + 1) gradient evaluations and refuses to
start from a geometry with no negative mode. hessian="analytic" (or "auto",
which falls back to finite differences where the analytic Hessian does not
apply) swaps in the analytic RHF Hessian (with xc, "auto" takes the
moving-grid KS Hessian), which needs no displaced gradients;
"analytic" raises for KS xc, point charges / an external field, ECPs or a
basis above the libint second-derivative limit, delta= is only valid with
"fd", and result.hessian_source says which ran. The default is "fd". result.is_transition_state()
requires both convergence and exactly one imaginary frequency.
ferric.run_irc(mol, basis_name, result.imaginary_mode) then follows the
reaction path both ways to show which minima the saddle connects (measured
about 71 gradients per branch on NH3 inversion). Both refuse
multiplicity ≠ 1.
Choosing how exchange is built
Four ways to build K, and the choice is a real one, measured on this code. Butane, one thread; TZ is def2-TZVP (184 functions), QZ is def2-QZVP (528).
[scf] setting | What it is | Exact? | Scope |
|---|---|---|---|
| (default) | Schwarz-screened direct four-centre J + K | yes | all SCF types |
k_builder = "link" | LinK: pair-list-screened direct K | yes (== direct to 9e-12 Ha, butane/def2-SVP) | RHF, UHF, ROHF |
df_j_aux / df_k_aux | density-fitted J and K (RI-JK) | fitting error, grows with size (see Kohn–Sham DFT above) | all SCF types |
k_builder = "cosx" | seminumerical (COSX) K on a grid; with RI-J active this is RIJCOSX | grid-dependent error, see below | RHF/UHF/ROHF and their Kohn–Sham variants, Coulomb operator only; analytic gradient for all six (see below) |
Time for one exchange build (seconds, butane, one thread):
| Builder | What the time covers | def2-TZVP | def2-QZVP |
|---|---|---|---|
| direct | J and K together (one integral sweep) | not measured | 400 |
| LinK | K | being re-measured | being re-measured |
| RI-JK | K | 0.05 | 0.43 |
| COSX | K | not measured | 90 |
Time for a full SCF (seconds, butane, one thread):
| Builder | def2-TZVP | def2-QZVP |
|---|---|---|
| direct | 98 | not measured |
| LinK | being re-measured | being re-measured |
| RI-JK | not measured | not measured |
| COSX | 358 | not measured |
Energy error against exact exchange:
| Builder | System / basis | Error |
|---|---|---|
| LinK | butane / def2-SVP | 9e-12 Ha |
| COSX, default (sgx (35,194) + final pass on sgx (50,302)) | water / aug-cc-pVDZ | −8.1e-9 Ha |
| COSX, default | butane / def2-SVP | −3.4e-5 Ha |
| COSX, default | butane / def2-TZVP | −4.3e-6 Ha |
| COSX, flat (50,110) | water / cc-pVDZ | 5e-6 Ha |
| COSX, flat (50,110) | butane / def2-SVP | 1.7e-4 Ha |
| COSX, flat (50,110) | butane / def2-TZVP | 1.2e-4 Ha |
| RI-JK | — | not measured on these systems |
Start with density fitting whenever its three-index tensor fits in memory
(n_aux × n_bf² × 8 bytes: 1 GB for butane/QZVP, 7 GB for octane/QZVP; half
that when stored as the packed symmetric triangle). When it does not fit, J is
built by Split-RI-J, which stores no tensor and on danuglipron PBE/def2-SVP
took 81 s for the whole SCF against 46 s with the tensor in memory and about
590 s spilling or recomputing it. A tensor K still needs is spilled to disk or
recomputed each iteration, whichever a measurement on the machine says is
cheaper ([scf] jk_storage). It is two to three orders of magnitude faster
per build than anything else here. Its error is a fitting error, not zero, and
it grows with system size.
Need exact exchange? Direct or LinK; both are exact to the screening
threshold as K builders (butane/def2-SVP: link == direct to 9e-12 Ha).
LinK's cost is being re-measured, so no LinK timing is quoted here.
k_builder is honoured by UHF and ROHF as well as RHF: the open-shell solvers
build K_α and K_β from one builder instance, refreshing its density-dependent
state per spin. Whether LinK is the faster choice for a given system is a
separate question from whether it is honoured; see the cost note above, which
is being re-measured. link is skipped with a warning, never silently,
whenever density-fitted J/K is active; both link and cosx are skipped with
a warning when the functional uses no exact exchange or is range-separated
(exchange then comes from the SR/LR fitters).
RIJCOSX. k_builder = "cosx" combines with density-fitted Coulomb: when
RI-J is active (df_j_aux named, or the Kohn–Sham default), J comes from RI-J
and K from COSX, as in ORCA's RIJCOSX and Psi4's DFDIRJ+COSX. COSX then
replaces RI-K: the Kohn–Sham RI-K default is not applied, and an explicitly
named df_k_aux next to k_builder = "cosx" is an error. df_j_aux = ""
keeps exact J with COSX K. The two approximations add: on water/6-31G (HF) the
COSX error is 7.140e-6 Ha with exact J and 7.141e-6 Ha with RI-J, and the
cross term is second order (2.8–5.8 × the product of the two errors; 7.8e-10
Ha at the (50,110) grid, 2.3e-11 Ha at (75,302)). Gradients take J from the
RI-J derivative and K from the COSX derivative (FD agreement 1.7e-9 Ha/Bohr
RHF, 3.6e-9 UHF).
COSX is for large basis sets on systems too big for RI-JK. Its cost per grid point barely moves with angular momentum while analytic exchange grows roughly tenfold from SVP to QZVP, so it wins at high angular momentum, not at large system size. Measured on one thread at the flat (50,110) grid:
- On butane, against exact direct exchange: at def2-TZVP the full COSX SCF is 3.7× slower (358 s vs 98 s); at def2-QZVP the COSX K build takes 90 s, against about 400 s for the direct build's single J+K sweep, at a relative K error of 5.4e-5.
- On n-alkanes at def2-SVP, against LinK: slower at every size measured, 1.59× (C20), 1.09× (C32) and 1.22× (C48), with no trend toward parity. At def2-TZVP on C20 it is faster (0.67×).
Below quadruple zeta it is the wrong tool unless RI-JK's three-index tensor does not fit in memory.
A density-driven pair screen (on the product of the integral bound and the
local half-transformed density) keeps the K error below 2e-6 Ha at the default
threshold, and the half-transforms D·X and X·Gᵀ run over per-batch sparse
AO lists (cosx_half_transform = "sparse", the default). At def2-SVP the K
build grows as N^1.29–N^1.32 between C20 and C48; that is one family of
molecules in one basis.
On the flat (50,110) grid COSX's energy error is about 5e-6 Ha on water/cc-pVDZ
(4.9e-6 in cosx_k_anchors.rs, 5.1e-6 in the ORCA comparison run),
1.7e-4 Ha on butane/def2-SVP and 1.2e-4 Ha on butane/def2-TZVP (against exact
exchange). ORCA 6.1.1's COSX at its own default grid gives 4.9e-6, 3.8e-5 and
1.3e-5 Ha on the same systems and bases with about half as many points: ORCA's
grids are pruned around a 194-point valence shell and it evaluates its final
energy once on a finer grid. ferric's default does the same (cosx_grid
pruning and cosx_final_pass, below). Refining the flat grid converges water to 3.4e-8 Ha,
but butane/def2-SVP stays at 3.4e-5 Ha at Lebedev-302 for every radial grid.
That residual is angular: an independent COSX (PySCF SGX, 75 radial shells) on
the same system goes from 5.5e-5 Ha at 302 points per shell to -8.5e-6 at 434
and 2.7e-6 at 590. ferric's COSX follows the same path at 75 radial shells:
-3.4e-5 at 302, -5.6e-6 at 434 and +1.8e-6 at 590, for 1.8x and 2.2x the
302-point wall time. Reaction energies cancel
most of the error (0.02 kcal/mol on an isodesmic alkane reaction at the flat
(50,110) grid); absolute energies do not. The knobs, all optional:
cosx_grid = { radial = 35, angular = 194, prune = "sgx" }is the default;{ radial = 50, angular = 110 }is the flat grid. The angular order matters most: on butane/def2-TZVP the error falls from 1.2e-4 Ha at 110 points per shell to 4.0e-6 Ha at 302, while going from 50 to 100 radial shells changes it by 1e-6 or less. Cost grows with the number of points.angularmust be one of 6, 14, 26, 50, 110, 194, 302, 434 or 590.prune = "sgx"prunes the grid the way ORCA's GridX and PySCF's SGX do: five radial regions per atom (NWChem boundaries on Bragg radii) with Lebedev orders from one row, picked by the peakangular— at 194 the regions get 26/50/110/194/110 points. ferric's pruned COSX energy equals PySCF SGX on the same grid to 1.8e-12 Ha. A table withoutpruneis flat.cosx_final_pass = true(the default;falseturns it off) re-evaluates exchange once on a larger grid at the converged density and reports that energy; the SCF-grid energy is printed and logged next to it.cosx_final_gridpicks the grid (default{ radial = 50, angular = 302, prune = "sgx" }). The pass is not self-consistent, but it lands within 1.5e-7 Ha of an SCF converged on the final grid (table below). It is energy-only: gradients and geometry tasks run without it.cosx_grid_schedule = false(default).trueruns the early SCF iterations on a coarse pruned{ radial = 25, angular = 110, prune = "sgx" }grid and switches tocosx_gridonce the largest density-matrix change between iterations drops below 1e-3, restarting DIIS at the switch. Convergence is only accepted after a full iteration oncosx_grid, so the converged energy is thecosx_gridenergy; the final pass is unchanged. On water/cc-pVDZ the coarse grid has 4062 points against 10866, and the scheduled SCF reaches the unscheduled energy to 1e-13 Ha in 15 iterations (8 coarse, 7 oncosx_grid) against 12. Its wall time has not been measured. RHF/UHF and their Kohn–Sham variants; ROHF refuses it.cosx_overlap_fit = true(default) applies the Izsák–Neese overlap correction. At the default grid it helps; on coarser grids it makes things worse, and its benefit depends strongly on the molecule (large on water, nil to negative on ethane), so the factor quoted in the literature does not carry over.cosx_backend = "md3c1e"(default) is the batched McMurchie–Davidson integral kernel."cosx-a"is the per-point libint2 path, about three times slower and kept only as the cross-check the kernel is anchored against.cosx_screen_thresh = 1e-7(default) is the density-driven pair-screening threshold.0.0disables screening bit-identically;1e-6already fails a 1e-6 Ha K-error bar on butane. Not available with the"cosx-a"backend.
Setting any cosx_* key without k_builder = "cosx", or k_builder = "cosx"
together with a named df_k_aux, is an error.
Which COSX grid. Measured against exact exchange (exact J on both sides, RHF, overlap fit on unless noted), one run each, six threads; times are whole SCFs, indicative only. Errors, E − E_exact in Ha:
| System | atoms | flat (50,110) | sgx (35,194) | sgx (35,194) + final sgx (50,302), default | sgx (50,302) | sgx (50,194) | flat (50,194) | sgx (35,194), no fit |
|---|---|---|---|---|---|---|---|---|
| water / aug-cc-pVDZ | 3 | +6.1e-6 | +1.9e-6 | −8.1e-9 | −8.1e-9 | +9.8e-7 | +1.6e-7 | +1.6e-6 |
| butane / def2-SVP | 14 | +1.7e-4 | +4.6e-5 | −3.4e-5 | −3.4e-5 | +4.1e-5 | +5.0e-5 | −2.7e-4 |
| butane / def2-TZVP | 14 | −1.2e-4 | −4.1e-5 | −4.3e-6 | −4.3e-6 | −3.2e-5 | −1.6e-5 | −1.2e-4 |
| benzene / def2-SVP | 12 | +8.2e-5 | −4.8e-5 | +5.6e-6 | +5.6e-6 | −4.8e-5 | −4.0e-5 | −8.8e-5 |
| sulfamethoxazole / def2-SVP | 28 | +2.5e-4 | −1.9e-5 | +2.2e-5 | +2.1e-5 | −2.3e-5 | −2.3e-5 | −1.9e-4 |
| methane / cc-pVDZ | 5 | +8.0e-6 | +9.2e-6 | +6.5e-7 | +6.5e-7 | +1.0e-5 | +5.1e-6 | −1.9e-4 |
| ethane / cc-pVDZ | 8 | +1.2e-4 | −4.3e-6 | −2.6e-6 | −2.6e-6 | −4.5e-6 | +1.7e-6 | −9.1e-5 |
| propane / cc-pVDZ | 11 | +1.2e-4 | +1.9e-5 | −1.0e-5 | −1.0e-5 | +1.8e-5 | +1.8e-5 | −7.0e-5 |
| C3H8 + CH4 → 2 C2H6, kcal/mol | +0.074 | −0.023 | +0.003 | +0.003 | −0.024 | −0.012 | +0.052 | |
| points per atom | 5500 | 3545–3629 | 3545–3629 SCF, 8109–8214 final | 8109–8214 | 4996–5068 | 9700 | 3545–3629 |
Whole-SCF wall time relative to exact-K RHF on the same system (COSX is slower than exact exchange at all of these sizes and bases; see above for where it wins):
| System | exact K | flat (50,110) | sgx (35,194) | sgx (35,194) + final | sgx (50,302) |
|---|---|---|---|---|---|
| water / aug-cc-pVDZ | 0.3 s | 15.7× | 10.1× | 12.8× | 21.7× |
| butane / def2-SVP | 2.6 s | 17.7× | 11.8× | 14.3× | 25.5× |
| butane / def2-TZVP | 19.0 s | 5.4× | 3.9× | 4.4× | 7.5× |
| benzene / def2-SVP | 3.2 s | 13.2× | 8.8× | 10.6× | 18.9× |
| sulfamethoxazole / def2-SVP | 78.7 s | 7.2× | 5.9× | 6.5× | 14.4× |
| methane / cc-pVDZ | 0.2 s | 27.0× | 20.5× | 22.5× | 40.2× |
| ethane / cc-pVDZ | 1.0 s | 18.7× | 12.7× | 15.8× | 28.7× |
| propane / cc-pVDZ | 3.6 s | 13.8× | 9.7× | 11.0× | 20.8× |
What the table shows:
- The pruned
sgx (35,194)grid uses 0.65× the points of flat (50,110) and is more accurate on seven of the eight molecules, by 1.7× (benzene) to 29× (ethane). On methane it is 1.15× worse (9.2e-6 against 8.0e-6 Ha). Geometry tasks (optimize, frequencies) use this grid, since they run without the final pass. - The final pass on
sgx (50,302)reproduces an SCF converged on that grid to 4e-8 Ha on seven molecules and 1.5e-7 Ha on sulfamethoxazole, at 0.45–0.59× of that SCF's cost. Together with the pruned SCF grid it is more accurate than flat (50,110) on all eight molecules and on the reaction (0.003 against 0.074 kcal/mol), and faster on all eight (0.80–0.91× flat (50,110)'s wall time). This pair is the default for energies; ROHF/ROKS has no final pass and skips it with a note. - More radial shells (35 → 50) at a 194 peak buy little; removing the pruning (flat 194) costs 2.7× the points for errors of the same size. The overlap fit helps on every molecule but water (1.8× to 21×; on water it is 1.2× worse).
COSX gradients.
task = "optimize" and task = "frequencies" with
k_builder = "cosx" differentiate the COSX energy itself (grid-function,
ESP-integral and Becke-weight derivatives). With the default overlap fit the
fitted exchange is not variational in the orbitals, so the gradient adds an
orbital-response (Z-vector) term. The gradient is exact for RHF, RKS, UHF,
UKS, ROHF and ROKS with cosx_overlap_fit = false, and for RHF and UHF with
the default fit; measured against finite differences of the COSX energy it
agrees to 2e-9–4e-9 Ha/Bohr, on flat and pruned (sgx) grids alike, and with
RI-J (RIJCOSX). The open-shell exchange term is differentiated per spin; an
exact-exchange gradient paired with the same COSX energy misses those finite
differences by 4e-5–2e-4 Ha/Bohr (HO2 and NH2, B3LYP and ROHF, STO-3G). The
MBD@rsSCS gradient's orbital-response term on a UKS or ROKS reference uses the
same COSX derivative. The gradient differentiates the SCF-grid energy: with
cosx_final_pass the reported energy is the final-grid one, so geometry tasks
run without the pass and a gradient of a final-pass result is refused (an
ORCA-style gradient evaluated on the final grid misses finite differences of
the final-grid energy by 4.6e-7–7.3e-7 Ha/Bohr on water/6-31G, so it is not
used). Refused for gradient tasks before the SCF runs: fitted COSX with a
Kohn–Sham functional (RKS, UKS, ROKS; the fitted energy's response needs the
nuclear derivative of the XC Fock matrix) and fitted COSX for ROHF (no
restricted-open-shell response).
Convergence
A few things worth knowing before debugging a stubborn SCF:
Check converged. These routines return a result whether or not they
converged; a non-converged SCF is a result with converged = false, not an
error. Downstream code that ignores the flag will happily consume a
half-converged density.
Multiple solutions. For systems like alkane chains, different initial guesses converge to different SCF solutions, each in its own basin. The guess picks the basin.
Density-fitting has a noise floor. DF-JK introduces an error floor that makes energy-based convergence criteria below roughly 1e-9 meaningless; SCF gates on the density RMS change instead.
Near-linear-dependence. Diffuse (aug-) basis sets on close-packed systems
can drive the overlap matrix near-singular; the canonical-orthogonalization
threshold is tunable via FERRIC_LINDEP_THRESH.
Secondary-basis guess (Q-Chem BASIS2)
[scf] guess_basis = "6-31g" (Python guess_basis="6-31g" on run_rhf,
run_uhf, run_rohf, run_dft, run_ksdft) converges the same SCF
(method, functional, charge, multiplicity and settings) in the named basis
first, projects the result into the target basis, and starts the target SCF
from it. guess_projection picks how the projection is done:
"fock"(default; Q-ChemFOPPROJECTION): the small-basis spin densities are projected,D = P D_small PᵀwithP = S_bb⁻¹ S_bs, and the target Fock built at that density is diagonalised. Closed shell, that Fock build is the target SCF's first iteration. Open shell, the guess Fock ish + J − K_σwith no XC term, which is the form ferric's MINAO open-shell guess also uses."overlap"(Q-ChemOVPROJECTION): the occupied MOs are mapped,C = S_bb⁻¹ S_bs C_small, and re-orthonormalised in the target metric. For ROHF, the doubly occupied block is orthonormalised first and the singly occupied block is made orthogonal to it.
S_bb⁻¹ is the lindep-filtered pseudo-inverse from the canonical
orthogonalizer. A secondary basis missing an element, a mismatched ECP core
count, or a projected occupied space that is near-singular in the target
basis are errors. A secondary basis larger than the target is allowed, with
a warning. The guess is refused with symmetry = true and on periodic
systems. An explicit starting density or MOs take precedence, as they
do over guess = "minao". With guess_basis set, guess = "minao" | "sad" | "hcore" selects the starting point of the secondary SCF only; the target
starts from the projection. The dual-basis energy correction (Q-Chem
DUAL_BASIS_ENERGY) is not implemented.
A secondary SCF that does not converge never stops the run. The target
SCF then starts from its default guess, a warning is printed, and the report
says converged = false, why, and used_as_guess = false. Projecting the
unconverged last iterate was tried and rejected: on CN UHF/aug-cc-pVDZ with
an STO-3G secondary (which oscillates for 200 iterations), the target
started from it failed to converge as well, for both projections, while the
default guess converges in 22 iterations. The result reports the secondary
SCF's iterations, energy and convergence (Python: result.guess_basis). The
report is kept when a later stage supplies the final result: a stability
descent, a later ladder rung, or the exact stage after a DF pre-stage.
Cost on re-solves. Every solve without an explicit starting point runs the secondary SCF again: each geometry of an optimization and each displaced geometry of a finite-difference Hessian. A ladder's later rungs, the stability descent's re-solves and the cDFT λ loop start from a carried density or MOs, so they do not repeat it (cDFT builds the guess once, before the λ loop).
The guess can select the state. The converged energy is not guaranteed to equal the default-guess energy. Like any guess, it decides which stationary state the SCF falls into, in either direction:
- NO UKS-PBE/aug-cc-pVDZ: MINAO −129.7833808133, BASIS2 −129.7833798480 Ha. These are two states 0.97 µHa apart, and BASIS2 lands on the higher.
- He₂ at 0.3 Å RHF/aug-cc-pVTZ: BASIS2 and the core guess give −3.2458864766 Ha, while MINAO gives −3.2388590827 Ha. Here BASIS2 lands 7 mHa lower.
Use check_stability (and stability_descent) when the state matters.
What it buys, measured in ferric (build 352eabab9, density_conv 1e-8;
target-SCF iterations, with the secondary SCF's own iterations listed
separately; converged energies agree to ≤ 3e-11 Ha in every row; wall times
were taken on a loaded machine and are not quoted):
| system | MINAO | hcore | BASIS2 fock | BASIS2 overlap | secondary SCF |
|---|---|---|---|---|---|
| NH₂ UHF 6-31G→aDZ | 14 | 18 (other state, +0.081 Ha) | 13 | 14 | 13 |
| NH₂ ROHF 6-31G→aDZ | 12 | – | 11 | 12 | 11 |
| CN UHF 6-31G→aDZ | 22 | no convergence in 200 | 15 | 16 | 24 |
| CN ROHF 6-31G→aDZ | 18 | – | 13 | 14 | 18 |
| O₂ UHF triplet 6-31G→aDZ | 12 | 14 | 10 | 11 | 11 |
| water dimer RHF 6-31G→aDZ | 14 | 17 | 13 | 13 | 13 |
| water dimer RHF 6-31G→aTZ | 14 | 17 | 14 | 14 | 13 |
| water dimer RHF cc-pVDZ→aTZ | 14 | 17 | 14 | 14 | 14 |
On closed shells the saving over MINAO is 0 to 1 target iterations, so there is no wall-time win. On open-shell radicals it saves up to 7 target iterations (CN), but the secondary SCF costs about as many iterations as it saves, or more; they are cheap iterations in a small basis. The value is a better starting point on hard open-shell cases, not a faster closed-shell SCF. Fock projection beats overlap projection by 0 to 1 iterations.
Screening
- Schwarz bounds on every 4-centre path, built so they can never underestimate (zero-valued table entries are floored; left unfloored, one such entry was measured to cost 1.5e-4 Ha)
- LinK (Ochsenfeld, White & Head-Gordon 1998) — exchange via significant-pair and density-pair lists; its scaling is being re-measured
- QQR (Maurer, Lambrecht & Ochsenfeld 2012) is implemented and validated as a bound but is not used in production: on LinK it screened only 0.009% more quartets than Schwarz at alkane_16 (measured against a LinK pair-list implementation that skipped quartets; not yet repeated on the current lists)
- COSX shell-pair screening uses a primitive-level Hölder bound that provably never underestimates; an overlap-based bound can underestimate, which silently corrupts K
QM/MM embedding
QM/MM (electrostatic, polarizable and Gaussian-smeared embedding, link atoms,
boundary-charge schemes, and a [qmmm] CLI section that reads a PQR) has its
own page: QM/MM, including its CLI section.
Determinism
The Fock build's reduction folds partial matrices in a strict ascending group
order, independent of thread count and of the memory band width. Results are
bit-identical across RAYON_NUM_THREADS, a property pinned by tests.
A tree-fold reduction would be deterministic but would produce different bits, since floating-point addition is not associative, so the sum must run in ascending order.
Cite
DIIS: Pulay 1980. MOM: Gilbert, Besley & Gill 2008. LinK: Ochsenfeld, White & Head-Gordon 1998. COSX: Neese et al. 2009; overlap fit: Izsák & Neese 2011. CSB/CSAM screening: Thompson & Ochsenfeld 2017. DFT grids: Becke 1988, Treutler & Ahlrichs 1995, Lebedev & Laikov 1999. Functionals through libxc (Lehtola et al. 2018): PBE, B3LYP, ωB97X-V, SCAN, r2SCAN, VV10. D3(BJ): Grimme et al. 2010, Grimme, Ehrlich & Goerigk 2011. IEF-PCM: Cancès, Mennucci & Tomasi 1997; COSMO: Klamt & Schüürmann 1993. P-RFO: Banerjee et al. 1985; Bofill 1994. Full entries in References.
DFT derivations
The exchange-correlation pieces of the Kohn–Sham solver, each stated in the
code's conventions and proved by a sympy script that exits nonzero on a
mismatch. The full equations, function locations and test cross-references
are in
crates/ferric-dft/theory.md.
Each script also checks mutated formulas (a dropped factor of 2,
a wrong libxc packing slot) and requires them to fail.
Conventions
Closed shell, total density matrix \( D \), real AOs \( \chi_\mu \):
\[ \rho = \sum_{\mu\nu} D_{\mu\nu}\chi_\mu\chi_\nu,\qquad \sigma = |\nabla\rho|^2,\qquad \tau = \tfrac12 \sum_b \sum_{\mu\nu} D_{\mu\nu},\partial_b\chi_\mu,\partial_b\chi_\nu . \]
Spin-polarized variables use libxc's packing: \( (\rho_\alpha,\rho_\beta) \), \( (\sigma_{\alpha\alpha},\sigma_{\alpha\beta},\sigma_{\beta\beta}) \) with \( \sigma_{\alpha\beta} = \nabla\rho_\alpha\cdot\nabla\rho_\beta \), and \( (\tau_\alpha,\tau_\beta) \). \( v_\sigma = \partial(\rho\epsilon)/\partial\sigma \), not twice it. Meta-GGAs are \( \tau \)-only (no density Laplacian).
What is proved
| Piece | Identity | Script |
|---|---|---|
| Becke partition | \( \partial w^A/\partial \mathbf R_C \) as coded, and \( \nabla_{\mathbf r} w^A = -\sum_C \partial w^A/\partial\mathbf R_C \) (used to build the grid-weight response) | scripts/dft_becke_weight_grad_sympy.py |
| Semilocal \( V_{xc} \) | \( \partial E_{xc}/\partial D_{\mu\nu} = \sum_g w_g [v_\rho\chi_\mu\chi_\nu + 2v_\sigma\nabla\rho\cdot\nabla(\chi_\mu\chi_\nu) + \tfrac12 v_\tau\nabla\chi_\mu\cdot\nabla\chi_\nu] \); polarized cross term \( v_{\sigma_{\alpha\beta}}\nabla\rho_\beta \) with coefficient 1; equal to \( v_\rho - 2\nabla\cdot(v_\sigma\nabla\rho) \) up to a divergence | scripts/dft_vxc_matrix_elements_sympy.py |
| \( f_{xc} \) kernels | GGA response \( (u^\sigma, \mathbf w^\sigma) \) is the linearization of \( (v_{\rho_\sigma}, 2v_{\sigma_{\sigma\sigma}}\nabla\rho_\sigma + v_{\sigma_{\alpha\beta}}\nabla\rho_{\bar\sigma}) \), with libxc's packed second derivatives | scripts/dft_fxc_kernel_sympy.py |
| VV10 | per-point \( v_\rho, v_\sigma \) are \( \partial E_{nl}/\partial\rho_i, \partial E_{nl}/\partial\sigma_i \) (self pair included) | scripts/dft_vv10_potential_sympy.py |
| XC nuclear gradient | \( \partial\tau/\partial R_{A,\alpha} = -\sum_b\sum_{\mu\in A}\partial^2_{\alpha b}\chi_\mu (D\partial_b\chi)\mu \): the \( \tfrac12 \) in \( \tau \) cancels against the \( \mu\leftrightarrow\nu \) doubling; grid-response terms \( \partial\alpha\nabla\rho \), \( \partial_\alpha\tau \) | scripts/dft_mgga_tau_gradient_sympy.py |
The numerical checks of the same identities (finite differences of the
energy, PySCF references) are listed per section in theory.md.
Known open issue
The VV10 AO matrix \( V_{nl} \) is not exactly \( \partial E_{nl}/\partial D \):
on NH₂/6-31G the directional derivative misses by \( 3.6\times10^{-6} \)
relative (\( -1.31\times10^{-7} \) absolute), independent of the step size,
while ωB97X without VV10 agrees to \( 8.6\times10^{-14} \). The per-point
partials are proved exact, so the residual lies elsewhere on the path from
\( D \) to \( V_{nl} \); the cause is not identified. The test is a strict
xfail (crates/ferric-python/tests/test_xc_blocks.py).
The MP2 family
The largest method family here. Every variant is density-fitted (RI): each
needs an orbital basis and a matching RI auxiliary basis ([mp2] auxbasis, or
the auxbasis argument in Python). Formal cost is O(N⁵) for the RI-MP2
transformation. Grades per method.kind are on
Capabilities and validation. The [mp2] keys are in
Input file. The working equations of every
variant on this page (spin factors, frozen core, operator and unit
conventions), with the code path behind each, are in
crates/ferric-mp2/theory.md;
the identities it relies on are checked by
scripts/mp2_variant_identities.py.
RI-MP2
What it is. Second-order Møller–Plesset theory on an RHF reference (UHF for an open shell, below) with 3-centre/2-centre density fitting. Canonical (non-RI) MP2 is also implemented, for cross-validation, not production.
Open shells. rimp2 and oo-rimp2 accept an open-shell molecule
(multiplicity > 1) from the CLI, for task = "energy" only: they solve the
same plain UHF that kind = "uhf" runs, then take unrestricted RI-MP2 (UMP2,
as PySCF mp.MP2(uhf)) or unrestricted OO-RI-MP2. There is no unrestricted
MP2 nuclear gradient, and [mp2] kappa is refused on an open shell. From
Python, run_rimp2 does the same (UHF + UMP2 when multiplicity > 1;
result.reference says which, and kappa raises); run_oo_rimp2 is closed
shell only. Both unrestricted methods are validated on the CH3 doublet at
cc-pVDZ: U-RI-MP2 against ORCA RI-MP2 NoRI, U-OO-RI-MP2 against an
independent numpy OO-RI-MP2 (see the
anchors).
The other MP2-family kinds (mp3, att-rimp2, scs-mp2, scs-mp2-2terfc,
laplace-mp2, laplace-sos-mp2, rs-mp2-rpa) and their Python drivers are
closed shell only: set multiplicity = 1. mp2-v switches to a UHF reference
when multiplicity > 1 (CLI, energy only); Python run_mp2_v is closed shell
only.
The reference SCF: exact, RI-JK or RIJCOSX. The auxbasis above fits the
correlation step only. The reference SCF is a separate choice, scf_jk on
every MP2-family Python driver ([scf] jk in the CLI, task energy):
"exact" (four-centre J and K, the default), "rijk" (RI-J + RI-K) or
"rijcosx" (RI-J + COSX K). The fitted modes use jk_auxbasis ([scf] jk_aux), a JK-fit basis with default def2-universal-jkfit. It is never the
MP2 auxbasis, because an MP2-fit set used for K costs mHa. rs-mp2-rpa is the
exception: its default is "rijk". Every result
echoes scf_jk and jk_auxbasis. An unknown mode, jk_auxbasis with
"exact", or a k_builder the mode would override is an error (so is any
k_builder on rs-mp2-rpa's RI-JK default, in the CLI as in Python). A
jk_auxbasis whose name lacks "jkfit" draws a warning. oo-rimp2 accepts only
"exact": orbital optimisation rebuilds exact four-centre J/K at every orbital
step, so a fitted reference would change only the starting orbitals; threading
RI-JK through the OO Fock builds is not implemented. In the CLI a fitted
reference cannot be combined with [scf] df_increments (an exact-J/K scheme)
or an explicit df_guess = true, and it skips the default DF-guess
pre-stage. At aug-cc-pVTZ
the four-centre SCF is most of an MP2 single point, so a fitted reference is the
usual choice for interaction-energy benchmarks. The fitting error is small and
largely cancels in a counterpoise-corrected interaction energy. Measured with
PySCF DF-MP2 (frozen core, aug-cc-pvXz-ri for MP2) on the S66 water dimer:
| Basis | JK aux | ΔE_total(dimer) | ΔIE (CP) |
|---|---|---|---|
| aug-cc-pVDZ | def2-universal-jkfit | +98 µHa | +0.0059 kcal/mol |
| aug-cc-pVDZ | aug-cc-pvdz-jkfit (not bundled) | +58 µHa | +0.0001 kcal/mol |
| aug-cc-pVTZ | def2-universal-jkfit | +69 µHa | +0.0043 kcal/mol |
| aug-cc-pVTZ | cc-pvtz-jkfit | +78 µHa | +0.0026 kcal/mol |
| aug-cc-pVTZ | aug-cc-pvtz-jkfit (not bundled) | +63 µHa | <0.00001 kcal/mol |
Run it. method.kind = "rimp2" (examples/water-rimp2.toml,
examples/water-rimp2-frozen-core.toml); Python ferric.run_rimp2(mol, bs, aux).
Accuracy. Proven. RI-MP2 is size-extensive to 2e-12 Ha for a
well-separated dimer against twice the monomer
(ferric-mp2/tests/rimp2_size_extensivity.rs, which asserts 1e-7).
Knobs. frozen_core defaults to 0 (all electrons correlated). Several
published parameterizations below assume frozen core, so set it when you use
them.
Attenuated MP2
What it is. MP2 with the Coulomb operator in the correlation energy replaced by a short-range one. Two forms are implemented:
- erfc: \( \mathrm{erfc}(\omega r)/r \), which is \( 1/r \) at short range and decays to zero beyond roughly \( 1/\omega \).
- terfc: a short-range operator that is Coulombic inside a cutoff radius
\( r_0 \) and attenuated outside it, with a controlled curvature
(Dutoi & Head-Gordon 2008). It needs precomputed interpolation tables
(
FERRIC_TERF_TABLE_DIR, generated underterf-tables/); without them the run errors rather than substituting another operator.
Why. MP2's error for non-covalent interactions has two parts that partly cancel in small basis sets: basis-set superposition error, and an error in the long-range part of the correlation energy. Attenuation removes the long-range part, so attenuated MP2 has no long-range dispersion: its asymptotic \( C_6 \) is zero. The method works in the basis and on the systems it was fitted for; MP2-V (below) adds long-range dispersion back through VV10.
Parameterization. The attenuation parameters are fitted, and each fit belongs to one basis. The aug-cc-pVTZ sets that ferric ships as
defaults were fitted on S66 without counterpoise and with frozen core
(stated in crates/ferric-mp2/src/scs.rs):
| Variant | Parameters | Basis of the fit | Source |
|---|---|---|---|
| erfc | ω; ferric's default is 0.420 Å⁻¹, recorded in the code as the dissertation's erfc optimum | aug-cc-pVDZ in the 2012 paper | Goldey & Head-Gordon 2012 |
| terfc | one cutoff \( r_0 \) | aug-cc-pVTZ | Goldey, Dutoi & Head-Gordon 2013 |
| SCS-MP2(2terfc) | \( r_0(1) \) = 0.75 Å, \( r_0(2) \) = 1.05 Å, cOS = 1.27, cSS = 4.05 | aug-cc-pVTZ, no counterpoise, frozen core | Goldey & Head-Gordon 2014 |
| MP2-V | \( r_0 \) = 1.00 Å, b = 11.0, C = 0.0089, terfc | aug-cc-pVTZ, no counterpoise, frozen core | Goldey, Belzunces & Head-Gordon 2015 |
Running a fitted variant in another basis, with counterpoise, or with
frozen_core = 0 (the default) is extrapolation outside the fit.
Run it.
| Variant | method.kind | Example | Python |
|---|---|---|---|
| erfc | att-rimp2 | examples/water-attmp2.toml | run_attenuated_rimp2(..., omega=0.420) (Å⁻¹) |
| terfc | att-rimp2 with [mp2] att_operator = "terfc", att_r0 (Å) | examples/water-attmp2-terfc.toml | run_terfc_rimp2(..., r0=...) |
| SCS-MP2 (Grimme) | scs-mp2 | examples/water-scs-mp2.toml | run_scs_mp2(..., c_os=, c_ss=) |
| SCS-MP2(2terfc) | scs-mp2-2terfc | examples/water-scs-mp2-2terfc.toml | run_scs_mp2_2terfc(...) |
| MP2-V | mp2-v | examples/water-mp2v.toml | run_mp2_v(...) |
ω is in Å⁻¹ in the CLI and Python and in Bohr⁻¹ in the Rust API. The SCS-MP2 defaults are Grimme's cOS = 1.2, cSS = 0.333.
Accuracy. att-rimp2, scs-mp2 and scs-mp2-2terfc are Proven (the erfc
energy is pinned against PySCF/libcint in
testdata/reference/h2o_cc-pvdz_attenuated-rimp2-erfc0p420.json). mp2-v's
implementation is Proven (narrow): the damped VV10 half, the terfc-attenuated MP2
half and the erfc control each match an independent PySCF/numpy reference. Its
parameterization is unbenchmarked: there is no published MP2-V total energy to
compare with, only an aug-cc-pVTZ S66 subset run (n=5, H-bonded/mixed, S66x8 geometry, no CP-anchored reference), and ferric evaluates the VV10 term on the converged HF
density (the post-HF variant) on its own 50×50 grid rather than SG-1. Read the header of examples/water-mp2v.toml before quoting a number.
Fitting metric. Each attenuated energy fits its integrals in the metric of the same operator, \( (P|\hat w|Q) \), so the fit is variational for that operator. Robust (Dunlap) fitting is required only when the metric operator differs from the integral operator.
The range split is not additive. MP2 is quadratic in the integrals, so the erfc and erf energies at the same ω do not sum to the Coulomb MP2 energy; the difference is the short-range × long-range cross term. RS-MP2 + LR-RPA (below) is built on it.
RS-MP2 + LR-RPA
What it is. Short-range MP2 plus long-range direct RPA: the attenuated-MP2 idea with the missing long-range correlation supplied by response rather than by a fitted dispersion term. Two formulations:
delta-lr(default): \( E_{MP2}[\text{Coulomb}] + (E_{dRPA}[\text{erf}] - 2E_{OS}[\text{erf}]) \), one dRPA call.coupled-rings: \( E_{MP2}[\text{Coulomb}] + \Delta dRPA[\text{Coulomb}] - \Delta dRPA[\text{erfc}] \), two dRPA calls; includes the mixed short/long-range ring diagrams.
Run it. method.kind = "rs-mp2-rpa" (examples/water-rs-mp2-rpa.toml,
[mp2] formulation); Python run_rs_mp2_rpa(..., omega=0.420, formulation="delta-lr").
Accuracy. Smoke. The ω→0 and ω→∞ limits reduce exactly to MP2 and to MP2 + dRPA; numbers at production ω are not established on new systems.
Orbital-optimized MP2 and MP3
OO-RI-MP2 (oo-rimp2, examples/water-oo-rimp2.toml, run_oo_rimp2):
orbitals optimized for the MP2 Lagrangian, with a level-shifted Newton step,
orbital DIIS, Cayley rotations and backtracking. Proven (narrow): the energy
matches an independent numpy OO-RI-MP2 to 7.5e-13 Ha (H2O, NH3, UHF CH3 at
cc-pVDZ), and the closed-shell analytic gradient matches a finite difference of
its own energy to 8e-9 Ha/Bohr. ORCA 6.1.1's OO-RI-MP2 stops 3.7e-8 to
7.0e-8 Ha above the same minimum, so it is only a loose cross-check.
MP3 (mp3, examples/water-mp3.toml, run_mp3): spin-orbital
third-order Møller–Plesset through the einsum! framework. Proven.
Laplace formulations
RI-Laplace MP2 (laplace-mp2, examples/water-laplace-rimp2.toml,
run_laplace_mp2): MP2 through a Laplace transform of the energy denominator,
using pseudo-density matrices in the AO basis. The implementation is dense.
It is the correctness reference for that formulation; no reduced scaling has
been measured.
Laplace SOS-MP2 (laplace-sos-mp2, examples/water-laplace-sos-mp2.toml,
run_laplace_sos_mp2): opposite-spin-only MP2 (Jung et al. 2004,
cOS = 1.3 by default) with a minimax Laplace quadrature
(n_quad must be 3, 5 or 7; anything else is an error). There is
no cSS: dropping the same-spin term is what lets the denominator
factorize. With cOS = 1.0 it reproduces the opposite-spin component
of RI-MP2 to quadrature error, which is the test anchor. Three formulations
(sos_formulation):
mo(default) andaoare both exact and agree to round-off.ao-sparserestricts each Boys-localized orbital's pseudo-density to an AO domain of radiusdomain_cutoff_bohr(required, no default). It is the one approximate variant.
What is measured for ao-sparse:
- Against the exact AO path on n-alkanes (cc-pVDZ, cOS = 1,
n_quad= 7, 2026-07-28), chemical accuracy (absolute error below 1.6 mHa) needs a domain radius of 3, 3, 3, 4, 5 and 5 Bohr for C2, C4, C6, C8, C10 and C12. The diameter grows fivefold over that series, so radius/diameter falls from 0.52 to 0.17. - In the STO-3G tests, a 12 Bohr domain that is exact for butane (10.5 Bohr across) is also exact for octane (19.9 Bohr across), and a 4 Bohr domain on butane is already within 0.1%.
- A 71-atom drug molecule (danuglipron, 31.3 Bohr across, STO-3G) is within 0.05% at 4 Bohr.
The test sos_ao_sparse_truncation_radius_is_transferable_across_sizes in
crates/ferric-mp2/src/laplace.rs pins the STO-3G butane/octane comparison:
at 12 Bohr butane is exact (relative error below 1e-9) and octane is
within 1e-6, and octane at 3 Bohr is worse than at 12 Bohr. The
4 Bohr butane figure and the danuglipron run (recorded in the test's doc
comment) are measurements, not assertions. The alkane sweep is kept in the project's working notes.
How to read it (provisional): the radius needed grows, but far more slowly than the molecule. That points to a finite decay length rather than strict saturation: no single radius is shown to suffice at every size.
What is not claimed: any speedup. The domains discard contributions but the tensor algebra is still dense, so there are no timings to report.
Exact and local MP2
method.kind = "rimp2" is the method; it is computed exactly unless a
[local] section asks for a local
approximation (see
Exact and local correlation).
The local approximation is closed shell and energy only.
Amplitude-threshold local MP2 (examples/water-rimp2-local.toml;
[local] scheme = "amplitude-threshold", eps = 1e-4; Python
run_rimp2(..., local="amplitude-threshold", eps=1e-4)): the
single-threshold local MP2 of Wang, Aldossary, Shi, Liu, Li & Head-Gordon
(2023), with localized virtuals and per-pair domain-local RI fits. eps has
no default: it is part of the model. eps = 0 reproduces RI-MP2 exactly; a
finite eps carries a one-sided truncation error that grows faster than
linearly in eps.
Every printout and run-log record of a local run states eps and the
fraction of amplitudes kept.
The amplitude equations are solved by preconditioned conjugate gradients to a
relative residual of 1e-6 by default. The energy is the stationary Hylleraas
form, so its solver error is second order in that residual. Using that form with
a loose stop comes from the method's authors: Wang et al. (JCTC 19, 7577, 2023;
Eq. 10 and Sec. 3.1) stop at an absolute ‖r‖ < 2.5·√eps, and the 2025 follow-up
(ChemRxiv preprint of JCTC 21, 10910) at 5·√eps. ferric uses a fixed relative tolerance instead and
gates it on a per-run bound, 3‖PJ‖²·relres²/λ_min with
λ_min ≥ 2(ε_LUMO − ε_HOMO). The bound holds for the true residual, up to rounding,
so it is evaluated on ‖J + At‖ from the energy's own matvec, not on the solver's
recursive residual. If the bound at the solver's stop exceeds 1e-10 Ha, the solve
continues from where it stopped to a tighter tolerance, never past 1e-11; with no
positive orbital gap the bound is undefined and the solve runs at 1e-11 from the
start. Running out of iterations before the tolerance being sought, first or
tighter, stops the run with an error; cg_max_iter and an explicit cg_rtol are
fields of the Rust AmplitudeLmp2Config, not CLI or Python options. The certificate (result.local in Python, "local" in the CLI JSON) records
cg_rtol, cg_rtol_source, cg_tightened, cg_relres, cg_true_relres,
cg_energy_error_bound, cg_energy_error_target and cg_energy_target_met, and
solver_warnings says if a converged solve did not meet the target, because the
target needs more than the 1e-11 floor or the true residual drifted from the
recursive one. The bound limits solver error
only, not the truncation error of eps.
The exact RI-MP2 reference that measures that error is opt-in. It is a
full canonical RI-MP2 and forms the global (naux, nocc·nvir) tensor, so a
run with it switched on is not reduced-cost. In the CLI, [local] reference = true computes it and prints the local error against it; without it the
output reads
E_corr(canonical RI) = not computed (opt-in: set [local] reference = true)
and the run log's e_corr_canonical_ri is null. In Python,
compute_reference=True computes it; by default
result.local["e_corr_canonical_ri"] is None.
Integral-direct local MP2 ([local] integral_direct = true,
examples/alkane8-rimp2-local-direct.toml; Python integral_direct=True) is
the reduced-cost path. Its correlation assembly never forms the global
3-index tensor; only the opt-in reference does. Locality comes from an
integral-free pair gate (gate_cal), per-occupied auxiliary-fit and virtual
domains (aux_radius, virt_radius), and truncation of each orbital's AO
support (ao_tail). With every map at its trivial setting it reproduces the
global 3-index path and canonical RI-MP2 (tests/lmp2_direct.rs).
What is measured for the integral-direct local MP2 (n-alkanes C20 → C48, 6-31G with
cc-pVDZ-RI, a quiet machine, 2026-09-07; benchmark
bench_direct_alkane_series in crates/ferric-mp2/tests/lmp2_direct.rs).
The run froze the carbon cores (frozen_core = number of carbons) and
calibrated the pair gate (0.7 Coulomb, 0.02 erfc with ω = 1.0) at
eps = 1e-4; the defaults are all-electron with no pair gate. Timings are the correlation
stage (assembly + solve) and exclude the canonical reference:
- Correlation-stage cost grows as about N1.24 with the erfc kernel and N1.4 with Coulomb, fitted to the last three sizes.
- It is at about parity with canonical RI-MP2 at C20 (about 1.1× slower) and about 6× faster at C32 (5.7–6.3×).
How to read it (provisional): sub-quadratic on this series, but the fit has three points on one family of molecules in one basis. It is not shown to be linear, and it is not measured on 3-D or diffuse systems.
Schwarz triple cut (schwarz_skip, default 1e-5) applies to the Coulomb, erfc and
terfc kernels alike. A shell triple is skipped when
sqrt(max (P|P)) * sqrt(max (ab|ab)) < schwarz_skip, both factors from the run's own
operator; schwarz_skip = 0 evaluates every triple. For terfc the factors come from the
table engine's own diagonals and carry a 1e-5 relative inflation for table error, and the
cut is only available inside the checked box: orbital shells up to g, r0 between 0.3 and
2.5 Å and r0·ω ≤ 8.9 (outside it the run errors and names schwarz_skip = 0.0). Measured
for terfc (n-alkanes, 6-31G with cc-pVDZ-RI, eps = 5e-5, default maps, plain Schwarz,
2026-10-11, a loaded machine so ratios only, order alternated, two repeats):
| System | operator | wall vs schwarz_skip = 0 at 1e-6 | at 1e-5 | energy change at 1e-5 |
|---|---|---|---|---|
| C12 | linked (r0 = 1.0 Å, r0·ω = 0.707) | 2.3–2.5× faster | 2.7× | 1.5e-7 relative |
| C12 | NCP (r0 = 0.991 Å, ω = 1.3663 Å⁻¹) | 2.4–2.5× | 2.6–2.9× | 3.6e-8 |
| C16 | linked | 3.3–3.5× | 3.5–3.8× | 1.7e-7 |
| C16 | NCP | 3.1–3.9× | 3.3–4.1× | 3.6e-8 |
At 1e-5 the cut skips 55% of the shell triples at C12 and 67% at C16 (51% and 63% at 1e-6). The pair gate
(gate_cal = 0.7) leaves the C12 result unchanged. At 1e-6 the energy changes by
1e-10 (C12 linked) to 1.8e-9 (C16 NCP).
Local MP2 without integral_direct makes no scaling claim. Its assembly
is pair-local (no dense J is formed), but it still builds the global 3-index
tensor. Use it as the reference implementation and for small systems.
Cite
RI-MP2 auxiliary sets: Weigend et al. 1998. SCS-MP2: Grimme 2003. SOS-MP2: Jung et al. 2004. OO-MP2: Lochan & Head-Gordon 2007; Bozkaya et al. 2011. Laplace MP2: Häser & Almlöf 1992. Attenuated MP2: Goldey & Head-Gordon 2012; terfc: Dutoi & Head-Gordon 2008, Goldey, Dutoi & Head-Gordon 2013; SCS-MP2(2terfc): Goldey & Head-Gordon 2014; MP2-V: Goldey, Belzunces & Head-Gordon 2015. LMP2: Wang et al. 2023. Robust fitting: Dunlap 2000. Full entries in References.
Coupled cluster
"RI-CC" here means the two-electron integrals in the amplitude equations come from density fitting (three-centre B tensors from an RI auxiliary basis), not from exact four-centre integrals. Everything below needs an orbital basis and an RI auxiliary basis. Formal cost is O(N⁶) for CCSD and O(N⁷) for (T).
CCSD and CCSD(T)
What it is. RI-CCSD and the perturbative triples correction (T), in two implementations:
- Spin-adapted, closed shell (
ccsd_closed_shell,ccsd_t_closed_shell): amplitudes over spatial orbitals, the algorithm PySCF'scc.CCSDuses. This is what both the CLI and Python run for an RHF reference. Its VVVV block is 16× smaller than the spin-orbital one; measured about 8–10× faster than the spin-orbital CCSD at cc-pVDZ, and the (T) step 9.6–42× faster. - Spin-orbital (
ccsd,ccsd_t): built from the same RHF spatial orbitals and kept as the cross-check of the spin-adapted solvers, so it is closed shell only as well. Its (T) streams one occupied triple at a time, so memory is O(no·nv³)-class rather than the dense six-index tensor.
No open-shell CCD, CCSD or CCSD(T) exists in ferric: every CC solver reads
restricted orbitals and refuses an unrestricted reference. The only open-shell
coupled-cluster-type method is LinLCCD(hh): the CLI linlccd kind is
closed shell, and its open-shell (UHF) version is library-only
(ferric_cc::linlccd_u::u_linlccd).
Run it.
- CCSD:
method.kind = "ccsd"(examples/water-ccsd.toml, water/cc-pVDZ); Pythonferric.run_ccsd(mol, bs, aux). - CCSD(T):
method.kind = "ccsd(t)"(examples/water-ccsd-t.toml); Pythonferric.run_ccsd_t(mol, bs, aux). - CCD:
method.kind = "ccd"(examples/water-ccd.toml); Pythonferric.run_ccd(mol, bs, aux). - All three kinds are closed shell only and refuse
multiplicity > 1.
Aux basis. The RI error is not negligible at CC accuracy: on water /
cc-pVDZ with cc-pvdz-ri, RI-CCSD differs from exact-integral CCSD by about
1.3e-4 Ha. Choose the aux basis for the accuracy you need.
Accuracy. ccsd is Proven.
| Quantity | System / basis | Reference | Agreement | Pinned by |
|---|---|---|---|---|
| CCSD correlation energy | H2 / STO-3G | exact-integral numpy | −0.02052453 Ha | ferric-cc/src/ccsd.rs::test_ccsd_h2_sto3g |
| Closed-shell (T) | H2O / cc-pVDZ | PySCF ccsd_t() | ~1e-6 Ha (test asserts 1e-4) | ferric-cc/src/ccsd_t_closed_shell.rs::closed_shell_t_h2o_ccpvdz_matches_pyscf |
| Streaming vs dense spin-orbital (T) | H2O / cc-pVDZ | ferric's former dense path | 5e-16 Ha | ferric-cc/src/ccsd_t.rs::streaming_matches_dense_h2o_ccpvdz |
Limits. Closed-shell entry points only in the CLI and Python. The spin-adapted (T) rejects spin-orbital amplitudes with a typed error rather than mixing conventions.
Particle-particle ladder: dense or RI
The particle–particle ladder Σcd Wabcd τijcd
of closed-shell CCSD can be evaluated two ways, chosen by [cc] ladder
(Python run_ccsd(ladder=) / run_ccsd_t(ladder=)):
denseholds the chemist (ab|cd) block in core and builds Wabcd every iteration: nv⁴ memory, a few times over.riholds neither the (ab|cd) block nor Wabcd. It rebuilds the (ac|bd) entries from the RI 3-index tensors every iteration in ablk·nv³ panels: at most one nv⁴-sized panel at full width (dense holds four nv⁴ buffers at its peak), and narrower panels when the budget requires. It is the same algebra over the same tensors, so it is exact: on water/cc-pVDZ |ΔE| and max|Δt| are ≲ 3e-17 (round-off), and the alkane energies below agree to all 10 printed digits.auto(the default) runsdensewhenever its memory plan fits the budget andriotherwise, so it is never refused whereriwould run. When it falls back toriit says so on stderr. The route taken is printed (ladder = dense (auto)), logged, and returned (CcResult::ladder_route; PythonCcResult.ladder_route).
Reproducibility. Under auto, a closed-shell CCSD job whose dense plan
exceeds the memory budget runs on the RI ladder instead of being refused.
Because auto decides against the budget, a run without an explicit
[memory] budget_gb (auto-detected as 0.8 × the RAM available at start-up)
can take different routes on the same input from one run to the next. The
energies agree to round-off either way; the time does not. Set ladder
explicitly, or fix the budget, for reproducible timings.
Measured 2026-10-26, alkane/cc-pVDZ (cc-pvdz-ri), OPENBLAS_NUM_THREADS=1,
RAYON_NUM_THREADS=4, routes interleaved one per process, load average 3–6
from other jobs (benchmarks/harness/examples/ri_ccsd_memory.rs). RI ran at
full block width (ablk = nv, the default-budget
choice). Per-iteration time is (time for k iterations − setup time) / k,
repeated 4× per route for water, 3× for C3H8 and 2× for C4H10/C5H12;
ratios are of the medians. Peak RSS is one converged run per route.
| System | nv | Per iteration, dense | Per iteration, RI | RI / dense | Peak RSS, dense | Peak RSS, RI |
|---|---|---|---|---|---|---|
| water | 19 | 7.8 ms | 8.3 ms | 1.06 | — | — |
| C3H8 | 69 | 1.27 s | 1.64 s | 1.30 | 1.50 GB | 0.70 GB |
| C4H10 | 89 | 3.2–3.7 s | 5.2–5.3 s | 1.50 | 3.79 GB | 1.67 GB |
| C5H12 | 109 | 8.8–10.8 s | 14.5–14.6 s | 1.48 | 7.90 GB | 3.40 GB |
RI is 1.3–1.5× slower per iteration from nv ≈ 70 up and uses
2.1–2.3× less memory, which is why auto takes it only when dense does
not fit. The RI route still holds the no·nv³
(ia|bc) block in core, so it lowers the nv⁴ ceiling but does
not remove every large tensor.
LinLCCD and ωB97X-L-V
LinLCCD(hh) is linearized coupled-cluster doubles with the hole–hole ladder
kept to all orders, closed shell only. The ladder keeps the correlation energy
finite as the HOMO–LUMO gap closes, where MP2 diverges.
method.kind = "linlccd" (examples/water-linlccd.toml). Proven (narrow):
no other quantum chemistry code implements LinLCCD(hh), so its energy is
checked against an independent numpy solve of the same equations on PySCF
density-fitted integrals (H2O and NH3 through the closed-shell path, and UHF
OH through the library-only open-shell path; agreement ≤1.2e-12 Ha). With
the ladder off it reduces exactly to RI-MP2, and with exact integrals its
driver terms reproduce canonical MP2.
[mp2] linlccd_variant (Python run_linlccd(variant=...)) selects the
method's ladder terms: hh (default), drivers-only (no ladder, equal to
RI-MP2) or full (hole–hole plus particle–particle, with CCD-like VVVV
memory). Every variant is computed exactly by default. All three variants
match an independent numpy solve on the same density-fitted integrals
(H2O and NH3, 6-31G and cc-pVDZ, ≤ 4.4e-13 Ha); drivers-only also matches
PySCF DFMP2.
Local LinLCCD. With [local] scheme = "amplitude-threshold" and eps
(examples/water-linlccd-local.toml; Python
run_linlccd(..., local="amplitude-threshold", eps=1e-4)) the same variant
is solved in the Boys-localized basis with pair amplitudes at or below eps
dropped (see Exact and local correlation).
eps has no default and is printed and logged with the kept fraction;
eps = 0 reproduces the exact LinLCCD of the same variant and the numpy
reference (≤ 4.4e-13 Ha, every variant), which is how the local path is
anchored. [local] reference = true also runs the exact
LinLCCD and prints the local error. Closed shell and energy only.
ωB97X-L-V is a double-hybrid functional that uses short-range LinLCCD(hh)
instead of MP2 for its correlation term. It converges its own ωB97X-L
Kohn–Sham reference (a non-converged reference is an error), then adds the
LinLCCD(hh) correction on those orbitals. method.kind = "wb97x-l-v"
(examples/water-wb97xlv.toml). [dft] lambda and omega override the
published 0.6 and 0.1 Bohr⁻¹; omitting them gives the published values.
λ enters the amplitude equations as well as the energy (the paper's eqn 22), so
the correlation term is quadratic in λ at leading order. Validated against PySCF
with the published parameters plus a numpy LinLCCD(hh) on water and OH, and
against the paper's Be₂ bond energy (2.3 kcal/mol; ferric 2.299).
MP2-based double hybrids
B2PLYP and DSD-PBEP86: a KS reference with weighted exchange and
correlation components, plus scaled (SCS-)RI-MP2 correlation.
method.kind = "b2plyp" / "dsd-pbep86" (examples/water-b2plyp.toml);
Python ferric.run_double_hybrid(mol, bs, aux, kind="b2plyp"). Spike: no
comparison to a reference code yet.
Implementation
The equations each solver implements, with file and line, are in
crates/ferric-cc/theory.md.
All contractions go through einsum!, a macro that maps tensor
contractions onto BLAS3 GEMMs. The permutation copies that feed those GEMMs are
parallelized, because for a strided permutation the copy can dominate the
contraction it feeds: measured at 47% at nv = 40 and 70% at
nv = 80. The copies are bit-identical regardless of thread count,
since a permutation writes each output element once; a test pins this.
Memory
The amplitude tensors dominate and grow as \( n_o^2 n_v^2 \), or \( (2n_o)^2 (2n_v)^2 \) in the spin-orbital drivers. Memory budgets are enforced: an oversized job is refused with a breakdown naming the dominant term instead of being OOM-killed partway through.
Cite
CCSD: Scuseria, Janssen & Schaefer 1988; spin-adapted closed-shell CCSD equations: Hirata et al. 2004. (T): Raghavachari et al. 1989; closed-shell (T) algorithm: Rendell, Lee & Komornicki 1991. LinLCCD(hh): Carter-Fenk 2025. ωB97X-L-V: Ransford & Carter-Fenk 2026. B2PLYP: Grimme 2006. DSD-PBEP86: Kozuch & Martin 2011. Review: Bartlett & Musiał 2007. Full entries in References.
CAS-CI
ferric runs complete-active-space configuration interaction (CAS-CI) from a closed-shell RHF reference. It is a Rust library routine only: the CLI and the Python bindings do not expose it.
The Hamiltonian, the determinant phase convention and the Slater-Condon rules
are derived in crates/ferric-ci/theory.md. The phase conventions are checked
by scripts/casci_slater_condon_phases_sympy.py.
Scope
| Implemented | |
|---|---|
| Space | full CI inside n_active spatial orbitals; no RAS1/RAS3 restrictions |
| Reference | restricted (closed-shell) RHF, a singlet with n_alpha == n_beta in the active space |
| Roots | the single lowest root |
| Solver | CI-native Davidson, diagonal preconditioner 1/(H_ii - λ) |
| Entry points | Rust only: ferric_ci::run_cas_ci (re-exported as ferric::ci) |
| Not implemented | open-shell or non-singlet references, S² control, multiple roots, CLI method.kind, Python function |
With no inactive orbitals the method is FCI in the basis. A call that fails
the bookkeeping check, 2 * active_start + n_alpha + n_beta == nelec, returns
an error. So does a non-restricted reference, n_active == 0, or
n_active > 63 (the u64 string limit).
Energy
$$ E_\text{CASCI} = E_\text{core} + \lambda_\text{min}(H_\text{act}), $$
where $E_\text{core}$ is the nuclear repulsion plus the energy of the
active_start doubly occupied inactive orbitals, and $H_\text{act}$ carries
the inactive-core Fock potential in its one-electron part. e_active is
e_total - e_core, the quantity PySCF reports as CASCI.e_cas.
Library use
use ferric_ci::{run_cas_ci, CasCiConfig};
// `mol`, `prep` and `rhf` come from a converged restricted solve_rhf
// with exact J/K (no density fitting), as in the validation tests.
let cfg = CasCiConfig {
n_active: 6,
n_elec_active: (3, 3),
active_start: 4, // MOs 0..4 are doubly occupied and inactive
..Default::default()
};
let res = run_cas_ci(&mol, &prep, &rhf, cfg)?;
println!("{res}"); // energy, determinant count, iterations
CasCiResult holds e_total, e_active, e_core, n_determinants,
ci_vector (index ia * n_beta + ib), converged and iterations.
Defaults: conv_thresh = 1e-9, max_subspace = 50, max_iter = 200.
Limits
- The active-space integrals come from a dense AO ERI tensor,
O(nbasis⁴)memory. Use it on minimal and double-zeta basis sets. - The sigma build is an
O(N_det²)loop over determinant pairs. It does not use the string-based Knowles-Handy/Olsen algorithm. - The Davidson subspace needs
2 * max_subspace * N_det * 8bytes.memory_budget_bytesbounds the integral transform and the subspace;Noneresolves throughFERRIC_MEM_BUDGET_GB, then 0.8 of available RAM, then a 2 GiB floor.
Validation
Compared with PySCF mcscf.CASCI on an exact-integral RHF with the same
orbital window, and with a dense diagonalization of the full active-space
Hamiltonian:
| System | Active space | Basis | Largest difference |
|---|---|---|---|
| N2, r = 1.10 and 2.20 Å | CAS(6,6) | cc-pVDZ | E_CASCI 1.4e-11 Ha |
| H2O | CAS(4,4) | 6-31G | e_core and e_active 2.6e-10 Ha |
Each system is also run with the window moved down one orbital. The test tolerance is 3e-9 Ha. The stretched N2 reference is the symmetric RHF, which is internally unstable toward a symmetry-broken RHF 0.19 Ha lower; both codes use the symmetric state.
The sign convention is checked three ways, because energy comparisons cannot detect a diagonal $\pm1$ similarity of $H$:
tests/slater_condon_brute_force.rscomparesgeneral_element,sigmaand the diagonal with an independent bit-loop second-quantized $H$ on random asymmetric integrals, CAS(4,4) to CAS(6,6), odd and even $N_\alpha$. The largest difference is 8.9e-15.scripts/casci_slater_condon_phases_sympy.pybuilds the Jordan-Wigner operators symbolically, verifies the anticommutation relations, and shows that a line-for-line port ofgeneral_elementequals the explicit $H$ as an exact linear form in $h_{pq}$ and $(pq|rs)$ for four $(n, N_\alpha, N_\beta)$ sectors. Four sign mutations and the interleaved orbital order each fail.- The PySCF energies above fix the spectrum.
The grade is on Capabilities and validation.
Sources: crates/ferric-ci/tests/validation_casci.rs and
scripts/validation/gen_casci.py.
RPA, GW and excited states
Methods built on the density–density response function: RPA correlation
energies, GW quasiparticle energies, BSE and TDDFT excitations, and
polarizabilities and \( C_6 \) coefficients. All need an RI auxiliary basis
([rpa] auxbasis). The [rpa], [gw] and [tddft] keys are in
Input file; grades are on
Capabilities and validation.
What PDEP does in ferric
The independent-particle response \( \chi_0(i\omega) \) is built in the RI
auxiliary basis from the three-centre B tensors, as an explicit
(Adler–Wiser) sum over every occupied–virtual pair \( ia \), weighted by
\( 4\varepsilon_{ia}/(\omega^2 + \varepsilon_{ia}^2) \)
(sternheimer::dielectric_matrix). The sum over empty states is still
there.
PDEP (projective dielectric eigenpotentials) then works in the
eigenbasis of the static dielectric matrix in that RI space. Eigenpotentials
whose eigenvalue is within trunc_thresh of 1 (default 1e-4) carry almost no
screening and are dropped, so the frequency-dependent work runs in a smaller
basis. That compression, not the removal of the empty-state sum, is what PDEP
contributes here. How much it saves depends on the threshold; runs that need
the full-rank answer set trunc_thresh = 0.0, as the GW, BSE and \( C_6 \)
examples do.
RPA correlation energy
What it is. Direct RPA (dRPA) correlation from the dielectric eigenvalues on an imaginary-frequency quadrature.
- PDEP-RPA, closed shell:
method.kind = "pdep-rpa"(examples/water-pdep-rpa.toml); Pythonferric.run_pdep_rpa. Proven. - U-PDEP-RPA, open shell over a spin-summed dielectric. From the CLI, set
method.kind = "pdep-rpa"withmultiplicity > 1andtask = "energy": the CLI solves UHF (UKS with[rpa] xc) with MOM after 5 iterations and runs U-PDEP-RPA on it. It is CLI-only: Pythonrun_pdep_rpais closed shell only. The library (ferric_rpa::run_u_pdep_rpa) also accepts a ROHF (or ROKS) reference, which it semi-canonicalizes first: each spin uses the orbitals and energies of its own Fock matrix, diagonalized in its occupied and virtual blocks (see the anchors). It runs the Lanczos (default) and Davidson eigensolvers, PDEP truncation (trunc_thresh), the Dense and Laplace χ₀ backends andrun_diagnostics(full-basis U-RI-dRPA check). It refuseschi0_sparsity = "boys", and"auto"when that resolves to Boys for the molecule: the Boys-screened χ₀ is closed shell only. - Attenuated RPA: short-range correlation with an erfc operator.
- RS-MP2 + LR-RPA: short-range MP2 plus long-range dRPA, on the MP2 page.
The static eigensolve defaults to Lanczos, with a dense path for small
problems. Geometry optimization with pdep-rpa is supported
(task = "optimize") on a closed-shell RHF reference.
Exact and local dRPA
What it is. method.kind = "drpa" (Python ferric.run_drpa) is dRPA@HF
computed by the drCCD Riccati equations in the Boys-localized basis,
closed shell, energy only. It is exact by default: no amplitude is
truncated (examples/water-drpa.toml), and the energy equals the canonical
plasmon formula to ≤ 1e-12 Ha. Riccati, plasmon and full-rank PDEP-RPA are
algorithms for the same exact dRPA; measured agreement is ≤ 2.6e-14 Ha
(H2/STO-3G, water/6-31G with and without frozen core, PDEP with
trunc_thresh = 0 at 64 Gauss–Legendre points). Proven (narrow).
Which exact algorithm. The Riccati solve holds a ring-product plan of
no³·nv² numbers, no times the size of the amplitudes, so it is the
small-system path: C12 thrashed and was then killed for memory. A run that
cannot fit the memory budget is refused before the SCF and pointed at
pdep-rpa with [rpa] trunc_thresh = 0, which gives the same energy (to its
frequency-quadrature error) at far lower memory and is faster at every size
measured (n-alkanes C4–C16).
The local approximation ([local] scheme = "amplitude-threshold" with
eps, examples/water-drpa-local.toml; Python
run_drpa(..., local="amplitude-threshold", eps=1e-4)) drops pair amplitudes
whose localized |2(ia|jb)| is at or below eps. eps has no default and
is printed and logged with the kept fraction; eps = 0 is the exact method.
dRPA is not variational, so the error is first order in what is dropped. It
is positive (less correlation) at every point measured, which is a
measurement, not a guarantee, and grows faster than linearly in eps: on
n-octane / 6-31G it is 6.7e-7, 3.9e-5, 5.5e-4 and 1.0e-2 Ha at eps = 1e-6,
1e-5, 1e-4 and 1e-3, keeping 82%, 50%, 18% and 3% of the amplitudes. [local] reference = true
(Python compute_reference=True) also computes the canonical plasmon dRPA and
prints the error against it. [local] eps_sweep (Python run_drpa_scan)
evaluates several eps on one SCF and one localized assembly. Proven
(narrow) through its exact limit.
What is measured (n-alkanes, 6-31G / cc-pVDZ-RI, frozen carbon cores, Coulomb, single thread; local error against the plasmon formula, PDEP error against full-rank PDEP at 32 quadrature points). The fraction of the truncated object retained at 1 kcal/mol error:
| Size | Local dRPA (amplitudes kept) | PDEP (modes kept) |
|---|---|---|
| C4 | 23.3% | 17.9% |
| C8 | 10.0% | 21.5% |
| C12 | 4.9% | 22.6% |
| C16 | 2.8% | 23.3% |
The amplitude threshold compresses more as the molecule grows, and PDEP's
fraction stays flat. The two fractions are of different objects (no²nv²
amplitudes against naux modes), so this is not a cost comparison. In wall
time at a matched error of about 1 kcal/mol (reference off), truncated PDEP
stays about 8× faster from C4 to C16 (C16: 165.7 s local at
eps = 1e-4 against about 21 s PDEP), and both grow at the same rate in that
range. No speedup over PDEP is claimed for local dRPA.
GW
The equations and conventions of this section, the BSE-TDA and the RPAx
polarizability, with the code paths and tests behind each, are in
crates/ferric-gw/theory.md;
the identities it relies on are checked by
scripts/gw_self_energy_sympy.py.
What it is. Quasiparticle energies from the GW self-energy: G0W0,
COHSEX, evGW0 and evGW, closed shell, plus unrestricted U-GW.
The starting point is HF by default or a KS functional ([rpa] xc). From a
KS starting point the static term Σx − v_xc enters the G0W0, evGW₀ and evGW
quasiparticle equation (for U-GW, each spin's own equation with that spin's
v_xc), so Σc is evaluated at the shifted root; COHSEX and U-COHSEX, which are
static, add it to the quasiparticle energy.
Run it. method.kind = "gw" with [gw] method = "g0w0"
(examples/water-g0w0-pbe.toml, open shell examples/oh-ugw.toml); Python
ferric.run_gw, run_u_gw. The open-shell reference is UHF by default;
[gw] reference = "rohf" (Python run_u_gw(reference="rohf")) uses ROHF
instead, or ROKS with [rpa] xc (examples/oh-ugw-rohf.toml), semi-canonicalized
per spin as for U-PDEP-RPA.
Accuracy. Smoke; treat results as about ±0.3 eV.
| Quantity | System / basis | Reference | Pinned by |
|---|---|---|---|
| G0W0@HF, G0W0@PBE, U-G0W0@UHF, ECP, COHSEX, evGW₀, evGW quasiparticle energies (HOMO−2 to LUMO+2) | H2O, NH3, N2, OH, CH3, NH2, O2, CH2, I2, Xe, Ag2 / cc-pVDZ, aug-cc-pVDZ(-PP) | PySCF gw_ac/ugw_ac at matched settings ([rpa] n_quad = 100, trunc_thresh = 0); see What is validated | ferric-gw/tests/validation_gw.rs |
| G0W0@PBE HOMO IP | H2O / cc-pVDZ | PySCF gw_ac, 11.1714 eV; asserted to <0.1 eV | ferric-gw/tests/g0w0_pbe_h2o.rs |
| U-G0W0@UKS/PBE quasiparticle energies, Σx − v_xc inside each spin's equation | OH, CH3, NH2 / cc-pVDZ | PySCF ugw_ac Σc(ef + iω), quasiparticle equation solved in numpy: ≤2.6e-8 Ha on orbitals whose quasiparticle equation has a single root | ferric-gw/tests/validation_gw.rs |
| Σx − v_xc placement in U-GW (inside the quasiparticle equation; none for a UHF reference) | OH / STO-3G | internal: shifted residual, bit-identity of the no-shift path | ferric-gw/tests/u_gw_ks_shift.rs |
| U-G0W0@UHF α-HOMO IP | OH / cc-pVDZ | ~13–14 eV window, brackets experiment 13.02 eV | ferric-gw/tests/oh_u_g0w0.rs |
Limits. The quasiparticle equation is solved by a Newton root search on the self-energy, which is fragile near \( \Sigma_c \) poles. Runs report whether each root and each eigenvalue-self-consistency loop converged, and warn when one did not; check those flags.
BSE-TDA
What it is. Bethe–Salpeter excitation energies in the Tamm–Dancoff approximation on top of G0W0@HF quasiparticle energies, closed shell.
Run it. method.kind = "bse-tda" (examples/water-bse-tda.toml, and a set
of *-bse-tda-augdz.toml examples for small organics); Python
ferric.run_bse_tda.
Accuracy. Smoke. Given the same quasiparticle energies, the lowest five
singlet excitation energies match an independent numpy BSE-TDA (PySCF
density-fitted integrals, static RPA W) to 1.8e-10 Ha and their oscillator
strengths to 2.6e-9, for H2O at cc-pVDZ and aug-cc-pVDZ and NH3 and CH2O at
cc-pVDZ. The quasiparticle energies come from the internal G0W0@HF,
which matches PySCF only at [rpa] n_quad = 100 and trunc_thresh = 0; the
defaults are coarser. The core and high-virtual quasiparticle energies are
ill-conditioned, which moves the lowest five excitations by at most
2.2e-7 Ha.
TDDFT and TDA
What it is. Linear-response excitations in the Tamm–Dancoff approximation (TDA, which is CIS for an HF reference) and the full Casida equations, closed shell.
Run it. method.kind = "tda" or "tddft" with [tddft] n_roots and
optionally xc (examples/water-tda.toml, examples/water-tddft-pbe.toml); Python
ferric.run_tddft(mol, bs, aux, functional=..., method="tda") or
method="casida".
Scope. Closed-shell references, singlet excitations. With a DFT
reference the \( (ia|f_{xc}|jb) \) XC-kernel term is included; with no
functional the result is CIS/TDHF. τ-only meta-GGAs (SCAN, r2SCAN, TPSS)
take the τ kernel; VV10 and range-separated functionals are refused. Grade:
Proven (narrow, closed shell): water, formaldehyde and NH3 at 6-31G and
aug-cc-pVDZ with HF, LDA, PBE and B3LYP match PySCF TDA/TDDFT to at most
8.1e-4 eV on the default pruned grid (2.8e-6 eV for B3LYP/aug-cc-pVDZ on the
reference's flat grid), test bar 1e-3 eV, and water/6-31G r2SCAN matches on the flat
75 × 110 grid (ferric-tddft/tests/validation_tddft.rs).
Meta-GGA response needs a finer grid. A TDA/TDDFT run on a meta-GGA
(SCAN, r2SCAN, TPSS) with no [dft] grid key (Python: no grid_* kwarg)
runs its reference SCF and its f_xc kernel on 150 × 434 with the default
nwchem-radial pruning and 1e-14 weight cut. Every other functional keeps
the production 75 × 110 grid. Any grid key replaces the meta-GGA default.
The run prints the grid it used (CLI: the TDDFT grid line; Python:
TddftResult.grid).
On the same grid, ferric and PySCF agree: fed ferric's own coordinates and weights, PySCF reproduces ferric's r2SCAN excitation energies to 6e-8 eV and its SCF energy to 1e-12 Ha. The grid itself is the error. Largest deviation of the five lowest TDA roots from a flat 150 × 590 grid (Casida roots follow the same pattern):
Grid (nwchem-radial unless noted) | Points (water) | r2SCAN water/6-31G | r2SCAN water/aug-cc-pVDZ | r2SCAN H2CO/6-31G | r2SCAN H2S/6-31G | SCAN water/6-31G |
|---|---|---|---|---|---|---|
| 75 × 110 (production) | 13 914 | 2.1e-3 | 1.0e-3 | 3.9e-3 | 5.5e-3 | 4.2e-2 |
| 99 × 302 | 43 044 | 3.9e-4 | 2.0e-4 | 5.0e-4 | 3.9e-4 | 1.7e-2 |
99 × 302, nwchem (angular only) | 58 720 | 3.5e-4 | 1.5e-4 | 3.5e-4 | 3.8e-4 | 1.2e-2 |
| 99 × 434 | 60 102 | 8.4e-4 | 2.1e-4 | 3.2e-4 | 4.0e-4 | 3.5e-2 |
| 150 × 302 | 65 326 | 1.6e-4 | 2.2e-5 | 2.8e-4 | 9.8e-5 | 4.2e-3 |
| 150 × 434 (meta-GGA default) | 91 216 | 9.2e-5 | 5.5e-6 | 1.0e-4 | 4.2e-5 | 3.6e-3 |
| 150 × 590 | 105 186 | 9.9e-5 | 3.6e-6 | 6.7e-5 | 8.6e-6 | 3.5e-3 |
| flat 200 × 590 (reference check) | 354 000 | 2.4e-5 | 4.2e-6 | 2.7e-5 | 4.1e-5 | 1.2e-3 |
All values in eV. 150 × 434 is the smallest grid tested with every r2SCAN case at or below 1e-4 eV (H2CO: 9.96e-5 TDA, 1.02e-4 Casida); the margin is comparable to the reference's own distance from flat 200 × 590 (2-4e-5 eV).
PBE on the production grid is 4.4e-4 eV from flat 150 × 590, and radial pruning moves it by 1.6e-7 eV. For r2SCAN the radial shells the production grid removes from H (50) and O (66) shift the roots; the radial count matters more than the angular order (99 × 434 is worse than 99 × 302). SCAN is not grid-converged even at flat 150 × 590 (1.2e-3 eV from 200 × 590), so no grid here gets its roots to 1e-4 eV; the 150 × 434 default is 3.6e-3 eV from flat 150 × 590. Compare codes only on the same grid.
A separate, library-only TDA-DFT in ferric-gw/src/tddft.rs uses the same
kernel (ferric_dft::lr_kernel) and is pinned against PySCF
(ferric-gw/tests/tda_dft_vs_pyscf.rs). The user-facing TDA reproduces it to
1e-8 Ha for HF, PBE and B3LYP.
Polarizabilities and dispersion coefficients
What exists. Static molecular and atom-partitioned polarizabilities,
Casimir–Polder \( C_6 \) coefficients from per-atom dynamic
polarizabilities \( \alpha^A(i\omega) \), and many-body dispersion (MBD).
Three sources feed the \( C_6 \) contraction ([rpa] c6_source):
ts: the Tkatchenko–Scheffler single-pole model (default).mbd: self-consistent dipole screening of the TS polarizabilities. This is the full-range screening of Tkatchenko et al. 2012 (Gaussian-damped dipole tensor, no Fermi range separation), not the range-separated screening of MBD@rsSCS.pdep: dynamic PDEP-RPA polarizabilities (examples/water-c6-pdep.toml,examples/argon-c6-rpa-pbe.toml).
MBD@rsSCS dispersion energy. ferric.mbd_rsscs_energy(mol, volume_ratios, functional=... | beta=...) (Rust: ferric_rpa::dispersion::mbd_rsscs_energy)
computes the MBD@rsSCS energy of Ambrosetti et al. 2014 for a finite molecule,
standalone (no SCF). Inputs are per-atom Hirshfeld volume ratios, which scale
the free-atom α, C6 and R_vdW (Z = 1–54). The polarizabilities are screened
with the short-range part (1 − f) of the Gaussian-damped dipole tensor on
libMBD's 15-point imaginary-frequency grid; the energy couples the screened
oscillators through the long-range part f of the bare dipole tensor, with
the Fermi function f(R) = 1/(1 + exp(−6(R/(β(R_A + R_B)) − 1))). β is
functional dependent: PBE 0.83, PBE0 and HSE06 0.85; any other functional
needs an explicit beta. It returns the energy and the screened α₀, C6,
R_vdW and ω per atom, and raises on a polarization catastrophe (a
non-positive coupled-oscillator eigenvalue). No periodic systems. As a
dispersion correction on a Kohn–Sham SCF it is [dft] dispersion = "mbd" /
run_dft(dispersion="mbd"), which take the volume ratios from the converged
density (reported as DftResult.volume_ratios) and add the energy and its
analytic gradient (see SCF and DFT).
The argon-c6-rpa-pbe.toml header records C6(Ar–Ar) = 56.4 a.u. at
RPA@PBE/aug-cc-pVTZ against the DOSD value 64.3 (−12%). Which of TS and
PDEP-RPA gives better molecular \( C_6 \) is not established.
Use an augmented basis for any polarizability or \( C_6 \): without diffuse functions the dipole response is badly underestimated.
TDHF/RPAx \( C_6 \) is a measured negative. \( C_6 \) built on the
RPAx@PBE kernel stays about 63% low regardless of the gap. Its static
polarizability is not established either: at a physical scissor (0.36 Ha)
water/cc-pVDZ gives an isotropic α of 5.20 a.u. against the DOSD 9.64 a.u.
(−46%). The 9.24 a.u. quoted in the example's header comes from
scissor = 0, where the tensor has a negative diagonal component, and that
setting is refused. method.kind = "tdhf-static-polarizability" computes
static α only. It needs a KS reference ([rpa] xc), and at the default
[gw] scissor = 0 it can hit an excitonic instability, which is reported as
an error rather than a negative α; examples/water-tdhf-static-alpha.toml
hits it as shipped, so set scissor to about 0.3–0.4 Ha.
Cite
PDEP: Wilson, Gygi & Galli 2008. RI-RPA quadrature: Eshuis, Yarkony & Furche 2010; minimax grids: Kaltak, Klimeš & Kresse 2014. GW: Hedin 1965; GW100: van Setten et al. 2015. TDDFT review: Dreuw & Head-Gordon 2005. TS: Tkatchenko & Scheffler 2009; MBD: Tkatchenko et al. 2012; MBD@rsSCS: Ambrosetti et al. 2014. Full entries in References.
Constrained DFT
Charge- and spin-constrained DFT, and the electron-transfer couplings that follow from it.
Run it
Python and the Rust library; there is no CLI section.
run_cdft(mol, basis_set, constraints, functional=None, ...)returns aCdftResult: a constrained UHF solve, or UKS whenfunctionalnames a libxc functional other than"HF"(Noneand"HF", any case, give UHF).CdftConstraint(atoms, target, kind="charge")defines one fragment constraint.atomsare 0-based atom indices.targetis the electron population on the fragment, the Becke-weighted trace \( \mathrm{Tr}[W D] \), not a net charge: \( N_\alpha + N_\beta \) forkind="charge"and \( N_\alpha - N_\beta \) forkind="spin". A neutral He atom has a charge population of 2.0; He⁺ has 1.0.cdft_coupling(state_a, state_b)returns aCdftCouplingResultwith the Wu–Van Voorhis couplingh_ab, the determinant overlaps_aband the two diabat energiese_a,e_b. The raw element uses each state's free energy F = E + λN, which makes the coupling independent of a constant shift of the constraint operator.
In Rust the entry points are ferric_scf::cdft_driver::solve_cdft_uhf and
ferric_scf::cdft_coupling::coupling_hab; see the
Rust API.
This example reproduces the HeNe⁺ constrained solution that
crates/ferric-scf/tests/cdft_outer_loop.rs pins (E = −130.4021906 Ha,
λ = −2.754 Ha per electron), with the He fragment held at 2.0 electrons:
import ferric
# HeNe+ doublet at 2.0 Å; hold the He atom (index 0) at 2.0 electrons.
mol = ferric.Molecule.from_xyz_string(
"2\nHeNe+\nHe 0.0 0.0 0.0\nNe 0.0 0.0 2.0\n", 1, 2
)
r = ferric.run_cdft(
mol,
ferric.BasisSet.bundled("def2-svp"),
[ferric.CdftConstraint([0], 2.0, kind="charge")],
guess="hcore",
level_shift=0.5,
scf_max_iter=400,
lambda_tol=1e-5,
max_outer=40,
stability_descent=False,
grid_radial=99,
grid_angular=302,
)
print(r.converged)
print(f"{r.energy:.6f}")
print(f"{r.populations[0]:.5f}")
True
-130.402190
2.00000
r.energy is the ordinary UHF/UKS energy at the constrained density, without
the constraint term. r.lambdas holds the multipliers (Ha per electron) and
r.weight_matrix(i) the AO weight operator of constraint i.
What to know before using it:
- A returned result has a converged λ loop. When the outer loop exceeds
max_outer,run_cdftraisesRuntimeError. The inner SCF at the final λ can still be unconverged, so checkr.converged: it is true only when the inner SCF converged and every constraint is met tolambda_tol. - A constraint is applied at least once before it counts as met. The
loop starts at λ = 0, where the state is the unconstrained one. If its
population is within
lambda_tolof the target but more than 1e-3 electrons away, the loop still takes a Newton step rather than returning the unconstrained state as if it were constrained. Within 1e-3 the unconstrained state is returned with λ = 0: that is the measured spread of one state's population across grids, level shifts and guesses (up to 8.9e-5), with a margin. A loose tolerance can therefore return a state that is only partly constrained, so checkr.max_constraint_error(or the per-constraintr.residuals) to see how far from the target it is. The stability-descent restarts follow the same rule. stability_descentdefaults to True here (inrun_uhfit defaults to False). After the λ loop converges, a constrained saddle point is followed downhill to a lower state that still meets the constraint. The example turns it off to reproduce the pinned solution; with it on, this HeNe⁺ case ends about 0.0245 Ha lower. The descent is skipped for a KS reference.- The weight grid defaults to 99 × 302. It must resolve populations below
lambda_tol; 75 × 110 resolves them only to about 1e-4. Settinggrid_radialorgrid_angularuses that grid for the XC quadrature too. - The UKS path is the one compared against another code. Constrained
UKS/PBE matches NWChem 7.2.2 (see Accuracy). The UHF path
(
functional=Noneor"HF") has no comparison for energies along a target scan or for couplings: NWChem's standalone SCF (Hartree–Fock) module has no cDFT, because the Becke weight operator is built on the XC grid of its DFT module. Constrained UHF state selection on HeNe⁺ and LiH⁺ is compared through that DFT module, with Hartree–Fock exchange and a vanishing Slater term extrapolated to zero (ferric-scf/tests/validation_cdft_state.rs). - One constraint is the tested case. For one constraint the outer loop is a Newton step kept inside a sign-change bracket. It backs off from a λ whose inner SCF does not converge, and it discards a finite-difference derivative whose two inner solves landed in different SCF states. With several constraints the outer loop is a plain k × k Newton step without these safeguards.
cdft_couplinghas strict preconditions and raisesValueErrorwhen one fails. Each state carries exactly onekind="charge"constraint, both are converged, and both come from the same molecule, geometry, basis, charge and multiplicity, and the same Hamiltonian: functional,df_j_aux/df_k_aux,k_builder, XC grid, point charges and external field. Two states that are the same determinant (\( |S_{ab}| \to 1 \)) also raise. The sign ofh_abis a determinant-phase convention; compare \( |H_{ab}| \).
Accuracy
Constrained UKS/PBE energies and multipliers are compared against NWChem 7.2.2
cdft ... pop becke on LiH, HF and H2O⁺ (charge and spin constraints) at
6-31G and def2-SVP (ferric-scf/tests/validation_cdft.rs): E(N) −
E_unconstrained to 2.0e-7 Ha and λ to 1.1e-6 against NWChem's grid limit, and
dE/dN = −λ to 2.3e-12 Ha (this identity is checked at def2-SVP, on the first
target of each constraint kind); numbers are on Capabilities and
validation. No external reference value
is stated for UHF-cDFT energies apart from the HeNe⁺ and LiH⁺ states in
validation_cdft_state.rs. On HeNe⁺/def2-SVP at R = 2.0 Å the hole-on-Ne
state (N_He = 2) is NWChem's lower state once NWChem's Becke partition is
given ferric's He/Ne radius ratio (0.667; NWChem's own is 0.700). Against
NWChem's native partition it sits 0.563 eV higher. The offset comes from the
partition: with NWChem's radii patched into
ferric's radius table (a one-off measurement, not a test), ferric reaches
−130.447404 Ha against NWChem's native −130.447401 Ha. The He₂⁺ coupling
ingredients (determinant overlap, one- and two-electron transition elements,
|V|) match NWChem's et module to ≤ 6e-11 Ha on NWChem's own determinants,
and the KS diabats and couplings match end to end when started from NWChem's
λ, except def2-SVP at 3.50 Å, where inner SCF solves within 1e-9 in λ of the
root do not converge in 100 iterations (see Capabilities and
validation). These diabats are validated only
from NWChem's λ, set through the Rust RhfConfig::cdft_lambda_init;
run_cdft starts from λ = 0, where the symmetric pair is delocalized and the
inner SCF is bistable. The tests also check the coupling kernel on synthetic
matrices and He₂⁺ identities (ferric-scf/tests/cdft_coupling.rs). On HeNe⁺,
an asymmetric pair, the same file checks that both diabats carry the requested
charge, that the hole-on-He state is reached through either fragment (3.9e-6
Ha apart), that the hole prefers Ne, the lower-IP atom (gap 1.7646 eV with the
default stability descent; 1.0975 eV is a saddle point),
and that H_ab is invariant under W → W − cS. These run at R = 2.0 Å only: from
the hcore start, at 2.5, 3.5 and 4.0 Å the outer loop does not converge or
returns the unconstrained state, and from MINAO at 2.5 Å the two routes to the
hole-on-Ne state disagree by 6e-4 Ha. |H_ab| is not asserted on HeNe⁺;
cdft_coupling_hene.rs probes it over a distance series. The tests also check
exact identities on LiH/def2-SVP (cdft_uhf.rs): the constraint is satisfied,
λ = 0 reproduces plain UHF, and the constraint composes with an external point
charge. The Python tests (crates/ferric-python/tests/test_cdft.py) rerun
those configurations through the bindings and check cdft_coupling against an
independent transition-density construction. Apart from the HeNe⁺ and LiH⁺
states above, treat UHF-cDFT energies as unvalidated against other codes; see
Capabilities and validation.
The response connection
A cDFT constraint couples a Lagrange multiplier \( \lambda \) to a fragment-weighted density operator. The derivative
\[ \frac{\partial N}{\partial \lambda} \]
is the charge moved per unit constraint potential, a susceptibility. cDFT therefore probes the same object as RPA and GW and attenuated MP2, through a different coupling.
Implementation
- Fragment charge and spin constraints via a grid-Becke weight operator
- A nested Lagrange-multiplier solve (Wu–Van Voorhis): an inner SCF at fixed \( \lambda \), an outer Newton iteration on \( \lambda \) itself
Each outer step is a full converged inner solve, so cDFT costs several plain SCFs.
Electron-transfer coupling
The coupling \( H_{ab} \) between two charge-localized diabatic states follows from a non-orthogonal determinant overlap, computed via Löwdin biorthogonalization. It is the matrix element governing electron-transfer rates in Marcus theory.
Tolerance and coupling error
The λ convergence tolerance (lambda_tol in Python, cdft_lambda_tol in
Rust, default 1e-5 electrons) affects the coupling: a loosely converged
\( \lambda \) produces diabatic states that miss their targets, and
\( H_{ab} \) inherits that error. Tighten it before trusting a coupling. The exception is a
constraint whose population barely responds to \( \lambda \), such as He₂⁺
at the localized-hole plateau with a UHF reference: on He₂⁺/def2-SVP UHF at
2.5–3.5 Å the outer loop does not converge in 40 iterations at 1e-5, while at
1e-2 (what the tests use) it stops after 3 with λ ≈ 0.91 and residuals of
1.6e-3 to 7.8e-3 electrons, so check r.max_constraint_error before trusting
the state. With a UKS/PBE reference the He₂⁺ diabats do converge from λ = 0 at
1e-10 (validation_cdft_et.rs).
Cite
Wu & Van Voorhis 2006 (electron-transfer coupling from cDFT); Becke 1988 (the fragment weight partition). Full entries in References.
Periodic systems
ferric runs 3-D periodic Gaussian-basis calculations through the
ferric-pbc crate. Conventions follow PySCF pyscf.pbc: Ewald nuclear
repulsion with a neutralising background, the dropped-G = 0 Coulomb
convention, and the Madelung exchange correction (exxdiv = "ewald").
Energies are Hartree per cell. The equations, with code and test
cross-references, are in crates/ferric-pbc/theory.md; the identities are
checked by scripts/pbc_ewald_pairft_stress_sympy.py.
What runs
| Gamma point | k-point mesh (kmesh) | |
|---|---|---|
| SCF | RHF, UHF, ROHF | RHF, UHF, ROHF |
| Kohn-Sham | RKS, UKS, ROKS: LDA, GGA, global hybrids; range-separated (CAM) GGA hybrids; SCAN / r2SCAN / TPSS on the routes listed in the input reference | RKS, UKS, ROKS with the same families (per-route functional limits in the input reference) |
| Correlation | MP2, dRPA on the RHF reference (open-shell UMP2 / URPA library-only) | MP2, dRPA on the RHF reference |
| Nuclear forces | analytic, every SCF route; CLI task = "optimize" (atoms move, lattice fixed) | analytic; CLI task = "optimize" |
| Stress | analytic, Python with_stress=True | analytic, Python with_stress=True and CLI [cell] stress = true |
| ECPs | yes | yes |
Meshes are Gamma-centred (centring = "gamma") or shifted Monkhorst-Pack
("mp"). The exchange divergence uses the Gamma Madelung constant, or for a
mesh that of the diag(N) supercell.
Two-electron builders
jk = "dense"(default): a dense pure-AFTnao⁴ERI tensor. It is a correctness oracle for toy cells, hard-capped bymax_eri_gb(default 0.5 GiB; range-separated hybrids need 40nao⁴bytes).jk = "rsgdf": range-separated Gaussian density fitting (PySCF RS-GDF conventions). Requires an auxiliary basis (auxbasis, e.g."cc-pvdz-ri") and is bounded by the unified memory budget ([memory] budget_gb/memory_budget_gb). Options:gdf_omega(the fit's Ewald split),range_split(energy only),sr_column_rotation.
The nuclear attraction is split at its own omega (default
min(2.5 √π/Ω^{1/3}, ω_cap)); any value gives the same energy up to
truncation.
Entry points
CLI. Add a [cell] section (lattice, unit, optional kmesh,
exxdiv, jk, …) to an input file; method.kind is rhf, uhf, rohf,
ksdft, rimp2 (periodic MP2) or pdep-rpa (periodic dRPA). Full key list:
Input file, [cell]. Examples:
examples/h2-cell-rhf.toml, examples/h2-cell-kpts-rhf.toml,
examples/h2-cell-kpts-rhf-stress.toml, examples/h2-cell-rks-opt.toml.
Python. Coordinates and lattice rows in Å, omega/gdf_omega in Å⁻¹:
- Gamma point:
run_rhf_gamma,run_uhf_gamma,run_rohf_gamma,run_rks_gamma,run_uks_gamma,run_roks_gamma,run_mp2_gamma,run_drpa_gamma. - k-point mesh:
run_rhf_kpts,run_uhf_kpts,run_rohf_kpts,run_rks_kpts,run_uks_kpts,run_roks_kpts,run_mp2_kpts,run_drpa_kpts. auto_gdf_omegachooses an RS-GDF split.
SCF drivers take with_gradient / with_stress; results expose
energy, e_nuc (Ewald), madelung, gradient() (Hartree/Bohr per cell)
and stress() (σ = (1/Ω) dE/dε, Hartree/Bohr³, not symmetrised; pressure
−tr σ/3). Signatures and refusals are in ferric.pyi.
import ferric
mol = ferric.Molecule.from_xyz("testdata/molecules/h2.xyz")
a = 4.0 * 0.529177210903 # 4 Bohr cube, in Angstrom
res = ferric.run_rhf_gamma(
mol, [[a, 0, 0], [0, a, 0], [0, 0, a]],
ferric.BasisSet.bundled("sto-3g"), exxdiv="ewald", with_stress=True,
)
print(res.energy, res.stress())
Limits
- 3-D periodicity only; no slabs, wires or 2-D Coulomb truncation.
- Charged cells are an error.
- No lattice-vector relaxation:
task = "optimize"moves atoms at a fixed lattice.task = "frequencies"is an error. - Gamma-point stress is Python-only; the CLI
stress = truerequireskmesh. - The dense AFT builder is toy-scale by construction (memory
8 nao⁴bytes, G count growing asΩ p_max^{3/2}); production-size cells needjk = "rsgdf". - VV10 is refused on every periodic derivative path; per-functional and per-route support for forces and stress is listed in the input reference and Capabilities and validation.
- Open-shell forces and stress refuse a non-stationary SCF state rather than
return a number: ROHF/ROKS orbital gradient above 1e-5 Ha, UKS spin
commutator above 1e-5 Ha (k-point), a stored Fock that differs from the
rebuilt one by more than 1e-8 Ha, or SCF k-points that are not the mesh's.
Converge the SCF tighter (
density_conv1e-10 is FD-grade). Derivation and the full list of tolerances:crates/ferric-pbc/theory.mdsection 6. - Gamma-point local MP2 is library-only and its default pair gate keeps every pair below a volume onset.
Electronic response
ferric is organized around electronic response: how the electron density
reacts to a perturbation. Standard quantum-chemistry codes are usually organized
around a hierarchy of wavefunction ansätze (HF → MP2 → CCSD → CCSD(T)).
ferric uses response as the organizing principle instead.
The central object appears under several names depending on which coupling you look at:
| Object | Definition | Where it appears in ferric |
|---|---|---|
| Density response \( \chi \) | \( \chi = \delta\rho / \delta v_{\text{ext}} \); \( \chi_0 \) is its independent-particle form | RPA, GW, MP2's dispersion |
| Dielectric matrix \( \varepsilon \) | \( \varepsilon = 1 - v\chi_0 \) (RPA), with \( v \) the Coulomb kernel | PDEP-RPA and GW screening |
| Polarizability \( \alpha \) | response of the dipole to a uniform field; \( \alpha(i\omega) \) gives \( C_6 \) | polarizabilities, dispersion coefficients |
| \( \partial N / \partial \lambda \) | charge moved per unit constraint potential | constrained DFT |
These are the same physics viewed through different couplings. A code that computes one well should be able to compute the others, and errors in one should be diagnosable as errors in the others.
The claim
The premise behind the architecture is that response is:
- Local in real space: a density fluctuation here does not much affect the density far away, so the response should be sparse in a localized basis.
- Low-rank in its eigenspectrum: the dielectric matrix has a small number of dominant eigenmodes, so it can be compressed without losing the physics.
If both hold, organizing the computation around response should make it cheaper: attenuate the operator, keep the dominant dielectric modes.
What is demonstrated
Low rank: demonstrated, with a narrower scope. PDEP compresses the dielectric matrix to its dominant eigenpotentials in the RI auxiliary space, and that works in production paths; see RPA and GW. It does not remove the sum over empty states: ferric builds \( \chi_0 \) by summing over every occupied–virtual pair, and PDEP compresses what comes out of that sum.
Locality: one positive result, still without a speedup.
- AO-sparse Laplace SOS-MP2. Restricting each localized orbital's
pseudo-density to an AO domain works: the radius needed grows far more
slowly than the molecule (chemical accuracy at 3 to 5 Bohr from ethane to
dodecane, while radius/diameter falls from 0.52 to 0.17; within 0.05% at
4 Bohr on a 71-atom drug molecule). The tensor algebra is
still dense, so no timing gain is claimed. This result is specific to
that formulation and does not carry over to the other locality approaches below.
The test
sos_ao_sparse_truncation_radius_is_transferable_across_sizespins the STO-3G butane/octane comparison (12 Bohr exact on both; octane worse at 3 Bohr). The C2-C12 sweep and the drug-molecule figure are measurements, not regression tests. - Local MP2 (amplitude threshold) has localized virtuals and per-pair
domain-local RI fits. The integral-direct variant (
rimp2with[local] integral_direct = true) is measured at about N1.24 (erfc) to N1.4 (Coulomb) on alkanes C20–C48, three points in one basis, so the reading is provisional. Local MP2 withoutintegral_directstill builds the global 3-index tensor and makes no scaling claim. - RI-Laplace MP2 is dense; it is the correctness reference for the AO formulation, not a reduced-scaling path.
The locality premise is therefore partly supported and not yet cashed in as cost. Measured limits are kept on Capabilities and validation.
Why this framing is useful anyway
Even where the scaling payoff has not arrived, the response framing makes the error in one method diagnosable through another.
MP2's dispersion is the clearest case. Its dispersion energy is built from an uncoupled Hartree–Fock response, in which the density fluctuation does not feel the field it creates. For systems with low-lying, highly polarizable excitations, such as π-stacked and other π systems, that uncoupled response over-polarizes, and MP2 overbinds. This is documented in the literature that replaces MP2's uncoupled dispersion with a coupled one (Cybulski & Lytle 2007; Heßelmann 2008; Pitoňák & Heßelmann 2010). The size of that coupling correction varies from system to system, and ferric has no coupled-dispersion (MP2C-style) implementation of its own, so read this as the literature's diagnosis, not a ferric measurement.
Attenuated MP2 takes a blunter route: it removes the long-range correlation altogether and relies on a fitted short-range operator plus the basis-set error it is fitted in. That is why it has zero asymptotic \( C_6 \), why its parameters belong to one basis, and why MP2-V adds long-range dispersion back through VV10. RS-MP2 + LR-RPA puts the long-range part back through response instead. See The MP2 family.
Where the methods come from
Each method family in ferric works on the response function from a
different direction. The definitions and
parameters live on the method pages; this page is the map.
Attenuated MP2: removing the long-range part
MP2's dispersion comes from an uncoupled response, which over-polarizes for systems with low-lying, highly polarizable excitations (π-stacked aromatics are the classic case); it is not a general property of polarizable molecules. In small basis sets the resulting overbinding is partly cancelled by basis-set superposition error, which disguises it.
Attenuated MP2 replaces \( 1/r \) in the correlation energy with a short-range operator (erfc or terfc) and fits its range to interaction energies in a chosen basis. It removes the long-range correlation entirely rather than correcting it, so it has no asymptotic \( C_6 \), and its parameters are specific to the basis and protocol they were fitted in. MP2-V restores long-range dispersion with VV10; RS-MP2 + LR-RPA restores it with long-range RPA.
Goldey & Head-Gordon (JPCL 2012) introduced it in aug-cc-pVDZ; Goldey, Dutoi & Head-Gordon (PCCP 2013) introduced terfc in aug-cc-pVTZ; the dual-attenuated SCS variant is Goldey & Head-Gordon (JPCB 2014). Operators, parameters and fitting protocol: The MP2 family.
PDEP-RPA and GW: compressing the response
The dielectric matrix is built from the density–density response function. ferric forms the independent-particle response in the RI auxiliary basis by summing over occupied–virtual pairs, then works in the eigenbasis of the static dielectric matrix, dropping eigenpotentials that carry almost no screening. PDEP (projective dielectric eigenpotentials) is that compression. It is the demonstrated part of the low-rank premise; the empty-state sum that feeds it is still there. See RPA, GW and excited states.
Constrained DFT: reading the response
A cDFT constraint couples a Lagrange multiplier \( \lambda \) to a fragment-weighted density operator. The derivative \( \partial N / \partial \lambda \), how much charge moves per unit constraint potential, is a susceptibility.
That makes cDFT a direct probe of the same object. It also yields charge-localized diabatic states whose electron-transfer couplings \( H_{ab} \) follow from non-orthogonal determinant overlaps. See Constrained DFT.
What this buys
Three families, one object. An error in the polarizability shows up as an error in dispersion, in screening, and in charge-transfer coupling, so a fix validated in one place has predictable consequences in the others.
That is the design bet. Whether it pays off in cost is still open; see Electronic response for what has and has not been measured.
Architecture
A Cargo workspace of focused crates, layered so that method crates depend on integrals and core, not on each other.
+------------------+
| ferric-cli | TOML config -> most methods
+--------+---------+ (+ ferric-python: pyo3 bindings)
|
+-----------+-----------+-------+------+-----------+------------+
| | | | | |
+--v----+ +----v----+ +----v----+ +-----v----+ +----v-----+ +---v------+
|ferric | |ferric | |ferric | |ferric | |ferric | |ferric |
|-scf | |-mp2 | |-dft | |-rpa | |-gw | |-cc |
|RHF/UHF| |RI-MP2, | |RKS/UKS/ | |PDEP-RPA, | |G0W0, | |CCD/CCSD/ |
|/ROHF, | |OO,att, | |ROKS, | |U-PDEP, | |COHSEX, | |(T) |
|KS-DFT,| |SCS, | |libxc, | |response | |evGW, | +----------+
|DIIS, | |2terfc, | |Becke | |props, | |U-GW |
|MOM,AH,| |Laplace | |grids, | |ESP/Hirsh/| +-----+----+
|cDFT, | +----+----+ |VV10 | |NPZ export| |
|grads | | +----+----+ +-----+----+ |
+---+---+ | | | |
| +-----+-----+------+-------+------------+
| | | |
| +------------v--+ +v------v-----+ ferric-tensors (sparse),
| |ferric-export | |ferric- | ferric-quadrature (Laplace/grid roots)
| |cube,NPZ,GTO | |integrals | support crates
| +--------------+ |libint2 FFI |
| |shim/shim.cc | Coulomb/erf/erfc, 1e/2e/3c/2c, derivs
+--------+-----------+------+------+
| |
+----v------------------v----+
| ferric-core | Molecule, BasisSet, Shell, elements,
| | BSE-JSON / G94 parsers, bundled bases
+----------------------------+
Layers
ferric-core: molecular structure, basis sets, shells, elements, and the
BSE-JSON / Gaussian-94 parsers. Also the home of shared infrastructure:
configuration (ConfigVar), memory budgets (MemoryPlan), the BLAS-thread
hazard model, and MPI context.
ferric-integrals: the libint2 FFI and its C++ shim. Coulomb, erf and erfc
operators; 1-electron, 2-electron, 3-center and 2-center integrals; first
derivatives. The shim wraps every libint2 call in try/catch and returns a
sentinel, so a C++ exception never unwinds across the FFI boundary.
Method crates: ferric-scf, ferric-mp2, ferric-dft, ferric-rpa,
ferric-gw, ferric-cc, ferric-ci, ferric-tddft, ferric-pcm,
ferric-mm, ferric-xtb, ferric-d3 (Grimme D3(BJ) dispersion energy and
gradient, optional ATM three-body term).
Support: ferric-tensors (the einsum! contraction macro),
ferric-quadrature (Lebedev grids, minimax Laplace roots), ferric-export
(cube files, NPZ, GTO evaluation).
Interfaces: ferric-cli (TOML-driven) and ferric-python (pyo3).
Cross-cutting conventions
Threading. rayon owns outer parallelism; BLAS is pinned to one thread inside any rayon worker, enforced at runtime rather than by convention. Throughput across many jobs comes from many single-threaded processes, not one wide job.
Determinism. Reductions fold in a fixed ascending order independent of
thread count, so results are bit-identical across RAYON_NUM_THREADS. This is
pinned by tests. Another deterministic order, such as a tree-fold, would change
the bits, because floating-point addition is not associative.
Memory. MemoryPlan expresses what a path will allocate and when. An
allocation that exceeds the budget and has no spill-to-disk or recompute
fallback is refused before allocating, with a breakdown naming the dominant
term. The CLI installs one process-wide MemoryPool sized from the resolved
budget, and the large, size-dependent allocations reserve their bytes from it,
so two allocations alive at the same time cannot each claim the whole budget.
Library and Python callers install no pool; each check then compares its own
allocation with the whole budget. Allocations that have a fallback spill to
disk or are recomputed instead of being refused, and basis-sized matrices, engines and
scratch are not charged (see
Sharp bits).
Guards are tested in both directions: a starved budget must be refused (or
must fall back), and an ample budget must still run.
Errors. Methods return Result; iterative solvers additionally carry a
converged flag, since non-convergence is a result rather than an error.
Callers are expected to check it.
Capabilities and validation
Implemented ≠ validated. Working code is not a checked number.
Read this page before trusting any result. It lists every CLI method.kind
(which references it accepts, which tasks it supports, the matching Python
function, an example input) and the Python entry points. Each graded CLI
method.kind gets a grade (three kinds are ungraded, and Python entry points
are not graded individually), and the page shows how far each grade's numbers have been checked against an
independent reference and where they are known to fail.
How to read it: find the capability in the matrix
or the Python entry points. A matrix Grade cell links
to the Anchors table when a row there holds the evidence; the
Known limits section records the measured
negatives. Every matrix cell comes from the CLI dispatch in
crates/ferric-cli/src/lib.rs or the bindings in
crates/ferric-python/src/lib.rs. The page is maintained by hand, so check
those two files if a cell looks wrong. For keyword details see the
input reference; for the example files see the
examples index.
Grades
The grades are the ones ferric's CLI uses. Every CLI method.kind except
laplace-sos-mp2 has a grade. A kind's grade is for the exact method; the
local approximation of rimp2, drpa and linlccd (the [local] section)
is graded separately in that row's Caveat. The CLI prints the Smoke and Spike
grades as a [warning] line on stderr at run time; Proven kinds and the
ungraded kind print nothing. The warning text is the
EPISTEMIC_WARNINGS table in crates/ferric-cli/src/lib.rs, and the
"Caveat" column of the matrix condenses it. The grades themselves are
METHOD_KIND_GRADES in crates/ferric-core/src/certificate.rs, a copy of the
Grade column below that a test keeps equal to it.
rhf, uhf, rohf and ksdft energy runs print a certificate block after
the energy and write it to the JSON run log (run_end.extra.certificate; a
certificate record for the open-shell runs); Python RhfResult,
UhfResult and DftResult carry it as .certificate. It states the kind's
grade, the build, the approximations in effect (RI-J / RI-K, LinK / COSX, the
XC grid, a level shift, ECPs, embedding, solvation) and warnings: a Plateau
exit, a non-Stable stability verdict, and an unchecked UHF/UKS stability. An
unconverged SCF is capped to not graded.
Post-SCF kinds print a certificate block after their result: the MP2 family
(rimp2 exact, local and open-shell, mp3, oo-rimp2, att-rimp2, mp2-v,
scs-mp2, scs-mp2-2terfc, laplace-mp2, laplace-sos-mp2), rs-mp2-rpa,
drpa, pdep-rpa, ccsd, ccd, ccsd(t), linlccd, wb97x-l-v, b2plyp,
dsd-pbep86, tda, tddft, gw, bse-tda and
tdhf-static-polarizability. Kinds that write a result record carry it as
result.components.certificate; pdep-rpa, wb97x-l-v, b2plyp,
dsd-pbep86, tda, tddft, gw, bse-tda and
tdhf-static-polarizability write a certificate record. Python
RiMp2Result, Mp3Result, OoRiMp2Result, AttenuatedMp2Result,
ScsMp2Result, Mp2VResult, LaplaceMp2Result, SosMp2Result,
RsMp2RpaResult, CcResult, PdepRpaResult, GwResult, UGwResult,
BseResult, TdhfStaticPolarizabilityResult, DoubleHybridResult and
TddftResult carry it as .certificate;
the run_drpa and run_linlccd dicts carry a "certificate" key. The grade is the kind's row, capped at the grade of the
SCF reference's certificate, whose approximations and warnings it repeats as
scf <name> and scf reference: .... It records the auxiliary basis, frozen
core, a non-Coulomb correlation operator and each method's settings (SCS
scales, Laplace points, frequency quadrature, PDEP truncation, GW variant,
double-hybrid scaling or λ and ω, the TDDFT response grid).
Caps to not graded: an [local] amplitude threshold at finite eps, an
unconverged orbital optimization (oo-rimp2), drCCD fixed point (drpa) or
evGW eigenvalue loop. Warnings: GW and BSE-TDA runs at settings other than
n_quad = 100, trunc_thresh = 0, unconverged quasiparticle roots, suspect
quasiparticle energies, an unconverged PDEP eigensolver. Periodic runs (the
CLI [cell] section and the Python Gamma-point and k-point results) carry a
certificate with the grade not graded.
| Grade | Meaning |
|---|---|
| Proven | Total energies (or the stated property) agree at a stated tolerance with an independent reference, pinned by tests. An independent reference is another code (PySCF, MOLGW), a numpy reference, a published value, or an exact limit the method must reduce to. The [warning] line treats "Proven" and "Proven (narrow)" alike; the certificate names the grade. |
| Proven (narrow) | As Proven, but only on the stated class of systems, or only against the stated kind of reference (for example exact limits only) |
| Smoke | Runs end to end, and some pieces are checked, but there is no independent reference for the headline number, or only one loose one. The checks are range bands, internal consistency or limits rather than a tight external number. Do not quote a Smoke result as a reference value. |
| Spike | Built on new infrastructure and not yet compared against any reference code; for exploration only |
| not graded | Dispatched by the CLI with no recorded grade, so it prints no warning; treat it as unproven |
Symbols: ✓ = supported. — = not supported (the CLI refuses it with an error).
FD = finite differences of the analytic gradient (6N gradient
evaluations). The analytic Hessian covers closed-shell RHF and UHF of any multiplicity (both also
with RI-J and/or RI-K, without an external potential; all with ECPs), and ROHF with exact J/K or RI and
no external potential, with a basis up to f functions; rhf, uhf and
rohf frequencies use it there by default.
CLI method.kind matrix
"Reference" is the SCF reference the CLI builds for that kind. Open-shell support is listed only where the dispatch code handles it (see Open shells below).
method.kind | Family | Reference (CLI) | Energy | task = "optimize" | task = "frequencies" | Python | Example | Grade | Caveat |
|---|---|---|---|---|---|---|---|---|---|
rhf | SCF | RHF; RKS with [dft] functional (refuses multiplicity > 1) | ✓ | ✓ analytic | analytic (RHF, exact J/K, ECPs included, up to f; RKS through the moving-grid KS Hessian, as for ksdft); FD otherwise | run_rhf, run_optimize, run_frequencies | water-rhf.toml | Proven | — |
uhf | SCF | UHF; UKS with [dft] functional | ✓ | ✓ analytic (+ D3(BJ) or MBD@rsSCS gradient with [dft] dispersion on UKS) | analytic (UHF, exact J/K, ECPs included, up to f); FD otherwise; with [dft] dispersion on UKS, FD of the KS + dispersion gradient | run_uhf, run_frequencies(reference="uhf", xc=..., dispersion=...) | h_uhf.toml | Proven | — |
rohf | SCF | ROHF; ROKS with [dft] functional | ✓ | ✓ analytic (+ D3(BJ) or MBD@rsSCS gradient with [dft] dispersion on ROKS) | analytic (ROHF, exact J/K or RI, ECP allowed, no external potential, up to f); FD otherwise, and for ROKS (+ the dispersion gradient with [dft] dispersion on ROKS) | run_rohf, run_frequencies(reference="rohf", xc=..., dispersion=...) | nh2-rohf-frequencies.toml | Proven | — |
ksdft | SCF/DFT | RKS; UKS when multiplicity > 1 | ✓ | ✓ analytic (+ D3(BJ) or MBD@rsSCS gradient with [dft] dispersion, RKS and UKS) | analytic moving-grid (RKS LDA/GGA, pure or global hybrid, exact or RI J/K, no ECP or external potential, unpruned grid, response AO table within its cap); FD otherwise (UKS, meta-GGA, VV10, RSH, …); with [dft] dispersion FD of the KS + dispersion gradient (RKS and UKS) | run_dft / run_ksdft, run_frequencies(xc=..., dispersion=...) | benzene-dfb3lyp.toml, h2-lda-opt.toml | Proven | — |
rimp2 | MP2 | RHF; UHF + unrestricted RI-MP2 when multiplicity > 1 (energy only) | ✓ | ✓ analytic (Z-vector; closed shell only) | — | run_rimp2 (UHF + UMP2 when multiplicity > 1) | water-rimp2.toml; local: water-rimp2-local.toml, alkane8-rimp2-local-direct.toml | Proven | Exact RI-MP2. With [local] scheme = "amplitude-threshold" (run_rimp2(local=..., eps=...)): Proven (narrow) by its exact limit — at ε = 0 the in-core and integral_direct paths match PySCF DF-MP2 to ≤1.38e-11 Ha (H2O and n-butane, 6-31G and cc-pVDZ), at the default PCG stop of relres 1e-6; finite ε is a measured error map, not validated; closed shell and task = "energy" only; ε = 0 reproduces exact rimp2; the exact reference and the error against it are opt-in ([local] reference = true, compute_reference=True). integral_direct = true never forms the global 3-index tensor; its measured scaling is under Known limits. On the device ([gpu]) the energy stage runs with B_ov resident: at precision = "f64" (the default) it agrees with the CPU path to within a bound on the difference of summation order (gpu_rimp2_resident.rs); with precision = "mixed" and rimp2-energy in mixed_kernels it is a measured error map, not Proven (f32-resident B_ov, f64 accumulation; closed-shell and unrestricted, Coulomb, erfc and terfc operators), graded under Mixed-precision GPU kernels. |
mp3 | MP2 | RHF | ✓ | — | — | run_mp3 | water-mp3.toml | Proven | — |
oo-rimp2 | MP2 | RHF; UHF + unrestricted OO-RI-MP2 when multiplicity > 1 (energy only) | ✓ | — | — | run_oo_rimp2 (closed shell only) | water-oo-rimp2.toml | Proven (narrow) | Energy matches an independent numpy OO-RI-MP2 to 7.5e-13 Ha (closed-shell H2O and NH3, UHF CH3 / cc-pVDZ) and the closed-shell analytic gradient matches a finite difference of its own energy to 8e-9 Ha/Bohr. ORCA 6.1.1 stops 3.7e-8 to 7.0e-8 Ha above the same minimum. |
att-rimp2 | MP2 | RHF | ✓ | — | — | run_attenuated_rimp2 | water-attmp2.toml | Proven | — |
mp2-v | MP2 | RHF; UHF when multiplicity > 1 (energy only) | ✓ | — | — | run_mp2_v (closed shell only) | water-mp2v.toml | Proven (narrow) | The implementation is Proven (narrow): the damped and undamped VV10 half, the terfc-attenuated MP2 half (the published operator) and the erfc control each match an independent PySCF/numpy reference on H2O (cc-pVDZ, aug-cc-pVDZ) and NH3 (cc-pVDZ); the VV10 half and the erfc control also on the S22 water dimer (aug-cc-pVDZ). The terfc E_c itself is not re-validated as an operator here (that is the SCS-MP2(2terfc) row); this row adds its wiring through AttVv10Config::mp2_v_terfc_atz (r₀ in Å → Bohr → ω, metric, frozen core) and the unrestricted path (see Anchors). The parameterization is not benchmarked: the defaults (r₀ = 1.00 Å, b = 11.0, C = 0.0089) are fitted for aug-cc-pVTZ with frozen core and no counterpoise, only an aug-cc-pVTZ subset of S66 has been run (n=5: H-bonded and mixed dimers, S66x8 geometry and references, no CP-anchored reference), and no published MP2-V total energy exists to compare against, so another basis, frozen_core = 0 (the CLI default) or a different system class is unparameterized extrapolation. Open shell is checked only as a UHF singlet collapsed onto RHF; no open-shell parameterization exists (S66 is closed-shell). |
scs-mp2 | MP2 | RHF | ✓ | — | — | run_scs_mp2 | water-scs-mp2.toml | Proven | — |
scs-mp2-2terfc | MP2 | RHF | ✓ | — | — | run_scs_mp2_2terfc | water-scs-mp2-2terfc.toml | Proven (narrow) | No other code implements terfc. The terf/terfc integrals and the E_OS/E_SS/Eq. 12 energies match an independent Fourier-space construction (H2O, NH3 / cc-pVDZ; H2O / aug-cc-pVDZ). The published parameters are fitted for aug-cc-pVTZ with frozen core and no counterpoise; Only an aug-cc-pVTZ S66 subset (n=5, H-bonded/mixed, S66x8 geometry) has been run; there is no full-set statistic. Needs the terfc tables (FERRIC_TERF_TABLE_DIR, or the repository's terf-tables/). |
laplace-mp2 | MP2 | RHF | ✓ | — | — | run_laplace_mp2 | water-laplace-rimp2.toml | Proven | Minimax-Laplace denominators on five systems (R = 19.0 to 45.7) against exact DF-MP2 and against a quadrature-isolated reference that separates quadrature error from implementation error. See the Anchors table. |
laplace-sos-mp2 | MP2 | RHF | ✓ | — | — | run_laplace_sos_mp2 | water-laplace-sos-mp2.toml | not graded | With c_os = 1.0 it reproduces the opposite-spin MP2 energy (internal reference). |
pdep-rpa | RPA/GW | RHF, or RKS via [rpa] xc; UHF/UKS when multiplicity > 1 (energy only) | ✓ | ✓ (closed-shell RHF reference only; [rpa] xc is refused; analytic SCF + FD correlation) | — | run_pdep_rpa (closed shell only; open-shell U-PDEP-RPA is CLI-only) | water-pdep-rpa.toml | Proven | Open shell: Lanczos/Davidson, Dense/Laplace χ₀ and run_diagnostics; full rank, Lanczos ≡ Davidson ≡ the full-basis U-RI-dRPA diagnostic and a UHF singlet ≡ the closed-shell path (u_pdep_rpa.rs); chi0_sparsity = "boys" (or "auto" resolving to it) is refused. |
rs-mp2-rpa | MP2 / RPA | RHF | ✓ | — | — | run_rs_mp2_rpa | water-rs-mp2-rpa.toml | Smoke | The ω→0 and ω→∞ limits reduce exactly to MP2 and MP2+dRPA and are Proven. At production ω it is only marginally benchmarked on one small subset and is unproven on new systems. |
gw | RPA/GW | RHF, or RKS via [rpa] xc; UHF/UKS, or ROHF/ROKS with [gw] reference = "rohf", when multiplicity > 1 (energy only) | ✓ (QP energies) | — | — | run_gw, run_u_gw | water-g0w0-pbe.toml, oh-ugw.toml | Smoke | Matches PySCF gw_ac/ugw_ac only at matched settings (G0W0@HF and @PBE ≤1.3e-7 Ha, evGW ≤5.7e-7, ECP ≤2.5e-6, U-G0W0@UHF ≤7e-6 Ha; U-G0W0@UKS/PBE, with Σx − v_xc inside each spin's quasiparticle equation, ≤2.6e-8 Ha; U-G0W0 on a semi-canonicalized ROHF reference in the Anchors; the ROKS reference is not compared) with [rpa] n_quad = 100 and trunc_thresh = 0; the CLI defaults are coarser (the 20-point grid moves the H2O G0W0@PBE HOMO by 13 meV) and truncation is not validated. See the G0W0 anchors. |
bse-tda | RPA/GW | RHF only (refuses multiplicity > 1) | ✓ (excitations) | — | — | run_bse_tda | water-bse-tda.toml | Smoke | The lowest five singlets match an independent numpy BSE-TDA to 1.8e-10 Ha given the same quasiparticle energies (H2O cc-pVDZ and aug-cc-pVDZ, NH3 and CH2O cc-pVDZ; see anchors). The quasiparticle energies come from the internal G0W0, which matches PySCF only at n_quad = 100, trunc_thresh = 0; the defaults are coarser. |
tdhf-static-polarizability | RPA/GW | RKS only ([rpa] xc required) | ✓ (static α) | — | — | run_tdhf_static_polarizability | water-tdhf-static-alpha.toml | Smoke | Static α only. The kernel is statically screened RPAx, not bare TDHF. Its implementation matches an independent numpy assembly of the same kernel to 5.3e-9 relative, and its bare-exchange limit matches PySCF CPHF (see anchors), at matched settings: exact-J SCF and full-rank PDEP (trunc_thresh = 0; the CLI default is 1e-4). The physics is not established: at a physical scissor (0.36 Ha) α is 5.20 a.u. for water/cc-pVDZ against DOSD 9.64 (−46%), and the independent kernel gives the same 5.20. The same kernel gives C6 about 63% low. At scissor = 0 it can hard-error on a negative α diagonal, which the independent kernel reproduces (water/cc-pVDZ α_xx = −2.68); set [gw] scissor to about 0.3–0.4 Ha. |
ccsd | CC | RHF (spin-adapted solver) | ✓ | — | — | run_ccsd | water-ccsd.toml | Proven | — |
ccd | CC | RHF only (refuses multiplicity > 1) | ✓ | — | — | run_ccd | water-ccd.toml | Proven (narrow) | RI-CCD, spin-orbital solver. |
ccsd(t) | CC | RHF only (refuses multiplicity > 1) | ✓ | — | — | run_ccsd_t | water-ccsd-t.toml | Proven (narrow) | Spin-adapted CCSD + spin-adapted (T). Prints E_CCSD, E_(T) and the total. |
linlccd | CC | RHF only (refuses multiplicity > 1; open-shell LinLCCD(hh) is library-only) | ✓ | — | — | run_linlccd | water-linlccd.toml; local: water-linlccd-local.toml | Proven (narrow) | No other code implements LinLCCD(hh); the energy matches an independent numpy solve on PySCF density-fitted integrals to ≤1.2e-12 Ha (H2O and NH3 through the CLI's closed-shell path; UHF OH through the library-only open-shell path). With the hole–hole ladder off it reduces exactly to RI-MP2, and with exact integrals its driver terms reproduce canonical MP2; size consistency is checked. [mp2] linlccd_variant (hh, drivers-only, full) applies exact and local. With [local] (amplitude threshold): Proven (narrow); at ε = 0 every variant, full included, matches the independent numpy solve to ≤ 4.4e-13 Ha, as does the exact path. |
drpa | RPA/GW | RHF only (refuses multiplicity > 1) | ✓ | — | — | run_drpa, run_drpa_scan (local ε scan) | water-drpa.toml; local: water-drpa-local.toml | Proven (narrow) | Exact dRPA@HF by the drCCD Riccati solve with nothing truncated; equals the canonical plasmon formula (≤1e-12 Ha), full-rank PDEP (pdep-rpa, trunc_thresh = 0), and an independent numpy plasmon dRPA on PySCF integrals (≤3.0e-11 Ha; water and n-butane at 6-31G and cc-pVDZ, n-octane at 6-31G; see Anchors). Its memory grows as no³·nv² and a run that cannot fit is refused before the SCF. With [local] (amplitude threshold, eps or eps_sweep): Proven (narrow) by its exact limit; the finite-ε error is not variational, and on n-octane it is positive and grows with ε at every point measured (Anchors). |
wb97x-l-v | CC § ωB97X-L-V | Its own RKS (wB97X-L-V) reference (refuses multiplicity > 1; open shell is library-only) | ✓ | — | — | none | water-wb97xlv.toml | Proven (narrow) | E_KS and λ·E_c against PySCF with the paper's parameters plus a numpy LinLCCD(hh) (water, OH / def2-SVP); Be₂ bond energy against the paper's Table 4. |
b2plyp | CC § double hybrids | Its own RKS reference | ✓ | — | — | run_double_hybrid(kind="b2plyp") | water-b2plyp.toml | Spike | Weighted B88+LYP reference. Not compared with any reference code. |
dsd-pbep86 | CC § double hybrids | Its own RKS reference | ✓ | — | — | run_double_hybrid(kind="dsd-pbep86") | — | Spike | Weighted PBE+P86 reference. Not compared with any reference code. |
tda | RPA/GW § TDDFT | RHF (CIS), or RKS via [tddft] xc (refuses multiplicity > 1) | ✓ (excitations) | — | — | run_tddft(method="tda") | water-tda.toml | Proven (narrow, closed shell) | The lowest five roots match PySCF TDA for water, formaldehyde and NH3 at 6-31G and aug-cc-pVDZ with HF, LDA, PBE and B3LYP: at most 8.1e-4 eV (formaldehyde/aug-cc-pVDZ B3LYP, pruned-grid error), test bar 1e-3 eV; B3LYP/aug-cc-pVDZ on the reference's own flat 75 × 110 grid ≤ 2.8e-6 eV (bar 2e-5 eV). τ-only meta-GGAs (SCAN, r2SCAN, TPSS) take the τ kernel; r2SCAN matches PySCF on water/6-31G only, on the flat 75 × 110 grid (2.1e-6 eV). Meta-GGA runs default to a 150 × 434 grid, on which r2SCAN water roots sit within 9.2e-5 eV (6-31G) and 5.5e-6 eV (aug-cc-pVDZ) of a flat 150 × 590 grid (75 × 110: 2.1e-3 eV); SCAN is not grid-converged at 1e-4 eV on any grid tested (see Known limits). A [tddft] xc with no f_xc kernel (VV10, range-separated) is refused before the SCF. |
tddft | RPA/GW § TDDFT | RHF (TDHF), or RKS via [tddft] xc (refuses multiplicity > 1) | ✓ (excitations) | — | — | run_tddft(method="casida") | water-tddft-pbe.toml | Proven (narrow, closed shell) | Same comparison of the lowest five roots against PySCF TDDFT, same bar. Same functional refusals as tda. |
task = "optimize" is accepted only for rhf, ksdft, uhf, rohf,
pdep-rpa and rimp2. task = "frequencies" is accepted only for rhf,
ksdft, uhf and rohf. On an open-shell molecule both tasks are accepted
only for uhf, rohf and ksdft (UHF/UKS, ROHF/ROKS). Both tasks refuse
[dft] grid_prune, and [scf] k_builder = "cosx" except for rhf, ksdft,
uhf and rohf (the overlap fit with a functional, and for rohf, is
refused before the SCF); a COSX final pass is skipped for them. The RKS, UKS and ROKS
optimizers and frequency drivers apply D3(BJ) and MBD@rsSCS (energy and exact
gradient); an
open-shell task = "energy" single point applies either. Any
other combination exits with an error before the SCF runs.
Open shells in the CLI
uhfandrohfread[molecule] multiplicitydirectly. With[dft] functionalthey run UKS and ROKS, for energies, optimizations and frequencies.ksdftwithmultiplicity > 1runs UKS (theuhfroute with the functional set). For ROKS userohfwith[dft] functional.rhfrefusesmultiplicity > 1and points touhf/rohf.rimp2andoo-rimp2run on the same plain UHF thatkind = "uhf"runs, then take unrestricted RI-MP2 (UMP2, as PySCFmp.MP2(uhf)) and unrestricted OO-RI-MP2.[mp2] kappais refused on an open shell.task = "energy"only: there is no unrestricted MP2 nuclear gradient.pdep-rpa,gwandmp2-vsolve UHF with MOM after 5 iterations whenmultiplicity > 1, fortask = "energy"only. Forpdep-rpaandgw, setting[rpa] xcmakes that reference UKS;gwwith[gw] reference = "rohf"uses ROHF (ROKS with[rpa] xc) instead, semi-canonicalized per spin; U-G0W0 on a semi-canonicalized ROHF reference is in the Anchors, the ROKS reference is not compared.mp2-vdoes not read[rpa] xcand stays UHF.linlccdandwb97x-l-vrefuse an open-shell molecule; their open-shell versions are library-only (ferric_cc::linlccd_u::u_linlccd,ferric_cc::double_hybrid::u_solve_wb97x_l_v).ccd,ccsdandccsd(t)refuse an open-shell molecule, and no open-shell CCD, CCSD or CCSD(T) exists in the library either: every CC solver reads restricted orbitals.- Every other kind refuses
multiplicity > 1with an error before any integral is computed.
Python entry points
These are Python functions without a method.kind of their own; some rows
also have a CLI route through a TOML section or task, noted in the row.
Scope is taken from the binding code and its docstrings. They are not graded
individually.
| Capability | Python | Scope (verified in code) |
|---|---|---|
| Open-shell KS frequencies | run_frequencies(reference="uhf"|"rohf", xc=...) | Setting xc promotes RHF/UHF/ROHF to RKS/UKS/ROKS. FD Hessian. Also CLI: kind = "uhf"/"rohf" with [dft] functional and task = "frequencies". |
| Transition-state search | run_saddle | P-RFO. Closed shell only (refuses multiplicity ≠ 1). Raises if the start has no negative mode. Costs 2(6N+1) + (steps+1) gradients. |
| Reaction path | run_irc | Both IRC branches from a saddle's imaginary mode. Closed shell only. |
| Geometry optimization (Python) | run_optimize | RHF only (no xc argument). Accepts point charges and a field. |
| QM/MM energy + forces | QmmmSystem, run_qmmm | method = "rhf"/"uhf"/"rks"/"uks". Link atoms, boundary schemes (keep/delete-host/rc/rcd), Gaussian-smeared charges, Thole polarizable sites, an optional MM force field (MmTopology). See QM/MM. The CLI [qmmm] section covers fixed point charges only. |
| QM/MM optimization | run_optimize_qmmm | Same four methods. move_mm = "none"/"all"/("within", r)/("residues", [...]). Moving MM atoms requires mm_topology. |
| Constrained DFT | run_cdft, CdftConstraint | UHF, or UKS when functional names a libxc functional other than "HF" (None and "HF", any case, give UHF). Constrained UKS/PBE matches NWChem 7.2.2 (see anchors); UHF-cDFT state selection is compared against NWChem's DFT module with Hartree–Fock exchange (see anchors); UHF energies along a target scan and UHF couplings have no external comparison. Fragment target is a Becke electron population ("charge": Nα + Nβ; "spin": Nα − Nβ), not a net charge. Raises if the λ loop does not converge. Not graded; see Constrained DFT. |
| cDFT electron-transfer coupling | cdft_coupling | Wu–Van Voorhis H_ab between two run_cdft states, each with one converged "charge" constraint, on the same geometry, basis, occupations and Hamiltonian. Not graded. |
| Point charges / uniform field | point_charges=, external_field= on run_rhf/run_uhf/run_rohf/run_dft, run_optimize, run_frequencies, run_saddle, run_irc, run_pdep_rpa | Bohr and atomic units. run_rhf also takes smeared_charges=. The MP2/CC drivers take no external-potential arguments. The CLI equivalent is [external_potential], where a width makes a charge smeared. |
| D3(BJ) | run_dft(dispersion="d3bj"), run_frequencies(xc=..., dispersion="d3bj"), d3bj_energy | Additive, with parameters fitted per functional. run_frequencies takes it on RKS, UKS and ROKS. |
| MBD@rsSCS | run_dft(dispersion="mbd"), run_frequencies(xc=..., dispersion="mbd"), mbd_rsscs_energy | run_dft adds it to total_energy from Hirshfeld volume ratios of the converged density (DftResult.volume_ratios), with β published for PBE, PBE0, HSE06. With with_gradient=True it adds the analytic gradient: the exact analytic gradient, including the orbital relaxation of the Hirshfeld volumes (Z-vector; LDA, GGA and hybrid-GGA functionals without VV10). mbd_rsscs_energy is the standalone energy from caller-supplied ratios. |
| Charges | mulliken_charges, lowdin_charges, hirshfeld_charges, chelpg_charges, resp_charges | Take an RhfResult or DftResult. Mulliken, Löwdin, CHELPG and RESP are documented closed-shell only. hirshfeld_charges uses the CLI's free-atom SCF proatoms, solved with the result's SCF settings; proatom="slater" selects the single-Slater proatom. RESP is a single-stage restrained fit, not multi-conformer RESP. |
| Electrostatic potential | esp_at_atoms, esp_at_points | Evaluated exactly from the density. esp_at_points takes (N, 3) points in Bohr. |
| Polarizability / moments | hirshfeld_polarizability, orbital_moments, density_second_moment | — |
| ω tuning | tune_omega | Range-separation ω for a named functional. With continuation=True (off by default) each evaluation after the first is seeded from the nearest already-evaluated ω; the first uses the default guess. The cation's spin-population asymmetry is compared with that same neighbour (branch_tol, default 1e-7); a mismatch sets branch_warning and the per-evaluation branch_changed. ⟨S²⟩ is reported but not compared, because its smooth ω-drift exceeds the signal. With check_cation_stability (default on) every cation also gets the UKS internal-stability analysis: each evaluation carries cation_lambda_min (the orbital Hessian's lowest eigenvalue) and cation_stability, and a tuned ω whose cation is an internal saddle is an error naming ω and λ_min. A functional whose Hessian cannot be built (VV10, so ωB97X-V) is reported not_analysed per evaluation with a stability_warning; that is never read as stable. |
| Conformer statistics | boltzmann_weights, weighted_stats*, ConformerEnsemble | — |
| Integrals | compute_eri3, compute_eri3_mo, compute_metric_2c, boys_localize, shell_info | Low-level access. |
Polarizable (Thole) embedding is Python and Rust only. IEF-PCM, UHF
stability descent and terfc-attenuated MP2 are reachable from both: see
[pcm], [scf] stability_descent and [mp2] att_operator in the
Input reference.
Anchors
Where numbers are checked, they are checked against external references or
exact limits, not against ferric's own earlier output. "Stated agreement" is
the measured difference recorded in the repository; "test tolerance" is what
the pinning test asserts, which is often looser. Proof links to
the test file that asserts the row, and, where one exists, the script that
generated its reference data; a row with nothing to link is not graded Proven.
Proof files named validation_*.rs are #[ignore]d, so an ordinary
cargo test skips them; they run in the weekly validation / validation-* CI jobs and on
demand (cargo nextest run --run-ignored only -E 'binary(/^validation_/)').
| Capability | System / basis | Reference | Stated agreement | Test tolerance | Proof |
|---|---|---|---|---|---|
| Bundled basis sets: every registered orbital, RI and JK set loads to the published contractions | 45 orbital-set RHF cases (Pople 6D/5D per the Gaussian convention, Dunning, def2, pcseg, -PP with ECPs), 26 RI-MP2 aux cases, 6 DF-HF JK cases (incl. aug-cc-pV{D,T,Q}Z-JKFIT); small molecules (H2O, HCl, CH3Cl, HBr, Pd, …) | PySCF 2.13.1 with each basis taken from BSE 0.12 by name (never ferric's files); aug-JKFIT from Psi4, digit-identical to PySCF's copy | 1.8e-12 Ha (RHF); 2.4e-10 Ha (RI-MP2, PP-RIFIT on Pd; others ≤ 5e-11); 7.3e-13 Ha (DF-HF) | 1e-8 Ha | validation_basis_coverage.rs (full sweep), basis_coverage_vs_pyscf.rs (per-commit subset), gen_pyscf_basis_coverage_refs.py |
| UHF and ROHF energies, stability-checked | HO2, NO2, CH2 (triplet), allyl / 6-31G, def2-SVP | PySCF UHF/ROHF + stability() | 4.0e-12 Ha (energy); 3e-7 (⟨S²⟩) | 1e-10 Ha; 1e-6 | validation_open_shell_scf.rs, gen_uhf_rohf.py |
SCF stability: lowest orbital-Hessian eigenvalues and verdict (UHF internal, UKS internal, RHF internal, RHF→UHF external). RhfConfig::check_stability on an RHF/HF run reports both the internal (singlet) verdict on ScfResult::stability and the external (RHF→UHF triplet, rhf_external_stability) verdict on ScfResult::stability_external; the external operator is also checked against the triplet block of ferric's UHF Hessian at the RHF point, an independent construction | N2⁺, OH (UHF), water at r(OH) = 0.9572 and 2.0 Å (RHF), NH2 (UKS/PBE) / 6-31G; H₂ at 2.0 Å / STO-3G (fast tier) | PySCF dense gen_g_hop_uhf/gen_g_hop_rhf/hop_rhf2uhf Hessians + stability() | eigenvalues ≤ 4.5e-10 Ha; energies ≤ 7.1e-12 Ha; the dedicated triplet operator matches the UHF-at-RHF triplet block to ≤ 2.3e-13 Ha (two independent constructions); stable/unstable verdicts match PySCF, including water at 2.0 Å where internal is STABLE (+1.97e-2) and external UNSTABLE (−3.07e-1); OH (an exact zero mode) is reported MARGINAL by ferric where PySCF reports stable. KS references are refused for the external channel (no triplet XC kernel f_αα − f_αβ) | 1e-8 Ha; 1e-10 Ha; 2e-12 Ha | validation_scf_stability.rs, scf_stability_external.rs, validation_rsh_stability.rs, gen_scf_stability.py |
SCF convergence ladder with RHF internal stability descent (solve_rhf_ladder, check_stability + scf_stability_descent): converged energy, internal (singlet) and external (RHF→UHF) Hessian eigenvalues | N2 at 1.60 Å, CuCN, Cr(CO)6 / def2-SVP (Cu, Cr from Basis Set Exchange) | PySCF RHF .newton() from four guesses; dense singlet and triplet orbital Hessians from MO integrals, checked against PySCF's own operators | energy ≤ 8.8e-10 Ha; eigenvalues ≤ 1.3e-9 Ha; the plain-DIIS rung lands on the N2 saddle 1.906e-2 Ha higher; the UHF-Hessian cross-construction of the external eigenvalue runs on N2 and CuCN only, and Cr(CO)6 asserts the dedicated triplet operator's result (its own weekly CI job, validation-cr-co6) | 1e-8 Ha; 1e-8 Ha | validation_scf_ladder.rs, rhf_stability_descent.rs, gen_scf_ladder.py |
| RHF and UHF with def2 ECPs: energies and analytic gradients | HI, CH3I, RbH, I atom (UHF) / def2-SVP, def2-TZVP; SnH4 / def2-TZVP; HBr (all-electron control) | PySCF RHF/UHF + analytic gradient, and ORCA RHF (NoRI), both fed ferric's basis and ECP | ≤ 2.9e-8 Ha (energy, both codes); ≤ 2.3e-8 Ha/Bohr (gradient); all-electron HBr 1e-11 | 2.5e-7 Ha; 1e-7 Ha/Bohr | validation_ecp.rs, gen_ecp.py, gen_ecp_orca.py |
| RHF, UHF, RKS, UKS, ROKS and RI-MP2 nuclear gradients with def2 ECPs: full gradient, the ECP term alone (HF and KS; for RI-MP2 the ECP term is tested through the full gradient and a control that the HF-density ECP term misses), translation invariance, 5-point finite difference of ferric's own energy | HF: HI, CH3I / def2-SVP, def2-TZVP; SnH4 / def2-TZVP; CH2I (UHF doublet) / def2-SVP, def2-TZVP; HBr (all-electron control). KS/PBE: CH3I (RKS), CH2I (UKS, ROKS) / def2-SVP. RI-MP2: CH3I / def2-SVP, def2-svp-rifit | HF: PySCF analytic gradient with its ECP term separated (checked against a fixed-density finite difference), and ORCA EnGrad (NoRI), both fed ferric's basis and ECP. KS: PySCF with exact J, the matched (75,110) Becke grid and grid_response=True (analytic for RKS/UKS, 5-point finite difference of the ROKS energy). RI-MP2: 5-point finite difference of the PySCF DF-MP2 energy, its ECP term from the relaxed-energy derivative with respect to a λ dV_ECP/dR hcore perturbation, and ORCA RI-MP2 EnGrad | HF: gradient ≤ 1.3e-7 Ha/Bohr vs PySCF and ≤ 2.5e-7 vs ORCA; the ECP term alone ≤ 1.3e-7; own 5-point FD ≤ 1.9e-7; energy ≤ 2.9e-8 Ha. KS: energy ≤ 2.0e-8 Ha, gradient ≤ 1.4e-8 Ha/Bohr, the ECP term alone ≤ 1.1e-8, own FD ≤ 4.3e-8. RI-MP2: E_corr 7.4e-10 Ha; gradient 7.7e-9 Ha/Bohr vs the PySCF finite difference and 4.3e-8 vs ORCA | HF: 5e-7 Ha/Bohr (PySCF), 1e-6 (ORCA), 6e-7 (FD). KS: 2e-7 (PySCF), 6e-7 (FD). RI-MP2: 1e-7 (PySCF FD), 5e-7 (ORCA) | validation_ecp_gradient.rs, validation_ecp_rimp2_gradient.rs, gen_ecp_gradient.py |
| ESP at the nuclei | H2O, CH3OH (RHF), HO2 (UHF) / cc-pVDZ, def2-SVP | PySCF int1e_rinv on PySCF's converged density, fed to ferric in ferric's AO order (AO order checked through the overlap matrix) | 4.8e-14 a.u. same density; 4.6e-9 own SCF | 5e-13 a.u.; 5e-8 a.u. | validation_density_properties.rs, gen_properties.py |
| Electric field at the nuclei | H2O, CH3OH, HO2 / cc-pVDZ, def2-SVP | PySCF int1e_iprinv, same density (checked against a finite difference of the ESP) | 1.5e-13 a.u. same density; 2.7e-9 own SCF | 1e-12 a.u.; 3e-8 a.u. | ″ |
| Becke effective volumes | H2O, CH3OH, HO2 / cc-pVDZ, def2-SVP; free atoms Z = 1–18 (UKS-PBE; B, C, O, F, Al, Si, S, Cl as the fractional-occupation ensemble) / aug-cc-pVDZ | numpy on ferric's grid rebuilt from PySCF's radial, Lebedev and AO values, with ferric's Becke partition; free atoms vs PySCF UKS | 1.4e-15 rel. same density; 7.1e-9 (molecules), 3.5e-8 (atoms) own SCF; Li, Be, Na, Al 1.5e-6 from ferric's XC density floor (ρ ≤ 1e-10), which the reference does not apply; free-atom energies 3.1e-12 Ha | 1e-13; 5e-8; 2e-6 (Li, Be, Na, Al) | ″ |
Hirshfeld charges (hirshfeld_charges) with the same-basis free-atom SCF proatom (what the CLI and ferric.hirshfeld_charges pass by default) and with the single-Slater proatom (proatom="slater"); free-atom proatom tables (spherically_averaged_proatom) | H2O, CO, CH3OH (RHF) / cc-pVDZ, def2-SVP; free H, C, O (UHF) and He (RHF) atoms; HeH promolecule (zero-charge limit) | numpy on ferric's grid rebuilt from PySCF's radial, Lebedev and AO values, with PySCF's converged density and PySCF free-atom proatoms tabulated and interpolated as ferric does (scipy cubic spline of ln ρ, an independent build of the interpolant); the same charges on a dense (200, 590) grid and on PySCF's own level-9 grid (agreeing to ≤2.8e-7 e) | 1.9e-13 e same density; 2.6e-9 e own SCF and own free atoms; ferric's default grid ≤2.0e-4 e from the dense-grid value; the 0.05 Bohr proatom table ≤2.9e-7 e from a 0.005 Bohr one; promolecule 8.7e-6 e | 1e-10 e; 1e-6 e; 1e-3 e; 5e-5 e | validation_hirshfeld.rs, gen_hirshfeld.py, validation_properties_hirshfeld.rs |
Hirshfeld effective volumes (atomic_effective_volumes_hirshfeld, the TS C6 volumes), on the atom-centred Becke–Lebedev grid of hirshfeld_volume_grid_config (75 radial × 590 angular — the volume integrand carries an r³ factor and needs a higher Lebedev order than the XC grid's 110); and atomic_effective_volumes_hirshfeld_on_grid on the 0.20 Bohr lattice, which MBD@rsSCS integrates on | H2O, CO, CH3OH / cc-pVDZ, def2-SVP (SCF proatom); free H, C, O atoms (no proatom, as the CLI's free-atom denominator) | numpy on a dense (200, 590) Becke–Lebedev grid, same density and proatoms; the free-atom weight-one moment ∫ρ r³ against the same grid is the exactness anchor (one atom ⇒ Hirshfeld weight 1); plus numpy on ferric's lattice rebuilt point for point for the lattice entry point | 1.1e-7 rel. vs the dense grid, same density and own SCF alike; ∫ρ within 2.0e-7 e of N_e on the same grid; CH3OH's two mirror-image methyl H equal to 2.9e-14 rel.; free-atom weight-one moment 8.1e-13 rel. (the 1e-12 Hirshfeld weight floor then removes 1.2e-3 of free H's volume, 3.4e-10 of C's); lattice entry point 3.2e-13 rel. same density | 1e-6; 2e-6 e; 1e-10; 1e-11; 1e-11 | ″ |
TS C6 and MBD@TS given the same volume ratios: TS α_eff, ω, pair and molecular C6 (ts_atom_params, ts_dynamic_polarizability, casimir_polder_c6); full-range SCS screened α(iω) and C6 (mbd_screen, mbd_dynamic_polarizability), and the plain Gaussian-damped coupled-oscillator energy (coupled_qho_energy_plain_gg), which checks the oscillator algebra and is not a dispersion energy | H2O, CO, CH3OH / cc-pVDZ, def2-SVP; CH4, benzene / cc-pVDZ (Hirshfeld volume ratios); ferric's and pymbd's frequency grids | pymbd 0.15.0 / libmbd: free-atom table checked equal for every element used; MBD screening as libmbd scs and energy as libmbd plain screening with Gaussian damping (dip,gg), cross-checked against a numpy build and mpmath dimer closed forms | TS ≤ 7.5e-16 rel.; MBD 8.1e-15 against the same A&S erf ferric uses, 5.1e-7 against an exact erf | 1e-12; 1e-11; 1e-6 | validation_ts_mbd.rs, gen_ts_mbd.py |
MBD@rsSCS energy (mbd_rsscs_energy, mbd_rsscs_energy_from_params): range-separated SCS screening (short-range (1−f)·T_GG), screened α₀, C6, ω and R_vdW, and the long-range coupled-oscillator energy with f·T_bare; free-atom R_vdW table (Z = 1–54); frequency grid | H2O, CO, CH3OH / cc-pVDZ, def2-SVP; CH4, benzene / cc-pVDZ (Hirshfeld volume ratios); β = 0.83 (PBE) and 0.85 (PBE0/HSE06), a = 6 | pymbd 0.15.0 (screening + mbd_energy in Python) and libmbd 0.15.0 (Fortran, variant='rsscs') given identical inputs, which agree with each other to < 1e-10; R_vdW table read back from pymbd vdw_params; grid vs pymbd freq_grid(15) | E ≤ 9.5e-12 rel.; screened α₀, C6, ω, R_vdW ≤ 1.5e-14 rel.; grid 3.8e-14 | 1e-10; 1e-12; 1e-12 | validation_mbd_rsscs.rs, gen_ts_mbd.py |
MBD@rsSCS nuclear gradient, exact (mbd_rsscs_gradient; on an SCF mbd_rsscs_for_scf, used by [dft] dispersion = "mbd" with task = "optimize" and by run_dft(with_gradient=True)): explicit term at fixed volume ratios; Hirshfeld-volume term with the occupied orbitals held fixed (AOs and proatoms follow the atoms, orthonormality term −½ Tr[V D Sˣ D], centroid-anchored lattice response); orbital relaxation from the KS Z-vector (ferric_scf::zvector_ks: closed-shell for RKS; coupled α/β for UKS, with orthonormality term −Σ_σ Tr[V D_σ Sˣ D_σ]; three-block closed/open/virtual for ROKS, with orthonormality term −Tr[Sˣ W_Q], W_Q = D_α V D_α + P_c V P_c + ½ (P_c V P_o + P_o V P_c)) | explicit: the 8 systems × 2 β above; SCF terms: H2O / STO-3G (HF and PBE), H2O / 6-31G (PBE, PBE0, HSE06, PBE + RI-J), NH3 / 6-31G (PBE); UKS: OH / STO-3G (PBE), NH2, OH (doublets) and O2 (triplet) / 6-31G (PBE, PBE0, HSE06), OH / 6-31G (PBE + RI-J, PBE0 + RI-JK); anchor: H2O / STO-3G RKS run through the UKS path (PBE, PBE0, HSE06); ROKS: HCO (doublet) and CH2 (triplet) / STO-3G (PBE), HCO, NH2 (doublets), CH2 and O2 (triplets) / 6-31G (PBE, PBE0, HSE06), HCO / 6-31G (PBE + RI-J); anchor: H2O / STO-3G RKS run through the ROKS path (PBE, PBE0, HSE06) | explicit vs libmbd 0.15.0 force=True (checked against libmbd's own finite differences to ≤ 4.2e-11) and vs central FD in every position and every α₀, C6, R_vdW input; volume term vs central FD of the volumes on a fixed lattice at fixed D; the unrelaxed gradient vs central FD of the pipeline with the reference occupied orbitals re-orthonormalized at each displaced geometry; the exact gradient vs central FD of the full SCF + MBD pipeline, and its relaxation term vs the difference of the two FDs | explicit 6.2e-16 abs vs libmbd; parameter derivatives ≤ 1.3e-6 rel. (FD); volume term 2.5e-8 abs (Slater proatom), 6.5e-9 (tabulated proatom), both at h = 1e-4; unrelaxed 3.4e-9 (H2O/STO-3G HF); exact 8.0e-10 (H2O/STO-3G PBE), ≤ 3.5e-9 (H2O/6-31G, all four), 2.0e-10 (NH3/6-31G) Hartree/Bohr at h = 1e-3; relaxation term (1.0e-5 H2O, 1.6e-6 NH3) vs FD ≤ 3.5e-9; UKS (h = 3e-5): unrelaxed 3.9e-11 and exact 2.9e-11 (OH/STO-3G, h = 1e-3), exact ≤ 1.9e-9 (6-31G, exact J/K), 6.0e-9 (RI-J), 5.0e-9 (RI-JK), relaxation term (2.7e-6–1.0e-5) vs FD ≤ 6.0e-9; the UKS path on a closed shell reproduces the RKS gradient to 3.0e-14; ROKS (h = 1e-3, re-orthonormalizing closed and open orbitals as one set for the unrelaxed FD): unrelaxed ≤ 2.2e-11 against a closed–open cross term of 7.1e-8–3.2e-7, exact ≤ 2.1e-11 (STO-3G), ≤ 2.1e-11 (6-31G, PBE and PBE0), ≤ 4.5e-10 (HSE06), 7.5e-9 (RI-J), relaxation term (2.7e-6–9.8e-6) vs FD ≤ 7.5e-9, every displaced SCF on the reference state (max|ΔD|/h ≤ 0.39 per Bohr); the ROKS path on a closed shell reproduces the RKS gradient to 2.7e-14 | 1e-14; 1e-5 rel.; 1e-6 rel. (Slater), 5e-8 rel. (tabulated); 5e-8; 5e-9; UKS 5e-10, anchor 1e-12; ROKS 5e-10, anchor 1e-12 | validation_mbd_rsscs.rs, mbd_scf_gradient.rs, mbd_scf_gradient_uks.rs, mbd_scf_gradient_roks.rs, unit tests in dispersion/mbd_rsscs.rs |
MBD@rsSCS exact gradient on ²Π radicals (UKS and ROKS Z-vector with the axis-rotation null mode projected out, zvector_ks::NullModeDiagnostics) | OH / STO-3G (UKS PBE, PBE0; ROKS PBE0), OH / 6-31G (UKS PBE), NO / STO-3G and 6-31G (UKS PBE), CH / 6-31G (ROKS PBE); null-mode identification on OH, NO / STO-3G and OH / cc-pVDZ | central FD of the full SCF + MBD pipeline (h = 1e-3); the softest eigenvector of the dense orbital Hessian, an independent construction of the mode | exact gradient 7.1e-12–5.4e-11 Hartree/Bohr against relaxation terms of 2.2e-7–1.0e-5; the mode matches the softest eigenvector to |cos| > 0.9999, Rayleigh quotients −3.5e-3 to +9.9e-3 against ≥ 0.57 for every other direction; a non-linear radical (NH2) is bit-identical to the unprojected solver | 5e-10; 0.9999; 0.05 | mbd_scf_gradient_degenerate.rs, unit tests in zvector_ks.rs |
Harmonic frequencies with dispersion (harmonic_frequencies_with_scf_correction; CLI task = "frequencies" + [dft] dispersion, Python run_frequencies(dispersion=...)): central differences of the KS + dispersion analytic gradient, each from the SCF converged at that displaced geometry | H2O / STO-3G, PBE (D3(BJ), MBD@rsSCS); H2 / STO-3G HF with a synthetic harmonic-spring correction | D3: the dispersion Hessian vs 4-point second differences of the D3(BJ) energy, and the frequencies vs those of the KS Hessian plus that independent D3 Hessian; MBD: v·H·v along three fixed directions vs second differences of the full SCF + MBD energy; spring: the closed-form spring Hessian; a no-op correction reproduces the plain FD Hessian bit for bit | D3 5.3e-9 Hartree/Bohr² (largest element 1.9e-5), frequencies 9.8e-6 cm⁻¹; MBD 1.5e-6 (0.8% of 2.0e-4, limited by noise in the energy differences; without the orbital-relaxation term 3.2e-5); spring 6.6e-8 at a 1e-3 Bohr step (truncation, 25× smaller than at 5e-3) | 2e-8; 5e-5 cm⁻¹; 3e-6; 2e-7 | dispersion_frequencies.rs |
Harmonic frequencies with dispersion (open shell): harmonic_frequencies_with_scf_correction on UKS (reference = Uhf) and ROKS (reference = Rohf); CLI uhf/rohf/ksdft + [dft] functional + [dft] dispersion with task = "frequencies", Python run_frequencies(reference="uhf"|"rohf", dispersion=...). Central differences of the KS + dispersion gradient, each from the UKS/ROKS SCF converged at that displaced geometry; MBD@rsSCS with the UKS or ROKS Z-vector | PBE / STO-3G, bent geometries: NH2 (doublet, UKS), HCO (doublet, ROKS; in-plane SOMO), CH2 (triplet, UKS and ROKS). OH is excluded (degenerate SOMO) | D3: the dispersion Hessian vs 4-point second differences of the D3(BJ) energy, the frequencies vs those of the KS Hessian plus that independent D3 Hessian, and the translational and rotational sum rules of the D3 Hessian; MBD: v·H·v along three fixed directions vs 5-point second differences (h = 0.08 Bohr) of the full SCF + MBD energy, with the unrelaxed gradient (no Z-vector) as negative control; a no-op correction reproduces the plain UKS / ROKS FD Hessian bit for bit, and the correction sees an SCF of the requested spin | D3 3.7e-9 (NH2) and 6.5e-9 (HCO) Hartree/Bohr² (largest elements 1.7e-5, 2.9e-5), frequencies 9.4e-6 and 3.0e-5 cm⁻¹; sum rules: translation ≤ 1.1e-11, rotation ≤ 3.0e-9 against a gradient term of 1.9e-6–1.1e-5; MBD ≤ 4.8e-8 (curvatures 6.1e-5–2.1e-4) vs 8.3e-7–1.6e-5 without the orbital-relaxation term; full corrected Hessian translational sum ≤ 1.3e-6 (KS grid noise) | 2e-8; 5e-5 cm⁻¹; 1e-10, 1e-8; 2e-7; 1e-5 | dispersion_frequencies_open_shell.rs, validation_dispersion_frequencies_open_shell.rs |
D3(BJ) dispersion energy and analytic gradient (ferric_d3::d3bj_energy, d3bj_gradient), two-body and with the Axilrod–Teller–Muto three-body term (s9 = 1: zero damping, rs9 = 4/3, exponent 16, CN-interpolated C6) | Two-body: 24 system/functional cases. With ATM: water, methane, Ar2, water dimer, Br2, benzene, Ar3, CF2ONH, CHBr3 × PBE, PBE0, B3LYP (27 cases; the three-body term is nonzero on the 21 with three or more atoms) | simple-dftd3 1.6.0 (dftd3 Python package, an independent Fortran implementation) with the parameters passed explicitly; central FD of ferric's energy; a sympy proof of the CN chain rule and of the ATM gradient | two-body energy ≤ 1.5e-13 Ha; with ATM: energy ≤ 1.2e-13 Ha, gradient ≤ 5.5e-13 Ha/Bohr, the three-body part alone ≤ 2.6e-15 Ha and ≤ 5.9e-16 Ha/Bohr; analytic vs FD ≤ 3.0e-11 Ha/Bohr (CF2ONH, CN spread 1.75); E linear in s9 to 1e-15, a dimer bit-identical at s9 = 0 and 1 | 1e-12 Ha (energy); 1e-11 Ha/Bohr (gradient); 1e-12 Ha/Bohr (three-body gradient); FD 1e-7 Ha/Bohr | vs_reference_dftd3.rs, vs_reference_dftd3_atm.rs, gradient_vs_fd.rs, gen_d3_atm_reference.py, d3_cn_chain_rule_gradient_sympy.py, d3_atm_gradient_sympy.py |
| Static α (direct RPA with a density-fitted Coulomb kernel, no exchange; not CPHF) | H2O / aug-cc-pVDZ; CH3OH / cc-pVDZ | numpy linear solve on PySCF int3c2e with the same aux basis | 4.8e-14 rel. same orbitals; 5.3e-10 rel. own SCF | 5e-13; 5e-9 | validation_static_alpha.rs, gen_properties.py |
Dynamic α(iω) and Casimir–Polder C6 (direct RPA, density-fitted Coulomb kernel, no exchange), molecular and Becke per-atom (molecular_dynamic_polarizability, pdep_polarizability_becke_dynamic, casimir_polder_c6); per-atom α is the Krishtal–Senet–Van Alsenoy intrinsic polarizability (atom-centred Becke dipole on the bra, analytic molecular dipole on the ket; JCP 125, 034312 (2006)) and the charge-transfer remainder α_CT = α_mol − Σ_A α^A (charge_transfer_remainder[_dynamic]); ω = 0 against the static α, molecular and per-atom | H2O, N2 / aug-cc-pVDZ (aug-cc-pvdz-rifit) | numpy sum over Casida excitations on PySCF int3c2e with the same aux basis, at ferric's 20 Gauss–Legendre nodes (rebuilt with leggauss); per-atom and α_CT on the copy of ferric's Becke grid, α_CT also as the independent charge-flow form 4 (Σ_A R_A q^A)ᵀ R μ | α(iω) 4.9e-14 rel. same orbitals; C6 6.2e-14; per-atom 3.4e-14; 5.3e-10 own SCF; α_CT,iso(0) is 27.0% (H2O) and 22.7% (N2) of α_iso; α_CT vs charge-flow form 1.1e-4 / 2.0e-5 (grid error of the lab dipole) | 1e-11; 1e-11; 1e-10; 5e-9; α_CT 1e-10, charge-flow 1e-3 | validation_pdep_c6.rs, gen_pdep_c6.py |
NPZ export ([rpa] export_npz) | water / STO-3G, CLI pdep-rpa | numpy.load of the CLI's file against the Python bindings | exact (keys, shapes, dtypes, C order, geometry); per-atom and molecular α origin-independent to 3.4e-13 rel.; alpha_ct = alpha_tensor − Σ_A alpha_atomic | exact; 1e-11 rel. under translation; 1e-13 | test_validation_npz_export.py |
| NPZ export against PySCF: all 30 keys for the default knobs plus the surface ESP, including the C6 block and the PDEP eigenpotentials; each knob turned off removes exactly its keys | water / cc-pVDZ, CLI pdep-rpa with exact J/K | numpy.load of the CLI's file against PySCF integrals contracted with the exported density and orbitals, and against PySCF RHF; α against the numpy dRPA-RI solve; PDEP eigenpotentials against PySCF's generalized dielectric eigenproblem; TS C6 recomputed from the exported α(iω) and the Tkatchenko–Scheffler free-atom table | integral level ≤2.8e-14; density/orbital-energy/property chain ≤5.9e-10; α 9.2e-11 rel.; PDEP metric 4.3e-12, projector 2.6e-7; C6 recomputed exactly | 5e-13; 1e-8 (density), 3e-9; 1e-9; 5e-11, 1e-6; 1e-12 | validation_npz.rs, gen_npz.py |
| KS-DFT energies: open-shell UKS and second-row closed shell | UKS: NH2, CH3, HO2, O2 (triplet) × PBE, B3LYP, ωB97X-V / 6-31G, def2-SVP; RKS: H2S, HCl, SiH4 × PBE, B3LYP, HSE06 / def2-SVP, def2-TZVP | PySCF UKS/RKS + stability(), same grid and density fitting; for UKS ωB97X-V both codes use exact J and range-separated DF-K, whose fitting metrics differ, which sets that functional's larger bar; for RKS HSE06 both use exact J and short-range DF-K in the attenuated metric | energy ≤3.1e-12 Ha (PBE, B3LYP), 4.2e-12 Ha (HSE06), 6e-6 Ha (ωB97X-V); ⟨S²⟩ ≤4.6e-9 (PBE, B3LYP), 1.3e-7 (ωB97X-V). The VV10 potential Vnl equals the derivative of Enl: FD(Exc) − Tr[V X] ≤ 8e-11 for a random symmetric X on ωB97X-V (NH2 / 6-31G, closed and polarized; test_xc_blocks.py), and the low-gradient σ derivative matches FD to 5.3e-7 relative (unit test in vv10.rs, bar 1e-5). The ωB97X-V static polarizability αzz of water / 6-31G (RI-JK) from the CPKS solve matches the finite-field SCF dipole to 1.5e-10 and the finite-field −d²E/dF² to 8.7e-10 relative (test_vv10_polarizability_matches_the_finite_field_dipole in test_response_blocks.py; bars 1e-8 and 2e-8); with the σ gate on ∂ω₀/∂σ restored the two routes miss by 1.3e-5 and 5.1e-5. | energy 1e-10 Ha (PBE, B3LYP, HSE06), 2e-5 Ha (ωB97X-V); ⟨S²⟩ 5e-8, 1e-6 | validation_ks_energies.rs, gen_ks_energies.py |
KS-DFT energies: ROKS (rohf with [dft] functional) | NH2, CH3, HO2, OH (degenerate π hole) × PBE, B3LYP / 6-31G, def2-SVP | PySCF ROKS, same grid and density fitting (RI-J; RI-K for B3LYP), six starts each polished by second-order SCF and checked with stability(); PySCF's own ROKS does not converge on OH without that polish, and its converged OH points spread by up to 1.3e-6 Ha (π-hole orientation on the grid) | 8.0e-13 Ha; OH 8.0e-7 Ha | 1e-11 Ha; OH 5e-6 Ha | validation_roks_energies.rs, gen_roks_energies.py |
| Meta-GGA energies (SCAN, r2SCAN) | RKS: H2O, NH3, H2S, CH4 / def2-SVP, def2-TZVP; RI-J (default) and exact J; UKS: NH2 / def2-SVP, def2-TZVP | PySCF RKS/UKS + stability(), same grid, density fitting and XC density floor | RKS energy ≤ 4.0e-12 Ha, UKS ≤ 2.4e-13 Ha; frontier orbitals ≤ 1.1e-8 Ha; ⟨S²⟩ ≤ 5.2e-10 | energy 5e-11 Ha; orbitals 1e-7 Ha; ⟨S²⟩ 5e-9 | validation_mgga_energies.rs, gen_mgga_energies.py |
| Meta-GGA TPSS (molecular): energies and closed-shell gradient | RKS: H2O, CH4 / cc-pVDZ; UKS: NH2 / cc-pVDZ; RI-J (def2-universal-jkfit); gradient: H2O / STO-3G | PySCF RKS/UKS (TPSS = MGGA_X_TPSS + MGGA_C_TPSS), flat (75,110) Becke-1988 grid, same density fitting; gradient: central FD of ferric's own energy | energy ≤ 2.1e-13 Ha (H2O 5.7e-14, CH4 1.6e-13, NH2 UKS 2.1e-13); gradient vs FD ≤ 1.4e-7 Ha/Bohr | 1e-4 Ha (the dft_scan.rs bar shared with SCAN / r2SCAN); 3e-4 Ha/Bohr. Limits: no PySCF gradient pin, no open-shell gradient, one basis per energy case | dft_scan.rs, dft_gradient_mgga.rs, gen_pyscf_mgga_refs.py |
SCF stability for a range-separated hybrid: the lowest UKS-internal orbital-Hessian eigenvalue and the stable/unstable verdict, with the exchange response built as c_SR·δK[erfc(ω)] + c_LR·δK[erf(ω)] from the same density-fitted SR/LR pair the converged Fock uses, and the f_xc kernel built at the same overridden ω. RhfConfig::check_stability refuses ωB97X-V itself (StabilitySkip::Vv10Kernel: nothing in the workspace has a VV10 response kernel), so this row is measured through the #[doc(hidden)] rsh_uks_lowest_hessian_eigenvalue entry point, which accepts that omission to compare against a reference making the same one | N2⁺ / def2-SVP, ωB97X-V with ω overridden to 0.40 and 0.60 Bohr⁻¹, bracketing the 0.53–0.56 instability onset; OH / cc-pVDZ for the fast-tier kernel and finite-difference checks | PySCF dense newton_ah.gen_g_hop_uhf on a dft.UKS with mf.omega overridden, exact J and the matched (75,110) / VV10 (50,50) grids, the cation converged from the default guess and not stability-followed (past the onset the symmetric branch is a saddle, which is the point); PySCF's KS response omits the VV10 second derivative and so does ferric's, so the comparison is like-for-like on that omission | λ_min ≤ 1.9e-7 Ha and SCF energy ≤ 1.7e-8 Ha at both ω; verdicts match (STABLE at 0.40, UNSTABLE at 0.60); the plain-Coulomb exchange response (one Coulomb-kernel δK scaled by c_SR) misses the same spectrum by ≥ 2.9e-1 Ha. Fast tier: the density-fitted SR/LR response agrees with c_SR·K[erfc(ω)] + c_LR·K[erf(ω)] from four-centre integrals to 7.7e-4 relative, while kernels swapped, c_SR for both, c_LR dropped and ω doubled each sit ≥ 0.42 away; the analytic matvec matches a finite difference of the orbital gradient at ω = 0.40 to 5.3e-12 (falling 16× for a 4× smaller step); erfc(ω) + erf(ω) at equal coefficients reproduces the global-hybrid exchange to 8.2e-15 relative in four-centre integrals; at ω = 0 the matvec is bit-identical | 2e-6 Ha (~10× the measured max); 1e-7 Ha; 8e-3 rel.; 1e-6 rel.; 1e-12 rel. | validation_rsh_stability.rs, rsh_orbital_hessian.rs, gen_scf_stability.py |
RSH ω tuning (tune_omega): the objective J(ω) = ε_HOMO(N) + E(N−1) − E(N) (signed; the tuner minimizes |J|) and the tuned ω* | H2O, NH3 / def2-SVP, ωB97X-V: J at ω = 0.2, 0.3, 0.4, 0.5, 0.6 Bohr⁻¹ and ω*; N2 / def2-SVP: J at ω = 0.2–0.5 only, because the N2⁺ cation turns UKS-unstable between ω = 0.53 and 0.56, below J's root. For ωB97X-V that onset cannot be flagged by tune_omega (VV10: no response kernel, so every evaluation is not_analysed and the result warns). On that symmetric branch ferric's J matches the PySCF probe to 7.4e-8 Ha (ω = 0.53) and 4.7e-7 Ha (ω = 0.56) and its ⟨S²⟩ to 3.0e-7, and J, ⟨S²⟩ and the cation spin-population asymmetry are all smooth across the onset: a lower cation state exists past it (reached when seeded toward it: 8.9e-5 Ha below the symmetric state at ω = 0.56, 4.6e-4 Ha at 0.60) but the tuner does not find it, and no energy, ⟨S²⟩ or population quantity flags it, because what changes there is a curvature; only the orbital Hessian's lowest eigenvalue does (next paragraph). continuation=True keeps ω* bit-identical on H2O and NH3; on H2O its J differs from the independent solves by SCF stopping noise in ε_HOMO that tracks density_conv: 1.7e-7 Ha at 1e-7, 2.6e-10 Ha at 1e-9 | PySCF RKS neutral and stability-checked doublet UKS cation with mf.omega overridden, same grid, exact J on both states and range-separated DF-K; ω* from brentq on J. Both states use exact J, as tune_omega does by default. Cation stability (non-VV10 ωB97X, def2-SVP, mf.omega overridden, default guess, dense gen_g_hop_uhf λ_min): N2⁺ λ_min = +3.06e-2, +2.01e-2, +1.23e-2, +2.2e-3 at ω = 0.30, 0.40, 0.45, 0.50 and −6.9e-3, −1.50e-2, −3.83e-2 at 0.55, 0.60, 0.80, so the symmetric cation is a saddle from between 0.50 and 0.55 on; H2O⁺ and NH3⁺ are ≥ +7.8e-2 at ω = 0.3, 0.6, 1.0. A tune_omega bracket past the onset raises an error naming ω and λ_min; a bracket below it returns with no warning | ε_HOMO ≤6.7e-6 Ha (N2 4.8e-7); IP ≤8.8e-7 Ha; J ≤6.3e-6 Ha; ω* ≤2.8e-5 Bohr⁻¹; cation λ_min ≤2.3e-8 Ha/rad² (N2 1.8e-9), verdict sign matches PySCF at all 13 points | 2e-5 Ha; 5e-6 Ha; 2e-5 Ha; 1e-4 Bohr⁻¹; 3e-7 Ha/rad² | validation_rsh_omega.rs, omega_tuning_cation_branch.rs, validation_omega_tuning_cation.rs, validation_rsh_cation_stability.rs, gen_rsh_omega.py, gen_rsh_cation_stability.py |
| Constrained DFT (Becke charge and spin constraints): E(N) − E_unconstrained, λ, and dE/dN = −λ | LiH (Li), HF (F), H2O⁺ (O; charge and spin) / 6-31G, def2-SVP, 3–4 targets each | NWChem 7.2.2 cdft ... pop becke, PBE, Becke-partitioned Treutler grid, fed ferric's basis; internal: natural-target limit (λ = 0) and dE/dN = −λ | unconstrained E 1.0e-8 Ha; E(N) − E_unc 2.0e-7 Ha; λ 1.1e-6 (NWChem, grid limit); dE/dN = −λ to 2.3e-12 Ha (Simpson, def2-SVP) | 5e-8 Ha; 1e-6 Ha; 5e-6; 5e-11 Ha | validation_cdft.rs, gen_cdft.py, NWChem inputs |
Constrained DFT state selection at an integer target: which constrained state ferric lands on, seeded from NWChem's orbitals (solve_cdft_uhf_seeded, CdftSeed) and unseeded with and without the stability descent | HeNe⁺ (N_He = 2, R = 2.0 Å), LiH⁺ (N_Li = 2.8, R = 3.0 Å) / def2-SVP, two states each | NWChem 7.2.2 cDFT (HF exchange), run both with its native Becke partition and with ferric's He/Ne radius ratio by relabelling the atoms; NWChem orbitals imported into ferric (AO order and the d(m=+1) sign mapped and checked) | one-shot energy of NWChem's orbitals ≤ 4.7e-10 Ha; seeded and unseeded states ≤ 7.7e-10 Ha after the quadrature correction; λ 3.2e-5 (HeNe⁺), 2.9e-7 (LiH⁺) | 1e-8 Ha; 1e-8 Ha; 1e-4 | validation_cdft_state.rs, gen_cdft_state.py |
cDFT-ET coupling (Wu–Van Voorhis, coupling_hab) and its ingredients: determinant overlap, one- and two-electron transition elements, |V|; KS-PBE diabats end to end | He₂⁺ at 2.50/3.00/3.50 Å / def2-SVP, aug-cc-pVDZ | NWChem 7.2.2 cDFT + et (its MO files recomputed in PySCF, every et number reproduced to ≤ 5.9e-11); the textbook form F = E + λN with its constraint-offset invariance | kernel ≤ 5.9e-11 Ha; coupling vs textbook ≤ 2e-18; E − E_unc ≤ 4.0e-7 Ha; λ ≤ 6.6e-6; |S| ≤ 2.8e-3 rel., |V| ≤ 8.3e-4 rel. (both largest at def2-SVP 3.50 Å, where |S_AB| = 3.9e-4 is the smallest of the six points; the absolute S offset is a flat 1.7e-7–1.1e-6 across the row, and V ∝ S so its relative error is smaller than S's everywhere); all six points converge end to end, deepest inner SCF 106 iterations (cap 150); and from λ = 0 with no multiplier start, all six reach the same root to ≤ 3.5e-10 in λ and ≤ 1.6e-10 Ha in E, in 11–21 outer iterations against the 3–4 a start at the root needs | 2e-10 Ha; 1e-12; 2e-6 Ha; 3e-5; 4e-3 rel. (S); 2e-3 rel. (V) | validation_cdft_et.rs, gen_cdft_et.py |
| IEF-PCM solver and SCF on PySCF's cavity; ferric's own cavity; the solver's analytic limits | water, NH3 / STO-3G, cc-pVDZ; ε = 78.4, 4.7. Limits: a unit point charge at d/R = 0, 0.25, 0.5 in one Lebedev sphere (50, 110, 302 points), ε = 78.4, 4; water's cavity for ε → ∞ | PySCF RHF.PCM() IEF-PCM; its SWIG cavity injected into ferric. Limits: the analytic Born and Kirkwood reaction-field energies, the Gauss-law charge −(ε−1)/ε, and the conductor equation S q = −v | charges 1.3e-17, E_pcm 3.5e-18 Ha (solver); total energy 4.8e-12 Ha (SCF); ferric's own cavity (modified-Bondi radii, 110-point spheres) 0.07–0.61% from PySCF. Limits at 302 points: Born 1.1e-8 (ε = 78.4) and 1.7e-7 (ε = 4) relative, Kirkwood ≤ 1.8e-4 relative at d/R = 0.5, each error falling monotonically 50 → 110 → 302; total charge within 1.3e-7 of Gauss's law; the conductor gap scales as 1 − f(ε) (ratio 100.000 between ε = 1e6 and 1e8) | 1e-11 Ha (solver); 1e-10 Ha (SCF); own cavity ±2%; limits: the a-posteriori estimate |E₃₀₂ − E₁₁₀| (|Q₃₀₂ − Q₁₁₀| for the charge), ratio within a factor 2 | validation_pcm.rs, gen_pcm.py, ief_limits.rs, pcm_ief_sympy.py |
| Geometry optimization, RHF and RKS | H2O, NH3, CH2O from distorted starts / RHF and B3LYP 6-31G, PBE cc-pVDZ | PySCF analytic gradient (KS with grid response) driven to max abs gradient ≤ 1e-6 Ha/Bohr by scipy BFGS; exact J/K | distances ≤ 2.6e-6 Bohr; angles ≤ 1.1e-4°; optimized energies ≤ 9.3e-13 Ha | 2e-5 Bohr; 1e-3°; 1e-10 Ha | validation_geometry_optimization.rs, gen_geometry_optimization.py |
| Geometry optimization, UHF, ROHF and UKS | HO2, CH3, NH2 from distorted starts / UHF, ROHF, UKS-PBE 6-31G | PySCF analytic gradient driven to max abs gradient ≤ 1e-6 Ha/Bohr by scipy BFGS; stability() at start and end | distances ≤ 2.6e-6 Bohr; angles ≤ 1.1e-4°; optimized energies ≤ 9.3e-13 Ha; ⟨S²⟩ ≤ 4.9e-9 (UHF, UKS; ROHF is a pure spin state and is not checked) | 2e-5 Bohr; 1e-3°; 1e-10 Ha; 5e-8 (⟨S²⟩, UHF and UKS) | validation_geometry_optimization.rs, gen_geometry_optimization.py |
| G0W0@HF | H2O / cc-pVDZ | Published MOLGW IP (11.97 eV); PySCF gw_ac (IP 12.160 eV) | — | 0.30 eV (IP); 0.20 eV (PySCF LUMO and gap) | h2o_g0w0_cohsex.rs |
| MP3 (RI integrals) | H2O, NH3 / cc-pVDZ, def2-SVP; all-electron and frozen core | exact-integral PySCF RHF, then the same aux basis and DF factorization as ferric; two independent numpy MP3 constructions (spin-orbital textbook, closed-shell via PySCF's linear doubles residual), agreeing to 2e-17 | ≤ 7.6e-12 Ha | 1e-10 Ha | validation_cc.rs, gen_cc.py, reference data |
| RI-CCD | H2O, NH3 / cc-pVDZ, def2-SVP | exact-integral PySCF RHF, then the same aux basis and DF factorization as ferric; PySCF CCD on the DF integrals | ≤ 1.9e-11 Ha | 2e-10 Ha | validation_cc.rs, gen_cc.py, reference data |
| RI-CCSD, spin-orbital and spin-adapted | H2O, NH3 / cc-pVDZ, def2-SVP; HCN / cc-pVDZ; H2O / aug-cc-pVDZ; C2H6 / cc-pVDZ (spin-adapted only) | exact-integral PySCF RHF, then the same aux basis and DF factorization as ferric; PySCF CCSD on the DF integrals | ≤ 4.7e-11 Ha (both solvers) | 2e-10 Ha | validation_cc.rs, gen_cc.py, reference data |
| CCSD(T), spin-orbital and spin-adapted (T) | H2O / cc-pVDZ, aug-cc-pVDZ; HCN / cc-pVDZ; C2H6 / cc-pVDZ (spin-adapted only) | exact-integral PySCF RHF, then the same aux basis and DF factorization as ferric; PySCF ccsd_t on the DF integrals | (T) ≤ 6.9e-12 Ha; the two (T) codes agree to 8.9e-12 | 1e-10 Ha | validation_cc.rs, gen_cc.py, reference data |
CAS-CI (library only: ferric_ci::run_cas_ci, closed-shell RHF reference, lowest Ms = 0 root): total, core and active-space energies | N2 at r = 1.10 and 2.20 Å, CAS(6,6) / cc-pVDZ; H2O, CAS(4,4) / 6-31G; each also with the window moved down one orbital | PySCF mcscf.CASCI on an exact-integral RHF with the same orbital window, checked against a dense diagonalization of the full active-space Hamiltonian. At 2.20 Å both codes use the symmetric RHF, which is internally unstable toward a symmetry-broken RHF 0.19 Ha lower | E_CASCI ≤ 1.4e-11 Ha; e_core and the active-space energy ≤ 2.6e-10 Ha; E_RHF 2.2e-12 Ha. Slater–Condon signs: every element of general_element, sigma and the diagonal against an independent bit-loop second-quantized H on random asymmetric integrals (CAS(4,4) to CAS(6,6), odd and even N_α), ≤ 8.9e-15; an exact Jordan–Wigner derivation of the same convention | 3e-9 Ha (CAS-CI energies), 5e-11 Ha (E_RHF), 1e-12 (signs) | validation_casci.rs, gen_casci.py, slater_condon_brute_force.rs, casci_slater_condon_phases_sympy.py |
Point-group-blocked closed-shell RHF (ferric_scf::symmetric::solve_rhf_symmetric with RhfConfig.symmetry, optional irrep_nelec; Python run_rhf(symmetry=True), CLI [scf] symmetry with kind = "rhf"; Abelian subgroup of the molecule's point group; returns a SymmetricRhf whose AO matrices are in the symmetry frame): total energy, per-irrep occupation, and the irrep_nelec excited-state energy | H2O (C2v), NH3 (Cs), C2H4 and benzene (D2h) / STO-3G, 6-31G; H2O with the out-of-plane 1b1 pair moved into a1 (irrep_nelec); H2 / cc-pVDZ at R = 50 and 10000 Å, sigma_g^2 | PySCF RHF with mol.symmetry = True and irrep_nelec, ferric's basis and geometry. D2h irrep labels depend on the detected axes, so D2h occupations are compared as a multiset. Negative control: without symmetry ferric ends not converged and far from sigma_g^2 (-0.71267 and -0.70740 Ha at R = 50 and 10000 Å); the value it stops at depends on the SCF trajectory and the BLAS kernel (PySCF without symmetry, recorded in the references: -0.42718 and -0.37335 Ha, also not converged) | E ≤ 1.0e-11 Ha over all 10 blocked and 2 excited cases; identical per-irrep occupations; blocked vs unblocked energy ≤ 1.1e-13 Ha (STO-3G, same frame) and ≤ 7.8e-14 Ha (H2O, NH3, CH4, N2 / cc-pVDZ, same frame); max |[R,F]| ≤ 6.4e-14; H2 sigma_g^2 (in scf_symmetry.rs, not the ignored PySCF test) matches PySCF to 1.1e-16 (R = 50 Å) and 4.4e-16 Ha (R = 10000 Å) | 1e-8 Ha (PySCF, H2O/NH3/C2H4/benzene and the excited state), 1e-9 Ha (stretched H2 reference energies), 1e-10 Ha (blocked vs unblocked), 1e-10 (commutator) | validation_scf_symmetry.rs, scf_symmetry.rs, gen_scf_symmetry.py |
Point-group-blocked closed-shell RKS (solve_rhf_symmetric with RhfConfig.xc set; Python run_dft/run_ksdft(symmetry=True), CLI [scf] symmetry with kind = "ksdft" or "rhf" + [dft] functional; no Fock or density symmetrisation: the KS grid is built in the symmetry frame, where every Lebedev rule and Becke weight maps onto itself under the detected operations; a near-degenerate HOMO/LUMO pair in different irreps not listed in irrep_nelec is refused, and so is a main grid below 110 angular points, where the grid splits degenerate partners by more than the 1e-3 Ha check): PBE total energy and per-irrep occupation | H2O (C2v) / STO-3G, 6-31G; H2 / cc-pVDZ at R = 50 and 10000 Å, sigma_g^2 and sigma_u^2 (irrep_nelec) | PySCF dft.RKS PBE with mol.symmetry = True on ferric's KS recipe: (75, 110) unpruned grid, Becke partition with Becke radii adjustment, RI-J with def2-universal-jkfit from ferric's JSON. Each code builds its grid in its own symmetry frame; the frames differ by an axis permutation or sign (an Oh operation), so the grids coincide. Negative control: without symmetry ferric RKS ends not converged, more than 1e-3 Ha (in practice about 0.4-0.6 Ha) above sigma_g^2 at -0.91423 Ha; the value depends on the SCF trajectory and the BLAS kernel (PySCF without symmetry, recorded in the references: -0.41244 and -0.40191 Ha, not converged) | E ≤ 3.6e-13 Ha over the 2 H2O and 4 H2 cases; identical per-irrep occupations; blocked vs unblocked energy ≤ 2.3e-13 Ha (H2O, NH3, C2H4, benzene / STO-3G PBE, same frame, RI-J; H2O / 6-31G also with exact J, 1.4e-14; H2O / STO-3G SCAN 5.7e-14 and ωB97X-V 1.4e-14); max |[R,F_KS]| ≤ 5.1e-13 (V_xc included); rotated and translated H2O input vs the unblocked input-frame energy 5.3e-5 Ha (the grid is not rotation invariant; the same-frame anchor holds) | 5e-12 Ha (PySCF), 1e-9 Ha (stretched H2 in scf_symmetry_ks.rs), 1e-10 Ha (blocked vs unblocked), 1e-10 (commutators), 1e-3 Ha (rotated input, documented) | validation_scf_symmetry.rs, scf_symmetry_ks.rs, gen_scf_symmetry.py |
Point-group-blocked open-shell UHF and ROHF (library only: ferric_scf::symmetric_open::solve_uhf_symmetric and solve_rohf_symmetric with RhfConfig.symmetry, optional per-irrep (n_alpha, n_beta) irrep_nelec argument; no Kohn-Sham; ROHF needs rohf_occupation_guard = false; return SymmetricUhf / SymmetricRohf, whose AO matrices are in the symmetry frame): total energy, per-irrep (alpha, beta) occupation, and the irrep_nelec excited-state energy | NH2, CH3 (D3h, blocked in a C2v subgroup), NO2 (C2v) doublets, O2 (D2h) and CH2 (C2v) triplets / STO-3G, 6-31G (NO2 6-31G only); H2O+ with the hole moved from the out-of-plane 1b1 orbital to 3a1 (2A1 instead of 2B1, irrep_nelec); singlet H2 at 2.0 Å / 6-31G as the negative control | PySCF UHF and ROHF with mol.symmetry = True and irrep_nelec ({irrep: (n_alpha, n_beta)}), ferric's basis and geometry. D2h (and CH3, whose D3h has three equivalent C2v subgroups) irrep labels depend on the detected axes, so those occupations are compared as a multiset of (n_alpha, n_beta). Negative control: unblocked UHF on singlet H2 at 2.0 Å lands on a spatially symmetry-broken solution 0.0847 Ha below the symmetric sigma_g^2 state (ferric with scf_stability_descent -1.00093524 Ha, PySCF after following its instability -1.00093524 Ha), which the blocked solver cannot reach by construction (a blocked closed-shell UHF has identical alpha and beta blocks, so it is the restricted determinant; it returns the sigma_g^2 energy, equal to RHF) | E ≤ 3.5e-12 Ha over all 11 UHF and 11 ROHF blocked cases and the 4 excited cases; identical per-irrep (alpha, beta) occupations; blocked vs unblocked energy ≤ 7.1e-14 Ha (STO-3G, same frame, scf_open_symmetry.rs); max |[R,F_alpha]|, |[R,F_beta]| ≤ 3.8e-15; ⟨S²⟩ blocked = unblocked to 1e-8; rotated and translated NH2 and O2 input vs the unblocked input-frame energy ≤ 5.7e-14 Ha (symmetrization shift ≤ 4.2e-16 Bohr) | 1e-8 Ha (PySCF, ground and excited), 1e-8 Ha (H2 negative control energies), 1e-10 Ha (blocked vs unblocked), 1e-9 Ha (rotated input), 1e-10 (commutators) | validation_uhf_symmetry.rs, scf_open_symmetry.rs, gen_uhf_symmetry.py |
| G0W0 quasiparticle energies, HOMO−2 to LUMO+2, @HF and @PBE (and @HF with one frozen core orbital) | H2O, NH3, N2 / cc-pVDZ (cc-pvdz-ri), aug-cc-pVDZ (aug-cc-pvdz-rifit); @PBE: H2O / both bases, N2 / cc-pVDZ | PySCF gw_ac fed ferric's basis and aux, with the same 100-point frequency grid, Padé nodes, full quasiparticle equation and exact-integral SCF; @PBE references apply ferric's XC density floor (ρ ≤ 1e-10) and evaluate the Padé as the standard Thiele fraction, because PySCF's pade_thiele_ndarray applies its last coefficient twice (up to 7.1e-3 Ha at @PBE) | @HF ≤ 1.3e-7 Ha, @PBE ≤ 6.2e-8 Ha; Σc(ef + iω) at the Padé nodes ≤ 8.6e-11 Ha; Σx ≤ 4.9e-9 Ha | 1e-6 Ha (Σc(iω) 1e-9) | validation_gw.rs, gen_gw.py |
| G0W0@HF with ECPs | I2, Xe, Ag2 / aug-cc-pVDZ-PP (def2-tzvp-rifit) | PySCF gw_ac, same basis, inline ECP and aux | ≤ 2.5e-6 Ha (ε_mf 2.9e-6 from the ECP SCF offset) | 2e-5 Ha | ″ |
| U-G0W0@UHF | OH, CH3, NH2 / cc-pVDZ, aug-cc-pVDZ; O2 and CH2 triplets / aug-cc-pVDZ | PySCF ugw_ac on a stability-checked UHF, with ferric's per-spin Fermi level | ≤ 8.7e-7 Ha; OH/cc-pVDZ 7.0e-6 Ha (its π hole makes the UHF marginally stable) | 3e-5 Ha | ″ |
U-G0W0@UKS/PBE (gw with [rpa] xc and multiplicity > 1) | OH, CH3, NH2 / cc-pVDZ (cc-pvdz-ri) | PySCF ugw_ac Σc(ef + iω) on a stability-checked exact-J UKS/PBE with ferric's per-spin XC density floor and per-spin Fermi level; standard Thiele continuation and the quasiparticle equation solved in numpy with the density-fitted Σx, because PySCF's ugw_ac vhf_df exchange has the wrong sign | Σc(ef + iω) ≤8.7e-9 Ha, v_xc ≤7.1e-10 Ha, quasiparticle energies ≤2.6e-8 Ha on orbitals whose quasiparticle equation has one root (NH2's α HOMO and HOMO−1 have three within 1 Ha and are not compared); ferric solves each spin's quasiparticle equation with Σx − v_xc inside it, as the reference does; adding the shift after an unshifted solve lands 0.38–0.96 eV away, and the test requires ferric to miss that root | Σc(ef + iω) 1e-7 Ha; v_xc 1e-8 Ha; quasiparticle energies 3e-7 Ha | validation_gw.rs, gen_gw.py |
| COHSEX (@HF and @PBE), evGW₀, evGW (@HF) | COHSEX: H2O, N2 / cc-pVDZ; evGW₀, evGW: H2O / cc-pVDZ | numpy COHSEX on PySCF's density-fitted integrals; @PBE on the exact-J RKS/PBE orbitals with ferric's XC density floor, plus the static Σx − v_xc from PySCF's density-fitted exchange and v_xc; anchored to U-COHSEX on the same RKS orbitals as UKS (1.1e-15 Ha); evGW₀ and evGW by iterating PySCF's get_sigma with ferric's update rule | COHSEX@HF 9.4e-10 Ha; COHSEX@PBE 1.2e-9 Ha; evGW₀/evGW 5.7e-7 Ha | 1e-8 / 1e-8 / 5e-6 Ha | ″ |
| U-COHSEX@UHF (static, HF reference, spin-summed W) | OH, CH3, NH2 / cc-pVDZ (cc-pvdz-ri), aug-cc-pVDZ (aug-cc-pvdz-rifit); O2 and CH2 triplets / aug-cc-pVDZ; H2O / cc-pVDZ singlet UHF against the closed-shell COHSEX | numpy U-COHSEX on PySCF's density-fitted integrals with ferric's aux, W from ε = I + Π_α + Π_β, on the stability-checked UHF of the U-G0W0 row; the singlet UHF numpy reproduces the closed-shell COHSEX reference to 2.1e-9 Ha | ε_qp ≤ 3.3e-9 Ha (open shell); the singlet UHF through run_u_gw equals closed-shell COHSEX to 8.1e-10 | 3e-8 Ha (anchor 1e-8) | validation_u_cohsex.rs, gen_u_cohsex.py, reference data |
| U-RPA, U-G0W0 and U-RI-MP2 (doubles only) on a ROHF reference, semi-canonicalized per spin | OH, CH3 doublets / cc-pVDZ (cc-pvdz-ri) | PySCF URPA, ugw_ac and a numpy UMP2 (checked against DFUMP2) on the ROHF orbitals semi-canonicalized in numpy (each spin's Fock diagonalized in its occupied and virtual blocks); the ROMP2 singles term is not included on either side | U-RPA E_c ≤ 7.3e-12 Ha, U-MP2 E_c ≤ 8.7e-12, U-G0W0 QP ≤ 1.5e-5 (OH β) and ≤ 1.3e-6 (CH3); semicanonical orbital energies ≤ 6.4e-10 | 1e-10 Ha (E_c), 5e-5 Ha (QP) | validation_rohf_reference.rs, gen_rohf_semicanonical.py, reference data |
| BSE-TDA singlet excitation energies and oscillator strengths (G0W0@HF, static W, frozen core 0) | H2O / cc-pVDZ (cc-pvdz-ri), aug-cc-pVDZ (aug-cc-pvdz-rifit); NH3, CH2O / cc-pVDZ; lowest five singlets | numpy BSE-TDA on PySCF density-fitted integrals with ferric's aux, W from the static RPA dielectric on HF energies; the kernel is diagonalized on ferric's own quasiparticle energies, because G0W0 energies of core and high virtual orbitals move by up to 0.29 Ha under a 1e-10 relative change of Σc(iω). The lowest five Ω are insensitive to that: two independent generator runs move individual far-from-Fermi quasiparticle energies by up to 3.8e-3 Ha and the lowest five Ω by at most 1.1e-7 Ha, because those states carry ≤ 7e-3 of their norm on the affected rows. Replacing the out-of-window quasiparticle energies by a rigid scissor outside HOMO−k…LUMO+k instead moves the lowest five Ω by 1.5e-4 to 9.4e-3 Ha (k = 2) and 1.4e-5 to 2.0e-4 Ha (k = 8), so every quasiparticle energy is solved and the poorly-determined ones are reported in BseResult::qp_suspect_mos rather than replaced; CIS limit (W replaced by the bare Coulomb interaction, HF energies) against the same numpy build and PySCF tdscf.TDA | Ω ≤ 1.8e-10 Ha and oscillator strengths ≤ 2.6e-9 against the kernel on ferric's QP energies; ≤ 2.2e-7 Ha with each side's own QP energies; CIS anchor ≤ 1.6e-9 Ha vs numpy DF-CIS | 2e-9 Ha (Ω), 3e-8 (f), 2e-6 Ha (own QP) | validation_bse.rs, gen_bse.py |
RPAx static polarizability (run_rpax_static_polarizability, statically screened W(0) from full-rank PDEP, frozen core 0) | H2O, NH3 / aug-cc-pVDZ (aug-cc-pvdz-rifit) at RHF, scissor 0; H2O / cc-pVDZ (cc-pvdz-ri) at PBE (exact J, (75,110) grid), scissor 0.36 Ha; full 3×3 tensor | numpy assembly of the same A±B kernel on PySCF density-fitted integrals with ferric's aux, W(0) = Lᵀ(I + Π(0))⁻¹L by a direct inverse on the unshifted mean-field energies. The numpy bare-exchange limit (W replaced by the bare Coulomb interaction) equals PySCF's CPHF α from its own response operator to ≤ 1.9e-14 relative, both density-fitted and with exact integrals. ferric's bare-exchange limit is compared with the same numpy build; against exact-integral CPHF it differs by the density-fitting error of the aux (9.0e-5 H2O, 1.5e-5 NH3). The lowest eigenvalues of A+B and A−B are compared too, because the static α does not depend on A−B | α ≤ 5.3e-9 relative (worst element over max |α|), α_iso ≤ 2.8e-9; bare limit ≤ 5.4e-9; lowest A±B eigenvalues ≤ 7.5e-10 Ha | 5e-8 (α), 1e-8 Ha (eigenvalues) | validation_rpax_alpha.rs, gen_rpax_alpha.py |
| TDA and Casida TDDFT excitation energies | water, formaldehyde, NH3 / 6-31G, aug-cc-pVDZ; r2SCAN (τ kernel): water / 6-31G on the flat 75 × 110 grid | PySCF tddft.TDA/TDDFT, same RI and grid; r2SCAN: PySCF get_ab | HF ≤ 2e-6 eV; LDA/PBE ≤ 1.2e-5 eV; B3LYP ≤ 8.1e-4 eV on the default pruned grid, ≤ 2.8e-6 eV (water, formaldehyde / aug-cc-pVDZ) on the reference's flat grid; r2SCAN 2.1e-6 eV (TDA and Casida). PySCF fed ferric's grid coordinates and weights reproduces ferric's r2SCAN roots to 6e-8 eV and E_SCF to 1e-12 Ha on the flat and the default grid | 1e-3 eV; same grid 2e-5 eV | validation_tddft.rs, gen_tddft_refs.py |
| RI-MP2 analytic gradient (exact-J/K RHF), all electrons and frozen core | distorted H2O, bent HCN / cc-pVDZ (cc-pvdz-ri); distorted NH3 / def2-SVP (def2-svp-rifit); frozen core: the same three, freezing the cores ORCA freezes | ORCA RI-MP2 NoRI NoFrozenCore EnGrad and frozen-core RI-MP2 EnGrad, with the same /C aux; PySCF DFMP2 5-point finite differences (all electrons) | ≤ 2.1e-9 Ha/Bohr (PySCF FD); ≤ 5.1e-8 Ha/Bohr (ORCA, its own floor); energies ≤ 1.3e-10 Ha. Frozen core: 4.9e-8 (H2O), 1.3e-8 (HCN), 1.7e-8 (NH3) Ha/Bohr vs ORCA, E_corr < 5e-11 Ha; H2O / STO-3G with O 1s frozen 2.0e-9 Ha/Bohr vs FD of ferric's own energy; without the core–active response block the H2O / cc-pVDZ gradient misses FD by 9.8e-5 | 2e-8 Ha/Bohr (FD), 2.5e-7 Ha/Bohr (ORCA); 1e-9 Ha; 1e-6 Ha/Bohr (own FD) | validation_rimp2_gradient.rs, gen_rimp2_gradient.py |
RI-MP2 relaxed dipole and finite-field relaxed polarizability (library: cpks_polar::static_relaxed_density_ao, ff_polar; all electrons) | water / STO-3G (pinned) and cc-pVDZ (measured, not pinned); aux cc-pvdz-ri | PySCF finite-field DF-MP2 energy with the same aux basis (scripts/cpks/ff_dfmp2_dipole_alpha.py) | relaxed μ_z 3e-8 a.u. (STO-3G), 2e-8 (cc-pVDZ); ff_polar relaxed α diagonal 3e-6 a.u. (STO-3G), α_zz 1.4e-5 (cc-pVDZ).; the analytic relaxed α (mp2_polarizability_analytic) is the next row | 1e-6 a.u. (μ); 1e-4 a.u. (α) | cpks_polar.rs, ff_polar.rs, ff_dfmp2_dipole_alpha.py |
Relaxed RI-MP2 static polarizability, analytic (mp2_polarizability_analytic → analytic_alpha_relaxed; library only, closed shell, no frozen core): dipole CPHF, first-order Fock response, central difference (h = 1e-4) of the relaxed density along the first-order field reference. Exactness anchor: the same difference of the SCF density equals the CPHF HF α to 4e-9 rel. ff_polar (finite field, re-converged SCF) is a cross-check. Attenuated correlation (Coulomb RHF, erfc(ωr) MP2) matches a 5-point finite field of that energy | H2O / STO-3G, cc-pVDZ; NH3 in a generic orientation (all 9 elements) / cc-pVDZ; aux cc-pvdz-ri, exact-J/K RHF | PySCF 2.13 finite-field −d²E/dF² of RHF + DF-MP2, same aux basis (5-point diagonal, 16-point mixed) | ≤ 2.2e-6 rel | 1e-5 × max(|α_ref|, α_iso) | cpks_polar.rs (relaxed_alpha_matches_pyscf_energy_hessian), ff_dfmp2_dipole_alpha.py |
IEF-PCM analytic nuclear gradient (ferric_pcm::pcm_gradient, threaded into the RHF/UHF/ROHF/KS gradients) | water, C1 methanol, NH2 / STO-3G, ε = 78.4 (RHF, RKS-SVWN, UHF, ROHF; point and Gaussian probes; Lebedev 50, 110, 302) | central FD of pcm_step's energy at fixed density and of the solvated SCF energy; PySCF 2.13 IEF-PCM nuc_grad_method() on ferric's 302-point cavity (= PySCF's gen_surface: tessera count, area, centroid to 1e-10); rigid-rotation FD for the torque | PySCF: 4.2e-11 (water), 7.2e-11 (methanol) Ha/Bohr; FD: ≤ 2.0e-7 (fixed density), ≤ 2.9e-7 (SCF) Ha/Bohr; ε = 1 + 2e-9 vs vacuum 3.3e-12; Σ_A g = 0 per part; torque 1.36e-4 vs FD to 8e-9; nine single-term mutants (switching, area, S/D diagonal and off-diagonal, tessera-site, nuclear-tessera terms) each fail the FD tests | 1e-6 Ha/Bohr (PySCF); FD: |FD(2h) − FD(h)| plus a round-off / SCF-convergence floor | pcm_gradient.rs (ferric-pcm), pcm_gradient.rs (ferric-scf), gen_pcm_grad.py |
| COSMO solvation (RHF, UHF; energy only, no gradient: gradient entries refuse it) | H2O, NH3, CH3OH, acetate(−) / STO-3G, cc-pVDZ × ε 4.7, 78.4; HO2 (UHF) / cc-pVDZ | PySCF pcm.py COSMO driver with ferric's radii, 110-point cavity and point-charge potential (ferric's model); stock PySCF COSMO for the formulation gap | solvated energy ≤ 1.9e-11 Ha, E_cosmo ≤ 9.7e-11 Ha (ferric's model); 0.05–0.51% from stock PySCF COSMO (point vs Gaussian surface charges) | 1e-10 Ha; 1e-9 Ha; 2% | validation_cosmo.rs, gen_cosmo.py |
| Attenuated RI-MP2 (erfc on 3-center and metric) | H2O, NH3 / aug-cc-pVDZ + aug-cc-pVDZ-RIFIT, ω 0.2, 0.222, 0.42, 1.0 Bohr⁻¹ | numpy RI-MP2 on PySCF int3c2e/int2c2e under with_range_coulomb(-ω) | E_corr ≤ 5.9e-12 Ha at every ω; ω → 0 reproduces Coulomb RI-MP2 to 1.6e-15 | 1e-10 Ha | validation_attenuated_mp2.rs, gen_attenuated_mp2.py |
MP2-V (att_mp2_vv10): damped VV10 half, terfc-attenuated MP2 half and erfc control | H2O / cc-pVDZ, aug-cc-pVDZ; NH3 / cc-pVDZ; water dimer (S22) / aug-cc-pVDZ (VV10 and erfc control only; the terfc reference has no dimer); r₀ = 1.00 Å (b 11.0) and 0.85 Å (b 8.0), C = 0.0089, ferric's 50 × 50 NLC grid, PySCF's RHF density | PySCF numint._vv10nlc (undamped); numpy VV10 from Vydrov & Van Voorhis 2010 with the 1 − terfc(R, r₀)² damping (damped); numpy RI-MP2 on PySCF with_range_coulomb(-ω) integrals, ω = 1/(r₀√2), frozen core (erfc control); numpy RI-MP2 on Fourier-space terfc integrals (PySCF ft_ao plus k-space quadrature; the SCS-MP2(2terfc) reference, reused, r₀ = 1.00 Å, frozen core 1; its terfc definition is taken from ferric's shim.cc, so only the r₀ → 0 and ∞ limits constrain ω = 1/(r₀√2)) (terfc) | E_nl ≤ 3.1e-16 Ha undamped, damped and at the r₀ → 0 self-pair limit; terfc E_c, E_OS, E_SS ≤ 2.5e-12 Ha; erfc E_c ≤ 7.0e-12 Ha; E_nl on ferric's own density ≤ 1.12e-13 Ha; ∫ρ on the NLC grid ≤ 1.10e-13; a collapsed UHF singlet reproduces them (terfc E_c ≤ 9e-12 Ha). Controls: the erfc and Coulomb operators miss the terfc E_c by ≥ 1.0e-3 Ha (the test requires only ≥ 1e-8 Ha); mutants: erfc for terfc, r₀ in Å, Coulomb fitting metric | 5e-15 Ha (VV10); 1e-12 electrons (∫ρ on the NLC grid); 1e-12 Ha (E_nl on own density); 3e-11 Ha (terfc E_c); 1e-10 Ha (erfc E_c, E_HF) | validation_mp2_v.rs, gen_mp2_v.py, gen_terfc_integrals.py |
SCS-MP2(2terfc): terf/terfc 3-index (P|μν) and 2-index (P|Q) integrals, element-wise by shell class (s through f), and E_OS, E_SS (r₀ = 0.75, 1.00, 1.05 Å) with the Eq. 12 total (scs_mp2_2terfc, published defaults) | H2O, NH3 / cc-pVDZ + cc-pVDZ-RI; H2O / aug-cc-pVDZ + aug-cc-pVDZ-RIFIT; frozen core 0 and 1 | Fourier-space terf, (2π)⁻³∫d³k (4π/k²)e^(−k²/4w²)cos(kr₀) Re[χ̂_P* ρ̂_μν], on PySCF ft_ao/ft_aopair and a Gauss–Legendre × Lebedev k-grid; terfc = PySCF analytic Coulomb − terf; numpy RI-MP2 with the terfc metric on PySCF exact-J/K RHF orbitals. Shares none of ferric's McMurchie–Davidson pass or tables. The same k-grid with the erf kernel reproduces PySCF with_range_coulomb(w) integrals to ≤ 3.4e-12, and the s-type terf/terfc elements match a 1-D real-space Gaussian-blob quadrature (no Fourier transform) to ≤ 3.5e-12; the generator refuses to write above 5e-11 | 3-index ≤ 1.4e-13 and 2-index ≤ 2.6e-12 in every shell class; E_OS, E_SS and the Eq. 12 correlation energy ≤ 3.3e-12 Ha; terfc at r₀ = 0.75 Å misses the 1.05 Å reference by 5.2e-3 Ha or more, and erfc at the same curvature misses terfc by 4.1e-2 Ha or more. the published parameters (aug-cc-pVTZ, frozen core, no counterpoise) are exercised only on an S66 subset (n=5, H-bonded/mixed, S66x8 geometry; no external anchor for the counterpoise path) | 2e-12 (3-index), 3e-11 (2-index); 5e-11 Ha | validation_terfc_integrals.rs, validation_scs_mp2_2terfc.rs, gen_terfc_integrals.py, reference data |
| QM/MM electrostatic embedding (RHF): energy, embedding shift, QM gradient, MM forces | H2O + 10 charges / cc-pVDZ, aug-cc-pVDZ; CH3OH + 501 TIP3P charges / 6-31G | PySCF qmmm.mm_charge | energy and shift ≤ 4.8e-12 Ha; QM gradient ≤ 1.2e-10, MM forces ≤ 4.4e-12 Ha/Bohr (up to 501 charges) | 5e-11 Ha; 1e-9, 5e-11 Ha/Bohr | validation_qmmm.rs, gen_qmmm.py |
Gaussian-smeared MM charges (width in Bohr, ζ = 1/width², same as PySCF radii with unit="Bohr") | H2O + 10 charges / cc-pVDZ, widths 0.5, 1, 2 Bohr | PySCF qmmm.mm_charge(radii=) | energy ≤ 4.1e-12 Ha; QM gradient ≤ 1.2e-10 Ha/Bohr; width → 0 reproduces point charges to 3.0e-12 Ha | 5e-11 Ha; 1e-9 Ha/Bohr | ″ |
| KS QM/MM (exact J/K, matched (75,110) grid; gradient with grid response) | CH3OH + 20 charges / def2-SVP, B3LYP and PBE | PySCF qmmm.mm_charge on dft.RKS, grid_response=True | energy ≤ 4.1e-12 Ha; QM gradient ≤ 2.9e-9, MM forces ≤ 1.8e-10 Ha/Bohr | 1e-10 Ha; 3e-8, 2e-9 Ha/Bohr | ″ |
| Thole polarizable embedding (induced point dipoles, exponential Thole damping a = 2.1304): energy, E_pol, induced dipoles, QM gradient, MM rows | H2O + 4 TIP3P waters (α O 0.837, H 0.496 ų; with and without intramolecular exclusions) / cc-pVDZ, RHF; NH2 + 3 waters / cc-pVDZ, UHF; anchors: one site 30 Å away (classical limit −½α|E0|²), α → 0 (PySCF qmmm.mm_charge) | numpy induction model on PySCF point-dipole field integrals and qmmm.mm_charge, self-consistent in the SCF; gradients by 5-point finite difference | total energy ≤ 3.7e-12 Ha, E_pol ≤ 1.5e-9, induced dipoles ≤ 1.5e-9 a.u., QM and MM gradients ≤ 3.0e-9 Ha/Bohr; far-site E_pol within 7.7e-8 (relative) of −½α|E0|² | 5e-11 Ha (total), 1e-8 (E_pol, dipoles), 3e-8 Ha/Bohr | validation_thole.rs, gen_thole.py |
QM/MM geometry optimization (optimize_qmmm): stationarity of the returned geometry and the full-structure gradient (link (1−g)/g fold, RC/RCD midpoint ½/½ fold, MM force field) | Capped ethane (QM CH3, link at the C–C cut) RHF × Keep, delete-host (Z1), RC / STO-3G, realistic MM LJ (C σ 3.40 Å, H 2.65 Å; every QM–MM pair excluded or 1-4 scaled), Z1 also 6-31G, move_mm all; ethyl radical (QM CH2•, capped CH3• doublet) UHF × Z1, RC, all; RKS/PBE and UKS/PBE (DF-J/K def2-universal-jkfit) × RCD × ("within", r) with the M2 hydrogens frozen; capped ethane + 2 TIP3P-charged waters selected by whole residue × RCD × ("residues", [...]) | None exists: no other code defines this energy surface (partition, link scale and boundary scheme are not standardized). Internal two-path check: central finite difference of the energy the optimizer minimizes, partition rebuilt at each displacement, against the analytic gradient. Both paths share the SCF, integrals, DF fit and grid; partition-construction errors are covered by charge-bookkeeping assertions and scheme-swap controls instead. With no MM atoms, each of RHF/UHF/RKS/UKS reproduces the plain optimizer bit for bit | Free rows at the returned geometry ≤ 4.4e-6 Ha/Bohr (frozen rows ≥ 3e-3, as they must be); FD vs analytic ≤ 1.8e-8 Ha/Bohr at the minimum and ≤ 5.8e-7 relative off it (PBE ≤ 8.4e-8). Gradients of the wrong scheme, of the field switched off, or with an MM-row fold broken miss by ≥ 3.5e-3 relative; Keep (host charge 0.44 Å from the link H, q = −0.1) converges in 13 steps. The residue system has no bound minimum with its generic force field, so only its free-set behaviour and off-minimum gradient are checked | per case, ~10× measured: stationarity 9.6e-6 to 4.4e-5 Ha/Bohr; FD 1.1e-7 to 1.8e-7 Ha/Bohr (minimum), 5.3e-7 to 5.8e-6 relative (off) | validation_optimize_qmmm.rs, qmmm_gradient_at_minimum.rs |
| MM force field (ferric-mm): bond, angle, torsion, Coulomb and LJ energies and gradients, exclusion and 1-4 pair sets | ALA-ALA, ACE-PHE-NME, TRP-PRO-ASP (charge −1), ACE-PHE-NME + 3 TIP3P waters, at the relaxed geometry and two random displacements; one extra case with every torsion phase shifted by 37° | OpenMM Reference platform (double precision), ff14SB and flexible TIP3P, no cutoff (the default; PME / Ewald and periodic images are not implemented); Coulomb and LJ split by evaluating copies of the nonbonded force with epsilons or charges zeroed | energies ≤ 1.75e-12 relative, gradients ≤ 8.5e-11 relative (after OpenMM's Coulomb constant, which differs from ferric's by 6.6e-11 relative); pair sets identical. Near φ = 0 and π and near-linear valence angles the torsion gradient matches central FD within a tolerance derived from the step (truncation + round-off); an exactly linear angle (and a torsion through a collinear angle) gives a finite, exactly zero gradient | 1e-11 (energy), 1e-9 (gradient), relative; FD: the derived step tolerance | validation_mm.rs, gen_mm.py, terms_fd.rs, mm_dihedral_gradient_sympy.py |
MM nonbonded cutoff (ferric-mm, opt-in NonbondedMethod::CutoffNonPeriodic, Rust library only): reaction-field Coulomb, LJ truncated or switched, 1-4 pairs uncut, cell list | Ethanol + 12 TIP3P-charged waters (45 atoms, diameter 14.2 Å): cutoff 5 Å with ε_rf 78.3, 78.3 + LJ switch from 4 Å, and 1; cutoff 2.9 Å with a switch from 2.2 Å (two 1-4 pairs beyond the cutoff) | OpenMM NonbondedForce CutoffNonPeriodic, Reference platform, charges rescaled to OpenMM's Coulomb constant; anchors: cutoff beyond the diameter with ε_rf = 1 equals the all-pairs sum bit for bit (LJ energy, gradient) and Coulomb minus exactly c_rf Σ q_i q_j; the default all-pairs path is unchanged bit for bit; cell list equals the brute-force pair set | energy ≤ 2.4e-14, gradient ≤ 2.4e-15 relative; central FD with pairs inside, in the switching region and beyond the cutoff ≤ 1.8e-11 Ha/Bohr | 1e-12 relative (energy, gradient); FD 1e-10 Ha/Bohr | cutoff.rs, gen_openmm_mm_cutoff_refs.py, mm_cutoff_sympy.py |
QM–MM Lennard-Jones across a covalent cut (qmmm_mm_terms): 1-2/1-3 QM–MM pairs excluded, 1-4 pairs scaled by the topology's scale_lj_14, both from the full bond graph | Ethanol cut at C–C (realistic LJ, C σ 3.4 Å), QM = CH2OH and QM = CH3 at 1-4 LJ scale 0.5, QM = CH2OH at 0.75; every pair class (1-2, 1-3, 1-4, 1-5) present across the cut | OpenMM Reference platform, exceptions from OpenMM's own createExceptionsFromBonds, charges zero; QM–MM part isolated as E(all) − E(QM ε = 0) − E(MM ε = 0). With no bond path between the regions the result is bit-identical to the every-pair sum | energy ≤ 8.7e-19 Ha, gradient ≤ 2.0e-18 Ha/Bohr; FD of the MM terms across the cut ≤ 1.5e-11 Ha/Bohr. The every-pair sum misses OpenMM by 6839 kcal/mol | 1e-14 Ha, 1e-14 Ha/Bohr; FD 1.5e-10 Ha/Bohr | qmmm_lj_vs_openmm.rs, gen_openmm_qmmm_lj_refs.py |
| External potential in the RI-MP2 analytic gradient | H2O + 10 charges / cc-pVDZ; CH3OH + 20 charges / 6-31G; aux cc-pVDZ-RI | 5-point finite difference of PySCF DFMP2 on qmmm.mm_charge RHF, same aux | energies ≤ 8.4e-12 Ha; RI-MP2 gradient ≤ 4.8e-9 Ha/Bohr vs PySCF FD | 1e-10 Ha; 3e-8 Ha/Bohr | validation_qmmm_mp2_gradient.rs, gen_qmmm.py |
| κ-regularized RI-MP2 (κ = 0.5, 1.1, 2.0 Eh⁻¹; opposite- and same-spin parts) | H2O, CH4 / cc-pVDZ (cc-pvdz-ri) | numpy (1 − exp(−κΔ))² sum on PySCF density-fitted integrals with the same aux basis; κ → ∞ checked against PySCF DFMP2 | E_corr (OS, SS, total) ≤ 3.3e-12 Ha at κ = 0.5, 1.1, 2.0; κ → ∞ equals RI-MP2 exactly | 3e-11 Ha | validation_kappa_mp2.rs, gen_kappa_mp2.py |
AO-Laplace RI-MP2 (compute_mo and compute_ao, n_quad 3/5/7, all electrons) | H2O / cc-pVDZ and aug-cc-pVDZ, CH4, s-trans-1,3-butadiene, n-octane / cc-pVDZ (cc-pvdz-ri, aug-cc-pvdz-rifit) | Two numpy references on PySCF density-fitted integrals with the same aux basis: the dense (i,a,j,b) sum with the exact 1/Δ (equal to PySCF mp.dfmp2.DFMP2 to 6.7e-16), and the same sum with 1/Δ → Σ_k w_k e^(−t_k Δ) over the identical minimax nodes parsed from minimax.rs, which isolates quadrature error from implementation error | MO ≡ AO ≤ 3.4e-13 Ha; vs the quadrature-isolated reference (implementation error) ≤ 3.1e-11 Ha; vs exact DF-MP2 at n_quad = 7 (quadrature error) 8.0e-9 (CH4, R = 19.0), 4.0e-8 (octane, R = 24.5), 1.5e-7 (butadiene, R = 33.7), 8.3e-8 (H2O, R = 36.4), 2.2e-7 (H2O/aug, R = 45.7) — the error tracks R, and no case approaches the k = 7 table limit R ≤ 1000 | 1e-10 Ha (MO ≡ AO); 1e-9 Ha (vs quadrature reference); 2e-6 Ha (vs exact DF-MP2) | validation_laplace_mp2.rs, gen_laplace_mp2.py, reference data, validation_laplace_energy_is_thread_independent.rs |
Local RI-MP2 (rimp2 with [local] scheme = "amplitude-threshold"), in-core (global and per-pair domain fit) and integral_direct, at ε = 0 with every locality map at its no-op limit; finite ε as a measured error map | H2O, n-butane / cc-pVDZ, 6-31G (cc-pvdz-ri), frozen core 0 and one per heavy atom; finite ε on n-butane / cc-pVDZ | PySCF DFMP2 with the same aux basis (canonical orbitals, closed-form denominators), checked against a dense numpy sum to 1e-12. ORCA 6.1.1 DLPNO-MP2 (NormalPNO, TightPNO) on n-butane and n-octane / cc-pVDZ is recorded as a ballpark only: it truncates by PNO occupation, not by an amplitude threshold, so the two recoveries are not compared | E_corr ≤ 1.38e-11 Ha at ε = 0 (24 comparisons), at the default PCG stop of relres 1e-6. At ε = 1e-5, 1e-4, 1e-3 (n-butane, all electrons) ferric recovers 99.98%, 99.67%, 95.0% of DF-MP2 in-core and 99.98%, 99.65%, 95.0% integral-direct with its production maps; ORCA DLPNO-MP2 recovers 99.97% (NormalPNO) and 99.99% (TightPNO) of its own RI-MP2 | 1e-10 Ha (ε = 0); finite ε: under-correlation that grows with ε, no tolerance | validation_lmp2_amplitude.rs, gen_lmp2_amplitude.py |
DLPNO-MP2, pair-driven library path (dlpno_mp2_from_b_ov; library only, canonical occupieds). Untruncated (DlpnoConfig::exact()) against PySCF, then a measured truncation sweep over t_cut_pno and t_cut_pairs | n-butane, n-hexane, n-octane / cc-pVDZ (cc-pvdz-ri), frozen core 0 and one per carbon | PySCF DFMP2 with the same aux basis (checked against a dense numpy sum to 1e-12) for the untruncated number. ORCA 6.1.1 RI-MP2 cross-checks PySCF to 4.0e-8 Ha. ORCA DLPNO-MP2 (Normal/TightPNO) and, for n-butane only, DLPNO-CCSD(T) against canonical CCSD(T) are recorded as context | Untruncated DLPNO and canonical RI-MP2 agree with PySCF to 3.6e-11 Ha (12 comparisons). Recovery of ferric's own canonical RI-MP2 (all electrons, C4 / C6 / C8): 99.98 / 99.95 / 99.92% at t_cut_pno = 1e-8, 99.75 / 99.48 / 99.10% at 1e-7, 97.2 / 95.3 / 93.4% at 1e-6, monotone in both thresholds and never above 100%. ORCA DLPNO-MP2 recovers 99.975 / 99.965 / 99.956% (NormalPNO) and 99.988 / 99.983 / 99.980% (TightPNO) of ORCA RI-MP2. ORCA DLPNO-CCSD(T) on n-butane recovers 99.87% (Normal) and 99.95% (Tight) of canonical CCSD(T); that canonical CCSD(T) uses exact integrals while DLPNO uses RI, so those percentages include the RI error. Different localization, different thresholds: the two sets of percentages are not a ranking, and the all-electron columns are lopsided, because ORCA applies a separate TCutPNO (core) of 1e-10 (against 1e-8 for valence at NormalPNO, observed for n-butane) while ferric applies one flat t_cut_pno to every pair (n-butane at 1e-8: 99.9957% frozen-core against 99.977% all-electron; compare the frozen-core rows in the JSON for the cleaner side-by-side). The pair screen t_cut_pairs is uninformative at C4–C8: all-electron it removes only core pairs (at 1e-4, 13 / 21 / 30% of pairs for C4 / C6 / C8, costing 0.22 / 0.44 / 0.82% of E_corr), frozen-core it keeps every valence pair. With ferric's own Boys-localized occupieds the same screen drops pairs (next row); ORCA's pair prescreen is a different criterion and was not re-run. No speedup is claimed and ferric has no DLPNO-CCSD(T) driver | 4e-10 Ha (untruncated vs PySCF); ORCA RI-MP2 vs PySCF 4e-7; truncated runs: monotone, one-sided, and the t_cut_pno = 1e-6 run must miss the reference by more than the anchor bar with virtual retention below 1 | validation_dlpno_mp2.rs, gen_dlpno.py |
DLPNO-MP2 pair screen on Boys-localized occupieds (dlpno_mp2_localized_from_b_ov; library only, closed shell, pair screening only: full canonical virtuals, no PNO/PAO truncation). Occupieds localized block by block (core, valence), Fock-coupled Jacobi amplitude solve over the retained pairs, pair retention and recovered E_corr against ferric's canonical RI-MP2 at t_cut_pairs = 1e-3 to 1e-6 | water, n-butane, n-hexane, n-octane / cc-pVDZ (cc-pvdz-ri), frozen core 0 and one per carbon | ferric's own canonical RI-MP2 (the energy is invariant to the occupied rotation, so the untruncated localized solve must reproduce it); a numpy prototype (proto_dlpno_loc_occ.py) of the formulation exists but its output is not committed and it compares only against its own canonical sum. No ORCA comparison | Untruncated localized solve == canonical RI-MP2 to 3.2e-12 Ha (water), 1.9e-12 / 3.9e-12 / 5.9e-12 Ha (C4 / C6 / C8); the residual is the Jacobi tolerance (2.8e-15 at a 1e3 tighter threshold). Pair retention at t_cut_pairs = 1e-4 (all electron, C4 / C6 / C8): 0.667 / 0.511 / 0.410, recovering 99.90 / 99.81 / 99.76% of E_corr (errors 0.58 / 1.7 / 2.8 mHa, one-sided); frozen core: 0.901 / 0.716 / 0.585, recovering 99.93 / 99.84 / 99.80%. At t_cut_pairs = 1e-5 (C4 / C6 / C8): all-electron retention 0.725 / 0.600 / 0.494 with errors 0.19 / 0.50 / 0.88 mHa; frozen-core retention 1.000 / 0.868 / 0.729 with errors 0 / 0.18 / 0.43 mHa. The C8 all-electron error at 1e-4 is 2.8 mHa (~1.8 kcal/mol), so 1e-4 is not chemical accuracy; 1e-5 is the more useful threshold. The screen is informative with localized occupieds, unlike the canonical row above. C4-C8 are below the locality onset (~30 Bohr), so these retentions describe these chains only. Opt-in add-back (add_back_truncation: the screened pairs' diagonal-f estimate is added to the result; cost one (ia|jb) block per discarded pair, at most one extra screen pass) cuts the all-electron error at every threshold measured: 1e-4 gives 0.58 / 1.68 / 2.80 mHa without and 0.12 / 0.40 / 0.68 mHa with (C4 / C6 / C8), 1e-5 gives 0.19 / 0.50 / 0.88 mHa without and 0.007 / 0.058 / 0.131 mHa with; gains 4.1x to 26x. There was no overshoot in the nine measured runs (C4–C8, cc-pVDZ, all electron, 1e-3 to 1e-5); the estimate is not a bound, so this is not a general guarantee. At 1e-3 the corrected error is still 5.4 / 9.4 / 13.3 mHa, so 1e-3 stays unusable. Frozen core was not measured with add-back. It is bit-identical when no pair is screened. The Jacobi conv_thresh default 1e-10 was checked: 1e-9 saves 3 of 23 iterations and moves the energy by at most 5.9e-8 mHa, so the default is unchanged. Mutation ledger in dlpno_localized_occupied.rs. No speedup, no PNO-level result, no ORCA ranking | 1e-10 Ha (anchor, ~17x the largest measured floor); screened runs one-sided, monotone in t_cut_pairs, and above the anchor bar whenever pairs are dropped; retention at 1e-4 below 1 for C6 and C8 and falling with chain length; add-back gain at least 2x with no overshoot | validation_dlpno_localized.rs, dlpno_localized_occupied.rs |
| LinLCCD(hh) correlation energy | H2O, NH3 (RHF); OH (UHF, stability-checked) / 6-31G, cc-pVDZ (cc-pvdz-ri) | numpy exact linear solve of the hole–hole ladder equations on PySCF density-fitted integrals; ladder off checked against PySCF DFMP2/DFUMP2 | LinLCCD(hh) ≤ 4.4e-13 Ha closed shell, 1.2e-12 Ha OH UHF; ladder off equals RI-MP2 to 1.1e-16 | 1e-11 Ha | validation_linlccd.rs, gen_linlccd.py |
Amplitude-threshold LinLCCD at ε = 0, all three variants (drivers-only, hh, full), global and integral-direct paths, plus exact canonical LinLCCD of each variant | H2O, NH3 (RHF) / 6-31G, cc-pVDZ (cc-pvdz-ri) | numpy exact linear solves on PySCF density-fitted integrals: hh in the occupied-pair eigenbasis, full (hh + pp ladders) as a Sylvester equation in the joint eigenbasis, each in spin orbitals and spin-adapted (agree ≤ 1.1e-16); no ladder checked against PySCF DFMP2 | local ≤ 4.4e-13 Ha (drivers-only), 2.8e-13 (hh), 2.0e-13 (full); integral-direct ≤ 3.0e-13; exact canonical ≤ 4.4e-13; at finite ε (NH3/cc-pVDZ, hh) the error is +1.6e-13 at ε = 1e-6, +2.7e-9 at 1e-5, +1.07e-5 Ha at 1e-4 | 5e-12, 3e-12, 2e-12 Ha; 3e-12; 5e-12 | validation_linlccd_amplitude.rs, gen_linlccd.py |
| ωB97X-L-V components: E_KS (libxc ωB97X-V form with the paper's 18 parameters, VV10 b = 10, C = 0.01), λ·E_c = LinLCCD(hh) under λ·erfc(ωr)/r (λ = 0.6, ω = 0.1), total; Be₂ bond energy E(10 000 Å) − E(r_e) | H2O (RKS, exact J and RI-J), OH (ROKS → semicanonical → unrestricted) / def2-SVP (def2-svp-rifit, def2-universal-jkfit); Be₂ / def2-QZVPPD | PySCF KS with register_custom_functional_ ext params on ferric's (75,110) grid; numpy LinLCCD(hh) on PySCF density-fitted λ·erfc integrals; ladder off equals λ² × PySCF SR-MP2; Be₂: paper Table 4 (2.3 kcal/mol) | E_KS ≤ 1.05e-9 Ha; λ·E_c on the same orbitals 7.2e-14 Ha; total on ferric's orbitals 1.1e-7 Ha; Be₂ 2.299 kcal/mol | 1e-8; 1e-11; 5e-7 Ha; 0.3 kcal/mol | validation_wb97xlv.rs, wb97x_l_v_be2_bde.rs, gen_wb97xlv.py |
| OO-RI-MP2 energy and analytic nuclear gradient (all electrons, exact-J/K Fock, RI correlation) | distorted H2O, distorted NH3 (RHF, energy and gradient); CH3 doublet (UHF, energy) / cc-pVDZ (cc-pvdz-ri) | Energy: an independent numpy OO-RI-MP2 on PySCF integrals (exact J/K, the same aux, minimized to max orbital gradient ≤ 1e-10, anchored to PySCF SCF + DF-MP2 at zero rotation); cross-check: ORCA OO-RI-MP2 NoRI NoFrozenCore with the same /C aux, which stops 3.7e-8 to 7.0e-8 Ha above the minimum (reference/doubles split off by up to 1.5e-5 Ha). Gradient: 5-point finite difference of ferric's own OO energy; loose cross-checks against the finite difference of ORCA's OO energy and ORCA's analytic EnGrad (which misses that finite difference by up to 8e-6 Ha/Bohr) | total 7.5e-13 Ha, reference/doubles split 6.4e-10 Ha vs numpy; gradient 8.0e-9 Ha/Bohr vs own FD, 2.0e-7 vs the FD of ORCA's energy | 1e-11 Ha (total), 5e-9 Ha (split), 2e-7 Ha (vs ORCA); 1e-7 Ha/Bohr (own FD), 1e-6 Ha/Bohr (ORCA FD) | validation_oo_rimp2.rs, gen_oo_rimp2.py, ORCA inputs |
Unrestricted RI-MP2 (rimp2 with multiplicity > 1): E_corr and the αα, ββ, αβ blocks | OH, CH3, NH2, O2 (triplet), HO2 / cc-pVDZ (cc-pvdz-ri); HO2 also with frozen core 2 | PySCF DFUMP2 with the same aux on a stability-checked exact-integral UHF; αα and ββ split by an independent numpy build anchored to DFUMP2's same-spin and opposite-spin energies to 1e-11 Ha | E_UHF ≤2.8e-12 Ha; every MP2 energy ≤9.2e-10 Ha (HO2), ≤5.8e-11 Ha elsewhere | 3e-11 Ha; 1e-8 Ha | validation_u_rimp2.rs, gen_u_rimp2.py |
| OO-RI-MP2 with a def2 ECP: energy and analytic nuclear gradient (core Hamiltonian includes V_ECP; gradient includes the ECP derivative term) | HI at 1.75 Å (RHF) / def2-SVP (def2-svp-rifit), gradient on the I atom | Energy: the independent numpy OO-RI-MP2 extended with PySCF's ECP core Hamiltonian (ECP read from ferric's basis JSON), anchored to PySCF RHF + DF-MP2 at zero rotation; gradient: 5-point finite difference of ferric's own OO energy | OO total 1.5e-8 Ha (the ECP integral offset the RHF energy also carries); doubles 5.5e-11 Ha; gradient 2.3e-9 Ha/Bohr vs its own finite difference | 2.5e-7 Ha; 5e-8 Ha/Bohr | validation_oo_rimp2_ecp.rs, gen_oo_rimp2_ecp.py, zero-rotation anchors in oo_rimp2_ecp.rs |
| Open-shell PDEP-RPA correlation energy (UHF reference, full rank, spin-summed dielectric) | OH, CH3, NH2 (doublets), O2 (triplet) / cc-pVDZ (cc-pvdz-ri); OH / aug-cc-pVDZ (aug-cc-pvdz-rifit); H2O / cc-pVDZ through both the restricted and the unrestricted path | PySCF gw.urpa.URPA (and gw.rpa.RPA for H2O) on the same stability-checked exact-integral UHF, same aux basis and the same 40-point Gauss–Legendre frequency grid; cross-checked by numpy on PySCF's density-fitted integrals; truncation checked against a numpy emulation | ≤ 9.2e-11 Ha (OH/aug-cc-pVDZ); truncated 6.4e-11; U path vs R path at the closed-shell limit 3.2e-12 | 1e-9 Ha (U vs R 3e-11) | validation_urpa.rs, gen_urpa.py |
dRPA@HF by the drCCD Riccati solve (drpa, exact) and its local approximation ([local] amplitude threshold); closed shell, frozen core 0, 1 and (n-butane) 4 | H2 / STO-3G (STO-3G aux); H2O, n-butane / 6-31G, cc-pVDZ (cc-pvdz-ri); n-octane / 6-31G (cc-pvdz-ri), carbon cores frozen, ε = 0 to 1e-3 | numpy plasmon dRPA on PySCF density-fitted integrals with the same aux basis and an exact-J/K RHF, Fock semicanonicalized, Ω² from a symmetric eigensolve; the formula and its frozen cores match PySCF gw.rpa.RPA on the same integrals (≤ 3.6e-11 Ha at 160 frequency points) and the H2 value matches the proof notebook's −0.0126072623 (3.7e-11) | ε = 0: ≤ 3.0e-11 Ha on the CLI's exact route, the integral-direct route and the in-crate canonical plasmon reference. n-octane, ε = 1e-6 / 1e-5 / 1e-4 / 1e-3: error +6.7e-7 / +3.9e-5 / +5.5e-4 / +1.0e-2 Ha (less correlation) at 82 / 50 / 18 / 2.8% of amplitudes kept; there is no external finite-ε reference | 3e-10 Ha (ε = 0) | validation_drpa_amplitude.rs, gen_drpa_amplitude.py; references in testdata/reference/validation/drpa_amplitude/ |
| Attenuated PDEP-RPA correlation energy (erf and erfc on the 3-center integrals and the metric; closed shell, full rank) | H2O, NH3 / cc-pVDZ (cc-pvdz-ri); H2O / aug-cc-pVDZ (aug-cc-pvdz-rifit); ω 0.2, 0.222, 0.42, 1.0 Bohr⁻¹ | numpy dRPA on PySCF int3c2e/int2c2e under with_range_coulomb(±ω), the same metric factorization as ferric (eigh with a 1e-10 cutoff for erf, Cholesky for erfc), the same RHF and the same 40-point Gauss–Legendre grid; the Coulomb kernel matches PySCF gw.rpa.RPA to 1.6e-14 Ha; erfc(ω → 0) and erf(ω → ∞) reproduce Coulomb RPA | ≤ 5.8e-11 Ha (erf), 3.5e-11 (erfc), 4.5e-11 (Coulomb); the ω → 0 erfc and ω → ∞ erf limits equal Coulomb RPA to 4.9e-11 | 1e-9 Ha (erf), 5e-10 (erfc, Coulomb, limits) | validation_attenuated_rpa.rs, gen_attenuated_rpa.py |
RS-MP2-RPA correlation energy, formulations B (DeltaLr: E_MP2[Coulomb] + E_dRPA[erf] − 2·E_OS[erf]) and T (CoupledRings: E_MP2[Coulomb] + ΔdRPA[Coulomb] − ΔdRPA[erfc]); every reported component | H2O, NH3 / cc-pVDZ (cc-pvdz-ri); H2O / aug-cc-pVDZ (aug-cc-pvdz-rifit); ω 0.222 (= 0.420 Å⁻¹, the default) and 0.42 Bohr⁻¹ | numpy assembly on PySCF int3c2e/int2c2e under with_range_coulomb(±ω): RI-MP2 spin components and dRPA from the same fitted integrals ferric uses per operator (eigh with a 1e-10 cutoff for erf, Cholesky otherwise), 40-point Gauss–Legendre grid; Coulomb pieces match PySCF DFMP2 to 3.3e-16 Ha and gw.rpa.RPA to 1.7e-14 Ha; the frequency-integrated second-order ring term equals 2·E_OS to 3.2e-15 relative; exact limits B, T(ω → 0) = MP2 and B, T(ω → ∞) = MP2 + ΔdRPA[Coulomb] | every component ≤ 4.7e-11 Ha (MP2 pieces 4.2e-11, dRPA pieces 2.9e-11, B/T e_corr 4.3e-11); ω → 0 and ω → ∞ limits to 4.3e-11 | 5e-10 Ha | validation_rs_mp2_rpa.rs, gen_rs_mp2_rpa.py |
Closed-shell RPA nuclear gradient (total_rpa_gradient: central finite difference of E_RHF + E_c^RPA, exact-J/K RHF reference, full rank; all electrons and one frozen core orbital) | distorted H2O, distorted NH3 / cc-pVDZ (cc-pvdz-ri) | finite differences of PySCF exact-integral RHF + gw.rpa.RPA with the same aux basis and the same 40-point Gauss–Legendre frequency grid (5-point stencil step-converged to 7.4e-10 Ha/Bohr, and the 3-point stencil at ferric's step); 5-point finite difference of ferric's own energy; controls: RHF gradient, E_c-only gradient, all-electron vs frozen core, 6- vs 40-point grid | E_c ≤ 1.5e-13 Ha; gradient ≤ 1.9e-9 Ha/Bohr vs PySCF at the same 3-point step, ≤ 5.7e-8 vs a converged 5-point FD (the 3-point truncation); sum over atoms ≤ 4.8e-9 | 1e-12 Ha (E_c); 2e-8 (same step), 2e-7 (5-point) Ha/Bohr | validation_rpa_gradient.rs, gen_rpa_gradient.py |
| RI-MP2 size-extensivity | H2 dimer at large separation | 2 × monomer | 2e-12 Ha | 1e-7 Ha | rimp2_size_extensivity.rs |
| RHF/UHF/ROHF/KS gradients, including density-fitted J/K | water, OH, HO2 / cc-pVDZ, 6-31G | finite differences of the energy; PySCF df.grad | 1e-7 to 3e-7 Ha/Bohr (FD); ~1e-10 (PySCF) | 1e-6 Ha/Bohr | df_jk_gradient.rs |
KS gradient on a custom or angularly pruned XC grid (RKS LDA/PBE, UKS PBE, ROKS PBE; flat 40x194, NWChem 30x194, SGX-pruned 20x194), through restricted_scf_gradient / unrestricted_scf_gradient / restricted_open_scf_gradient | water, NH2 / 6-31G | central FD of the energy on the same grid; an LDA water optimization on 40x194 then checked for a stationary point of that grid's energy | 0.9e-7 to 1.4e-7 Ha/Bohr (default-grid FD floor 0.9e-7 to 1.4e-7); the default-grid gradient paired with the same SCF misses by 2.8e-4 to 6.0e-4; the optimum has max FD gradient 1.4e-7. The VV10 nonlocal gradient (no Becke weight response by design) is not FD-validated, only shown to depend on nlc_grid | 3e-7 Ha/Bohr | ks_gradient_scf_grid.rs |
| Analytic RHF Hessian (skeleton one- and two-electron, overlap/W and CPHF response terms) and its harmonic frequencies | H2O, NH3, CH2O / cc-pVDZ; distorted H2O / def2-SVP | PySCF hessian.rhf (analytic, and its skeleton partial_hess_elec + hess_nuc); PySCF finite differences of its analytic gradient; ferric's own gradient differenced | total Hessian ≤ 1.6e-7 Ha/Bohr² vs PySCF analytic (≤ 8.2e-8 at cc-pVDZ); skeleton ≤ 5.9e-10; frequencies ≤ 6.9e-4 cm⁻¹; each skeleton term vs a finite difference of its gradient piece ≤ 3.4e-8 | 1e-6 Ha/Bohr² (total), 1e-8 (skeleton), 1e-2 cm⁻¹ | validation_rhf_hessian.rs, rhf_hessian_fd.rs, gen_rhf_hessian.py |
Analytic RHF Hessian for RI-J with exact K (the fitted Coulomb energy ½ dᵀV⁻¹d with the Cholesky-solved metric: skeleton with the 3c and 2c second derivatives (a stencil of the first-derivative blocks) and the metric cross terms, J^x with the auxiliary-fit part in the first-order Fock matrix, the SCF's own DfJ in the CPHF kernel) and its harmonic frequencies | H2O, CH2O, distorted NH3 / cc-pVDZ with cc-pVDZ-RIFIT; distorted H2O / def2-SVP with def2-universal-jkfit | PySCF has no analytic RI-J + exact-K Hessian, so: an analytic Hessian composed from PySCF's own pieces (df.hessian.rhf J skeleton and vj1 with the full auxiliary response, hessian.rhf exact K, gen_response of the only_dfj mean field) and finite differences of PySCF's analytic only_dfj gradient; ferric's own DF gradient differenced with the SCF re-converged; an exactness anchor (an auxiliary basis that spans the density's products); the exact-J Hessian as negative control | total vs the composed PySCF Hessian ≤ 1.6e-7 Ha/Bohr² (3.3e-8 H2O, 8.3e-8 CH2O, 1.7e-8 NH3, 1.6e-7 H2O/def2-SVP); skeleton ≤ 1.9e-10; frequencies ≤ 6.9e-4 cm⁻¹; vs PySCF's FD Hessian ≤ 1.6e-6 (that FD's own floor); vs ferric's differenced DF gradient (displaced SCFs converged to 1e-14 in energy) 9.2e-11 (STO-3G), 3.3e-10 (cc-pVDZ), 1.2e-10 (NH3), 4.2e-10 (def2-SVP); with an auxiliary basis that spans the density's products the fitted skeleton and J^x equal the exact four-centre ones to 2.2e-13 and 5.8e-13 (1.8e-5 and 4.4e-5 with the ordinary RIFIT functions); the exact-J Hessian misses by 1.0e-4 (def2-SVP) to 5.3e-4 (51× to 265× the bar) and its frequencies by 0.32 to 1.04 cm⁻¹; omitting the auxiliary-fit part of J^x, the metric second-derivative term or the cross terms misses by 0.12 to 7.5e3; a stencil step ten times too large (4.0e-7) is seen only by the 1e-8 comparison with ferric's own gradient, and an explicit metric inverse is blind on well-conditioned metrics (8e-12 for water) and seen only on an ill-conditioned one (9.0e-7 on the RI-J pieces for benzene/def2-universal-jkfit); neither reaches the PySCF bar | 2e-6 Ha/Bohr² (total), 2e-9 (skeleton), 1e-5 (vs PySCF FD), 1e-8 (vs ferric's DF gradient), 1e-2 cm⁻¹; control ≥ 10× the bar. RI-J with an external potential, and a metric with eigenvalues below 1e-10 are refused, not validated | validation_df_hessian.rs, df_hessian_rij.rs, df_hessian_stencil.rs, gen_df_hessian.py |
Analytic RHF Hessian for RI-K (the fitted exchange energy −¼ Σ_ij L_ij · V⁻¹ L_ij per occupied pair with the SCF's V^{-1/2}V^{-1/2} metric: skeleton with the 3c and 2c second derivatives (a stencil of the first-derivative blocks) and the cross terms, K^x with its metric and auxiliary-centre response in the first-order Fock matrix, the SCF's own DfK in the CPHF kernel), alone and with RI-J, and its harmonic frequencies | H2O, CH2O, distorted NH3 / cc-pVDZ and distorted H2O / def2-SVP, all with def2-universal-jkfit; RI-K alone also with STO-3G, 6-31G and cc-pVDZ-RIFIT | PySCF's analytic density-fitted Hessian (df.hessian.rhf, full auxiliary response; J and K fitted with the same auxiliary basis, a direct reference) and finite differences of PySCF's analytic DF gradient; ferric's own DF-K gradient differenced with the SCF re-converged; for one occupied column the RI-K skeleton equals 2α times the RI-J skeleton (an independent code path); the exact-integral Hessian as negative control | total (RI-J + RI-K) vs PySCF ≤ 1.6e-7 Ha/Bohr² (3.3e-8 H2O, 8.2e-8 CH2O, 1.8e-8 NH3, 1.6e-7 H2O/def2-SVP); frequencies ≤ 6.9e-4 cm⁻¹; vs PySCF's FD Hessian ≤ 1.6e-6 (that FD's own floor); vs ferric's differenced DF-K gradient (RI-K alone, displaced SCFs converged to 1e-14 in energy) 1.1e-10 (STO-3G), 1.3e-10 (6-31G), 3.4e-10 (cc-pVDZ), 5.7e-10 (NH3), 4.8e-10 (def2-SVP) and 3.8e-10 for RI-J + RI-K at cc-pVDZ; K^x vs the SCF's DfK differenced ≤ 5e-7; the one-column identity holds to 5e-11; the error is flat (1.3e-10 to 4.4e-10) for cond(V) 2.5e5 to 2.5e7 and 6.7e-9 at 2e8, where the Hessian warns; the exact-integral Hessian misses by 2.7e-5 (def2-SVP) to 1.6e-4 (CH2O) (17× to 99× the bar) and its frequencies by 0.28 to 0.54 cm⁻¹, RI-J with exact K by 8.7e-5 to 1.6e-4, RI-K with exact J by 1.0e-4 to 3.2e-4; dropping K^x, its metric response, its auxiliary-centre part, the 2c second derivative or the cross terms misses by 2.0e-2 to 5.9e4; a stencil step ten times too large (6.8e-8) is seen only by the comparison with ferric's own gradient, and an explicit metric inverse is blind on well-conditioned metrics (4e-13 for water) and 7.1e-9 on benzene/def2-universal-jkfit; there is no exactly-spanning auxiliary basis for exchange (the pair densities of different atoms are two-centre products), so no exact-integral anchor exists | 1.6e-6 Ha/Bohr² (total vs PySCF), 1e-5 (vs PySCF FD), 1e-8 (vs ferric's DF-K gradient), 1e-2 cm⁻¹; control ≥ 10× the bar. Range-separated RI-K, RI with an external potential, and a metric with a mode below 1e-10 (the modes DfK drops) are refused, not validated; auto also falls back to finite differences for a mode in [1e-10, 1e-7), cond(V) above 1e8 and memory that does not fit. The PySCF comparison runs only in the weekly validation_ CI job; the per-PR tests are the finite-difference, anchor, mutation, refusal and memory tests of df_hessian_rik.rs and df_hessian_rik_memory.rs. At cond(V) = 2e8 (aug-cc-pVQZ-RIFIT) the Hessian agrees with PySCF's analytic Hessian to 2.7e-8 (2.2e-8 at cond 2.5e5) but with the differenced ferric DF-K gradient only to 6.7e-9 (the RI-J analogue 1.7e-10), which is not established as an error of the Hessian | validation_df_hessian_rik.rs, df_hessian_rik.rs, gen_df_hessian.py |
Analytic UHF Hessian for RI-J and/or RI-K (the Coulomb energy ½ dᵀV⁻¹d of the total density with the Cholesky-solved metric, and the exchange energy as one channel per spin, −½ Σ_ij L^σ_ij · V⁻¹ L^σ_ij with X_σ = C_σ,occ over the SCF's V^{-1/2}V^{-1/2} metric shared by both channels: skeleton from the summed 3c and 2c stencil weights and the per-channel cross terms, J^x of the total density and K^x per spin in the first-order Fock matrices, the SCF's DfJ on the total response density and DfK on each spin's response density in the coupled α/β CPHF), and its harmonic frequencies | NH2 (²B1), CH3 (²A₂″) and CH2 (³B1) / cc-pVDZ, NH2 / def2-SVP, all with def2-universal-jkfit; STO-3G and 6-31G (cc-pVDZ-RIFIT) for the per-term tests | PySCF's analytic density-fitted UHF Hessian (df.hessian.uhf, full auxiliary response; J and K fitted with the same auxiliary basis, a direct reference) on the lowest internally stable UHF, and finite differences of PySCF's analytic DF UHF gradient; ferric's own UHF DF gradient differenced with the SCF re-converged; a closed-shell UHF equals the RHF RI-J / RI-K / RI-JK Hessian term by term; the multi-channel RI-K terms equal the sum of the one-channel (RHF-validated) terms; exact-integral, RI-J-only and RI-K-only Hessians as negative controls | total (RI-J + RI-K) vs PySCF ≤ 1.7e-7 Ha/Bohr² (7.6e-8 NH2, 6.1e-8 CH2, 1.7e-7 CH3, 1.2e-7 NH2/def2-SVP); frequencies ≤ 3.1e-3 cm⁻¹; vs PySCF's FD Hessian ≤ 1.1e-6 (that FD's own floor, 5.6e-7 to 1.2e-6); vs ferric's differenced UHF DF gradient (SCF converged to 1e-14 in energy) 7.1e-10 (CH2), 2.8e-9 (NH2), 9.4e-10 (CH3) at cc-pVDZ, 6.6e-11 to 7.3e-10 for RI-J, RI-K and RI-JK at STO-3G and 6-31G; closed-shell limit 5.6e-17 (RI-J), 2.9e-13 (RI-K), 1.2e-13 (RI-JK); K^x of each spin vs the SCF's DfK differenced ≤ 1.2e-8; the per-spin skeleton pieces and the J + K skeleton vs fixed-density differences of the DF gradient ≤ 4.1e-7; the exact-integral Hessian misses by 5.9e-5 (NH2/def2-SVP) to 1.2e-4 (35× to 72× the bar) and its frequencies by 0.27 to 1.4 cm⁻¹, RI-J with exact K by 9.6e-5 to 1.6e-4, RI-K with exact J by 1.6e-4 to 2.9e-4; swapped spin channels (0.57 to 1.2), the closed-shell channel for each spin (0.57 to 0.87), a dropped β channel (2.0 to 2.9), the exchange of the wrong spin in the CPHF (7.8e-2 to 0.14), a dropped K^x (4.1 to 6.0), the dropped metric second derivative (0.19 to 0.29), dropped cross terms (8.7e2 to 2.6e3) all miss by more than 10⁴ bars; a stencil step ten times too large (4.8e-8 for K, 2.4e-7 for J) is seen only by the comparison with ferric's own gradient; with df_j_aux / df_k_aux unset or empty no density-fitting code runs and the Hessian is bit-identical to the exact-integral one | 1.7e-6 Ha/Bohr² (total vs PySCF), 1e-5 (vs PySCF FD), 1e-8 (vs ferric's DF gradient), 1e-10 (closed-shell limit), 1e-2 cm⁻¹; control ≥ 10× the bar. RI with an external potential, range-separated RI-K and a metric with a mode below 1e-10 are refused, not validated; auto also falls back to finite differences for a mode in [1e-10, 1e-7), cond(V) above 1e8 and memory that does not fit (the two spin channels and the CPHF phase are sized together and, with the raw three-index tensor, bound the measured peak allocation of the whole Hessian (0.86 to 0.99 of estimate plus raw on two shapes)). The PySCF comparison runs only in the weekly validation_ CI job; the per-PR tests are the finite-difference, anchor, mutation, refusal, dispatch and memory tests of df_hessian_uhf.rs, df_hessian_uhf_memory.rs and frequency_hessian_dispatch.rs | validation_df_hessian_uhf.rs, df_hessian_uhf.rs, df_hessian_uhf_memory.rs, gen_df_hessian.py |
Analytic ROHF Hessian for RI-J and/or RI-K (the ROHF energy is the UHF functional of the constrained (D_α, D_β): RI-J of the total density and one RI-K channel per spin with X_α = C[:, ..n_α], X_β = C[:, ..n_β] of the single orbital set; the skeleton, J^x and per-spin K^x are the UHF ones, and the commutator-form response uses the SCF's DfJ on the total response density and DfK on each spin's response density, with the spin Focks and W rebuilt through the same fitted builders) and its harmonic frequencies | NH2 (²B1), HO2 (²A″) and CH2 (³B1) / STO-3G, 6-31G and cc-pVDZ; singlet water / 6-31G for the closed-shell limit; def2-universal-jkfit and cc-pVDZ-RIFIT | ferric's own ROHF DF gradient differenced with the SCF re-converged (Richardson, h = 2e-3 and 1e-3, SCF to 1e-14 in energy); the RHF RI-J / RI-K / RI-JK Hessian for a closed-shell ROHF (an exactness anchor, term by term); exact-integral, RI-J-only and RI-K-only Hessians as negative controls. PySCF has no density-fitted ROHF Hessian (df.hessian covers RHF, UHF, RKS and UKS), so there is no PySCF leg: the shared per-spin RI code is validated against PySCF for UHF above | vs ferric's differenced ROHF DF gradient 5.5e-11 (NH2 RI-JK), 1.6e-11 (CH2 RI-J), 7.4e-10 (CH2 RI-K), 8.5e-10 (HO2 RI-JK) at STO-3G; 2.3e-11 (NH2) and 1.4e-11 (CH2) at 6-31G; 1.1e-10 (NH2) and 2.6e-10 (CH2) at cc-pVDZ / def2-universal-jkfit; closed-shell limit 1.0e-11 (RI-J), 1.1e-11 (RI-K), 9.8e-12 (RI-JK), the same level as the exact-integral ROHF anchor (CG tolerance); the exact-integral ROHF Hessian misses the RI-JK reference by 1.6e-4, RI-J with exact K by 1.8e-4 and RI-K with exact J by 3.4e-4 (1.6e4 to 3.4e4 bars); mutants (NH2 / 6-31G, RI-JK): J from the α density only 20, swapped spin channels 0.65, the closed-shell channel for each spin 0.61, a dropped β channel 0.53, the β occupied factor taken from α (open orbitals in the β channel) 0.32, the exchange of the other spin in the CPHF 0.16, a dropped K^x 2.4, the dropped metric second derivative 0.22 (K) and 1.3 (J), dropped cross terms 2.9e2 (K) and 1.6e3 (J), the omitted auxiliary-fit part of J^x 8.3e-2, the six ROHF response defects of the exact path 1.9e-2 to 0.64 all miss by more than 6e6 bars, the exact four-centre K inside the CPHF kernel by 1.8e-5 (5.9e3 bars, the closest), and two kernel defects (Coulomb from the α response density only, exchange of the total response density per spin) make the orbital Hessian indefinite and are caught by the solver's typed error; with df_j_aux / df_k_aux unset or empty the ROHF- and UHF-exact Hessians and the UHF RI-JK Hessian are bit-identical to the commit before this change (FNV hash of all bits, NH2 / 6-31G and CH2 / STO-3G; a one-time measurement, the committed test compares unset against empty names) | 1e-8 Ha/Bohr² (vs ferric's DF gradient), 1e-10 (closed-shell limit), 3e-9 (mutation bar: 130× the production agreement on the mutation system, 2.3e-11; the closest mutant misses by 5.9e3 bars); control ≥ 10× the bar. External potentials, range-separated RI-K and a metric with a mode below 1e-10 are refused, not validated; auto also falls back to finite differences for a mode in [1e-10, 1e-7), cond(V) above 1e8 and memory that does not fit. Not covered: bases above cc-pVDZ, molecules above 3 atoms, states other than the doublets and triplet above (a quartet or higher, a non-aufbau or internally unstable ROHF solution gets the Hessian of that solution), a run on a first-derivative libint, and an end-to-end CLI/Python run | df_hessian_rohf.rs, frequency_hessian_dispatch.rs |
Analytic closed-shell KS Hessian (LDA, GGA, and global hybrids of both) with exact or RI-J Coulomb and exact or RI-K exchange on a grid fixed in space (rks_fixed_grid_hessian, library entry point only: not reachable from auto or analytic, so no frequency run changes). The energy differentiated has its Becke grid frozen at the reference geometry (PySCF grid_response=False), so the Hessian is the XC AO-derivative skeleton (a GGA needs the third AO derivatives, ferric_integrals::ao_grid_third), the first-order XC Fock matrix and the f_xc kernel (with the σ = |∇ρ|² couplings for a GGA) in the CPHF on top of the RHF exact-J/K Hessian with the exchange scaled by the hybrid fraction a; it omits the Becke weight response that the production KS gradient carries | distorted H2O / STO-3G, 6-31G, cc-pVDZ (SCF-level anchors on a 50x110 grid; PySCF legs on a 30x50 grid) | central FD of the validated fixed-grid gradient with a fresh fixed-grid SCF at every displaced geometry (Richardson, h = 5e-4 and 2.5e-4), that gradient vs central FD of the fixed-grid energy, each XC piece vs a frozen-density difference (no SCF), and PySCF dft.RKS (LDA_X,LDA_C_VWN, HYB_LDA_XC_LDA0, PBE0, PBE, B3LYP) Hessian with grid_response=False fed ferric's own grid points and weights through mf.grids.coords / weights (gen_ks_fixed_grid_hessian.py) | gradient vs FD of the energy 3.7e-10; XC skeleton vs frozen-density gradient difference 1.9e-8 (6-31G; falls 16x per halving of h, so it is the stencil's h^4 truncation) and the first-order Fock matrix vs FD of V_xc 5.6e-12; kernel response vs FD of V_xc(D) 1.9e-12; Hessian vs FD of the fixed-grid gradient 5.7e-11 (STO-3G), 1.9e-8 (6-31G), 8.1e-9 (cc-pVDZ); Hessian from the production SCF vs the injected fixed-grid SCF 4.8e-12; vs PySCF on the same grid: energy 5.9e-12 (6-31G) and 1.2e-11 (cc-pVDZ), Hessian 4.1e-8 and 9.3e-7, frequencies 7.9e-5 and 2.8e-3 cm⁻¹ (PySCF's own analytic Hessian vs central FD of its own gradient at h = 5e-4 differs by 7.4e-7 and 4.2e-6: that is the h^4 truncation of the stencil, not reference noise (ferric's FD at that step equals PySCF's FD to 7.8e-9, and halving h takes ferric's own FD from 3.3e-6 to 2.1e-7); taking PySCF's FD to h = 2.5e-4 and 1.25e-4 the cc-pVDZ gap falls to 1.2e-6 and 9.2e-7 and stops, so about 9.2e-7 of PySCF's analytic cc-pVDZ Hessian is not the derivative of PySCF's own gradient, and that is the whole of the 9.3e-7 by which ferric's analytic Hessian differs from PySCF's analytic one (ferric's analytic Hessian matches FD of its own gradient to 8.1e-9; the cause on the PySCF side is not isolated); negative control: differencing the production gradient (grid on the atoms, with weight response) misses the fixed-grid Hessian by 1.1e-3 Ha/Bohr² (1.1e4 bars) and moves a frequency by 3.0 cm⁻¹ (water / 6-31G, default grid); production-wired mutants (XC skeleton dropped 4.8e-1, f''ρρ term dropped 1.5e2, f'ρ₍₂₎ term dropped or doubled 1.5e2, f'' term of F^x dropped 8.7e-1, f' term of F^x dropped 9.1e-1, CPHF kernel dropped 2.6e-2, doubled 3.0e-2, halved 1.4e-2) miss by 1.4e5 to 1.5e9 bars; the RHF exact-J/RI Hessian bits are unchanged (FNV hash of all bits, water STO-3G and 6-31G exact, STO-3G RI-J+RI-K and RI-J, before and after the change; a one-time measurement) | 1e-7 Ha/Bohr² (vs FD of the gradient, ~5x the measured max), 2e-9 Ha/Bohr (gradient), 1e-9 Ha (energy vs PySCF), 5e-7 and 1e-5 Ha/Bohr² (vs PySCF's analytic Hessian: 12x the measured 4.1e-8 at 6-31G and 11x the measured 9.3e-7 at cc-pVDZ, where the 9.3e-7 is PySCF's own analytic-vs-gradient inconsistency described above, so the cc-pVDZ bar tests ferric against PySCF to PySCF's own consistency, not tighter), 1e-3 and 3e-2 cm⁻¹. GGA and hybrid GGA (PBE, PBE0, B3LYP; STO-3G and 6-31G, PBE also cc-pVDZ): gradient vs FD of the energy 3.3e-10; XC skeleton vs frozen-density gradient difference 2.9e-11 (STO-3G) and 4.2e-8 / 9.1e-8 (6-31G, PBE0 / PBE: the stencil's h^4 truncation, error ratios 38, 22, 15.5 per halving of h); first-order Fock vs FD of V_xc 3.2e-10; kernel response vs FD of V_xc(D) 2.2e-12; Hessian vs FD of the fixed-grid gradient 5.4e-11 (PBE0), 7.7e-11 (PBE), 5.1e-11 (B3LYP) at STO-3G and 4.2e-8 (PBE0, 6-31G); the energy on the injected fixed-grid SCF vs the production SCF 1.7e-11; an LDA pushed through the gradient-dependent assembly equals the LDA Hessian to 2.8e-14; vs PySCF on the same grid: energy 3.4e-12 (PBE0), 7.1e-10 (B3LYP, bar 5e-9), 1.2e-11 (PBE/cc-pVDZ), Hessian 1.0e-7, 1.0e-7 and 9.9e-7 (the cc-pVDZ number matches the pure-LDA cc-pVDZ gap, 9.3e-7; its cause is not isolated: the ρ < 1e-10 density floor ferric applies and PySCF does not was tested and moves PySCF's Hessian by only 2e-10, so it is not the cause; PySCF's own analytic-vs-FD gap at that basis, 1.2e-5, is itself noise-limited by its stencil), frequencies 1.4e-4, 1.5e-4, 4.1e-3 cm⁻¹ (bars 5e-7, 5e-7, 1e-5 Ha/Bohr²; a full-Hessian FD anchor with d functions runs per PR for LDA and PBE at cc-pVDZ); the third AO derivatives match a difference of the analytic Hessian to 1e-8 relative for every shell up to g, pure and Cartesian; ten production-wired mutants (kernel part, F ρ_xy + G·g_xy, F^x parts, gradient terms in skeleton/F^x/CPHF, exchange) miss by 2.3e4 to 1.4e9 bars. A global-hybrid LDA (HYB_LDA_XC_LDA0, a = 0.25) scales the exact exchange of the skeleton, the first-order Fock matrix and the CPHF G by a: Hessian vs central FD of the fixed-grid gradient 6.4e-11 (STO-3G), 1.4e-8 (6-31G); gradient vs FD of the energy 2.8e-10; vs PySCF on the same grid (gen_ks_fixed_grid_hessian.py, _hyb file) energy 3.8e-12 Ha, Hessian 1.3e-7 Ha/Bohr², frequencies 2.4e-4 cm⁻¹ (bar 5e-7 and 1e-3); the three exact-exchange scalings dropped or the CPHF one doubled miss the FD anchor by 5.5e5 to 1.6e6 bars. RI-J (DfJ over def2-universal-jkfit, exact K; the RI-J skeleton, J^x and CPHF Coulomb of the RHF RI-J Hessian reused unchanged; ks_rij_fixed_grid_hessian.rs, water / STO-3G and 6-31G): fixed-grid DF-J gradient vs FD of the DF-J energy 2.6e-10 (PBE/STO-3G) and 3.1e-10 (PBE0/6-31G), bar 3e-9; LDA and PBE at 6-31G also tied; Hessian vs FD of the DF-J gradient (fresh DF-J SCF per geometry) 7.7e-11 (PBE/STO-3G), 7.1e-11 (PBE0/STO-3G), 1.9e-8 (LDA/6-31G), 9.1e-8 (PBE/6-31G; bar 2e-7); DF-J vs exact-J (PBE/6-31G): |E_DF-E_exact| 3.9e-5 Ha, max|H_DF-H_exact| 1.45e-4 (mixed-unit ratio 3.7, corroboration only), shrinking with the auxiliary basis (cc-pVDZ-RI 2.4e-3 / 2.9e-4 to def2-universal-jkfit); negative control: the exact-J Hessian misses the FD of the DF-J gradient by 1.45e-4 (RI-J: 9.1e-8); the production RI-J SCF (route recorded) gives the injected-SCF Hessian to 2.3e-12; vs PySCF density_fit(only_dfj=True) Hessian (auxbasis_response = 2, grid_response=False, same grid, pure functionals only since PySCF has no RI-J + exact-K Hessian; _rij / _pbe_rij files): energy 5.9e-12 / 5.8e-12, Hessian 4.2e-8 (LDA) and 7.6e-7 (PBE; PySCF's stored raw analytic PBE DF-J Hessian is 9.1e-7 asymmetric, which limits agreement), frequencies 8e-5 and 1e-3 cm⁻¹ (bars 5e-7 / 2e-6 Ha/Bohr²; the per-PR FD anchor at 2e-7 is the discriminating test, the PySCF comparison corroboration); seven production-wired RI-J mutants (2c metric second derivative dropped 2.2, 3c-2c cross terms dropped 7.6e3, aux-fit part of J^x dropped 0.15, erfc metric 3.5e3, exact four-centre Γ_J skeleton in place of the fitted one 2.0e-4, exact J^x 1.4e-4, exact J in the CPHF 7.1e-5) miss by 3.5e2 to 3.8e10 bars; hybrid RI-J + exact K has NO PySCF reference (PySCF has no RI-J + exact-K Hessian) and is anchored by ferric's own FD only (PBE0/STO-3G Hessian, PBE0/6-31G gradient). RI-K (DfK, the hybrid's exchange −(a/4) Tr[D K_DF(D)]: the one closed-shell channel of the RHF RI-K Hessian with weight −a/4, its skeleton, K^x and CPHF DfK reused unchanged; ks_rik_fixed_grid_hessian.rs, water / STO-3G and 6-31G, def2-universal-jkfit): fixed-grid RI-K gradient vs FD of the RI-K energy 1.6e-9 (PBE0/STO-3G, RI-J), 1.3e-9 (B3LYP/6-31G, RI-J), 1.7e-9 (PBE0/6-31G, exact J), bar 3e-9; Hessian vs FD of the RI-K gradient (fresh RI-K SCF per geometry) 7.3e-11 (PBE0/STO-3G, RI-J), 8.9e-11 (PBE0/STO-3G, exact J), 5.7e-11 (B3LYP/STO-3G), 1.4e-8 (hybrid LDA/6-31G), 4.2e-8 (PBE0/6-31G, exact J and RI-J; bar 5e-7, set by the h^4 FD truncation of the GGA XC gradient: B3LYP/6-31G measures 1.2e-7 and runs only against PySCF); exactness anchor: a pure functional handed an RI-K fit gives the RI-J Hessian to 0 (bar 1e-12; the RI-K weight with the exact-exchange fraction forgotten misses by 2.9); RI-K vs exact K (PBE0/6-31G, RI-J both): |dE| 1.22e-5 / 4.31e-6 / 3.33e-6 Ha and max |dH| 2.26e-5 / 1.41e-6 / 1.53e-6 for def2-universal-jkfit / cc-pVTZ-JKFIT / cc-pVQZ-JKFIT (the Hessian difference drops 16x and then sits at the fit's floor for this orbital basis, where the energy difference also stops falling); negative control: the exact-K Hessian misses the FD of the RI-K gradient by 2.3e-5 (RI-K: 4.2e-8); the production SCF with the default RI-J and RI-K (route recorded) gives the injected-SCF Hessian to 3.3e-12; vs PySCF density_fit(auxbasis) (J and K over one def2-universal-jkfit, auxbasis_response = 2, grid_response=False, same grid; _pbe0_rijk / _b3lyp_rijk files): energy 3.9e-12 / 7.1e-10, Hessian 1.0e-7 / 1.0e-7, frequencies 1.4e-4 / 1.6e-4 cm⁻¹ (bars 1e-6 Ha/Bohr² and 2e-3 cm⁻¹); PySCF's raw analytic Hessians are 4.1e-8 (PBE0) and 1.9e-7 (B3LYP) asymmetric, so the B3LYP agreement is at PySCF's own asymmetry while the PBE0 one is 2.4x above it (the cause is not isolated), and PySCF's own analytic Hessian vs central FD of its own DF gradient at h = 5e-4 differs by 3.8e-6 (PBE0) and 1.3e-6 (B3LYP), the h^4 truncation of that stencil as for the exact-J cases; a J fit and a K fit over DIFFERENT bases (def2-universal-jkfit for J, cc-pVTZ-JKFIT for K, PBE0/STO-3G) match the FD of their gradient to 5.9e-11 (bar 1e-9), and fitting the RI-K skeleton and K^x over the J basis misses by 3.4e-5, the CPHF DfK over the J basis by 3.1e-7, and at that bar the exact Γ_K skeleton, exact K^x and exact K in the CPHF by 3.2e-6, 3.5e-6 and 1.3e-6 (3.1e2 to 3.4e4 bars); thirteen production-wired RI-K mutants (2c metric second derivative 6.8e-2, cross terms 2.8e2, metric response of K^x 3.9e-2, auxiliary-centre part of K^x 2.8e-2, transposed pair in K^x 0.14, erfc metric 2.1, exact four-centre Γ_K skeleton 3.0e-5, exact K^x 2.0e-5, exact K in the CPHF 5.2e-6, RI-K weight without a 1.6, exchange dropped from the skeleton 0.17, from F^x 0.14, from the CPHF 5.5e-2) miss by 10.4 to 1.4e9 bars (PBE0/6-31G, RI-J + RI-K). The SCF must state its fits: the same RhfConfig the SCF ran with (df_j_aux / df_k_aux unset resolve to the default basis where the SCF fits, as in solve_rhf; a pure functional never fits K, so a named df_k_aux is inert there; a stated basis must equal the recorded route's); a Fock matrix rebuilt with the stated Coulomb and exchange matrices must reproduce the stored one (an exact-K SCF stated as RI-K, or the reverse, is refused). The Becke weight response is the opt-in moving-grid variant (next row); Auto is unchanged and reaches neither. meta-GGA, VV10, double hybrids (B2PLYP, DSD-PBEP86: the MP2 term is not differentiated), range-separated hybrids (and so range-separated RI-K), open-shell references, ECPs and external potentials are refused with a typed error naming the next unit of work; a first-derivative libint2 returns a typed error. Grid caveat (independent review): this Hessian is the derivative of the fixed-grid energy, and at the default 75x110 grid it is itself about 1e-3 Ha/Bohr² from the converged-grid answer (2.1e-6 at 150x590) and breaks translational invariance by 9.7e-4, because a grid frozen in space is not translation invariant; harmonic frequencies are expected to be only weakly affected because translations and rotations are projected out, but that was not measured for this surface. The 1.1e-3 gap to differencing the production gradient closes with grid refinement: it is quadrature error plus the weight response, not a defect. The negative control runs on the default grid because the plain ks_gradient_closed entry point differentiates the default grid by design (the *_on_grids variants and restricted_scf_gradient take the SCF's grid, issue #428). Pruned grids are refused. A Fock matrix rebuilt at the result's density must reproduce its stored Fock to 1e-8 (measured 0 to 3.6e-15), which refuses an HF, other-functional or missing-external-potential result handed in under an LDA configuration (residuals 2.0, 0.17 and 0.14 Ha). Not covered: bases above cc-pVDZ, systems above 3 atoms, a run on a first-derivative libint, and any CLI or Python surface | ks_fixed_grid_hessian.rs, validation_ks_fixed_grid_hessian.rs, ks_rij_fixed_grid_hessian.rs, ks_rik_fixed_grid_hessian.rs, gen_ks_fixed_grid_hessian.py |
Analytic closed-shell KS Hessian on the MOVING (production) grid (rks_moving_grid_hessian; the frequency Hessian of every closed-shell KS run under the default hessian = "auto" wherever rks_moving_grid_hessian_available accepts the system, finite differences otherwise, and of the explicit hessian = "analytic-moving-grid", CLI [frequencies] and Python run_frequencies; analytic reaches neither KS Hessian). The energy differentiated is the one the production KS SCF and ks_gradient_closed use: grid points ride their home atom and the Becke weights (original Becke, 3 iterations, Bragg radii) move with the nuclei. Per home atom h the XC skeleton is the fixed-grid AO skeleton plus the point-fixed Becke weight terms W''f + W'f'ᵀ + f'W'ᵀ (ferric_dft::becke_hessian, contracted point by point, never stored) on the atoms ≠ h, with h's rows and columns filled by translation invariance; the first-order XC Fock matrix gains V[W'_x f_ρ, 2W'_x f_σ∇ρ] and the same fill; the CPKS kernel and every non-XC term are the fixed-grid ones. LDA, GGA and global hybrids, exact or RI-J Coulomb, exact or RI-K exchange | distorted H2O / STO-3G, 6-31G, cc-pVDZ (50x110 and the default 75x110 grid); Ne / 6-31G (exactness anchor) | central FD (Richardson, h = 5e-4, 2.5e-4) of the production gradient (ks_gradient_closed_with_exchange_on_grids) with a production SCF at every displaced geometry; at frozen density, central FD of ferric_dft's moving-grid XC gradient (riding weight1, an independent construction) and of V_xc on a grid rebuilt at each displaced geometry; the Becke weight Hessian vs FD of becke_weights_and_grad; the Python prototype (prototypes/ks_hessian_grid_response, second-order forward AD, H2O/STO-3G LDA: vs FD of its gradient 5.6e-8 at h = 5e-4, falling as h², and the translation-fill reconstruction 4.9e-15) | exactness anchor: one atom gives a moving-grid XC skeleton and F^x of exactly 0 (fixed grid 9.4e-3 LDA, 4.6e-2 PBE; the anchor is blind to the weight terms); Becke weight Hessian vs FD 3.9e-12 (four atoms, 35 points; mutants 0.48 to 3.4); moving XC skeleton vs FD of the moving gradient 4.9e-11 and F^x vs FD of rebuilt V_xc 7.3e-12 (LDA, PBE × STO-3G, 6-31G), 2.4e-11 / 7.9e-12 (PBE cc-pVDZ), while the fixed-grid skeleton misses the same FD by 2.4e-3 to 9.6e-3; translation sum rule 6e-17 (skeleton and F^x, by construction) and 4e-12 to 3e-11 on the full Hessian; full Hessian vs FD of the production gradient 7.9e-11 (LDA) and 4.8e-11 (PBE) at STO-3G, 1.7e-10 (LDA / 6-31G, default grid: the system and reference of the fixed-grid negative control, which the fixed-grid Hessian misses by 1.1e-3 and 3.0 cm⁻¹), 1.05e-9 (PBE / 6-31G; 5.5e-10, 1.05e-9, 1.6e-10 at h = 1e-3, 5e-4, 2.5e-4, so noise, not truncation), 6.0e-11 / 8.2e-11 (PBE0 / STO-3G, 6-31G), 5.8e-11 / 6.7e-11 (RI-J PBE, RI-J + RI-K PBE0, STO-3G), 2.6e-10 (LDA / cc-pVDZ, default grid); harmonic frequencies from the moving-grid Hessian vs those of the symmetrized FD Hessian ≤ 5.0e-7 cm⁻¹ (fixed grid 2.8 to 6.7 cm⁻¹); seven mutants (drop W'' 1.1, drop the W'f' cross terms 2.1, drop the weight term of F^x 0.18, skip the translation fill 0.35, flip its sign 0.50, riding weight1 cross terms unfilled 1.2, batches not split at home atoms 9.5e-3) miss the frozen-density anchors by 9.5e6 to 3.6e9 bars (PBE / 6-31G); cost on H2O / cc-pVDZ, 75x110 grid (shared box): 2.86 s vs 2.59 s fixed-grid (LDA, ratio 1.10), 4.97 s vs 4.63 s (PBE, 1.07); a 3-atom ratio only: the Becke weight Hessian adds O(N^3) work per point and the translation fill a dense (3N)^3 product per batch, so the ratio grows with N and is unmeasured above 3 atoms. Same refusals as the fixed-grid row; a grid built from explicit points (no home atoms) is refused. Binding surface (HessianMethod::AnalyticMovingGrid, H2O / STO-3G, default 75x110 grid, RI-J): the CLI's Hessian, wavenumbers and energy (full-precision frequencies run-log record) equal harmonic_frequencies called in-process bit for bit (LDA), and Python's equal the CLI's bit for bit (PBE; validation tier, the weekly job builds ferric-cli for it); CLI moving-grid vs the CLI's own fd Richardson-extrapolated (h = 5e-4, 2.5e-4, density_conv 1e-11) 7.3e-8 (LDA) and 2.1e-7 (PBE) cm⁻¹, Hessian 2.7e-10 and 1.2e-10 Ha/Bohr² (plain FD at h/2: 1.8e-4 cm⁻¹); Python (run_frequencies(scf_density_conv=1e-11, scf_energy_conv=1e-13)) vs its own fd at the same h: 7.3e-8 and 2.1e-7 cm⁻¹, the CLI's numbers; the per-commit routing check is harmonic_frequencies(AnalyticMovingGrid) vs a direct rks_moving_grid_hessian call on the same SCF, bit for bit; auto gives the analytic-moving-grid result bit for bit for closed-shell KS (H2O / STO-3G, LDA and PBE: library, CLI and Python) and the fd result bit for bit where the moving-grid Hessian does not apply (H2 / STO-3G: meta-GGA SCAN, UKS and D3(BJ) dispersion in the CLI and Python; SCAN, UKS and a pruned grid in the library); hessian = "fd" still differences gradients for a supported KS reference; with a test-only cap on the response AO table (the one size check rks_moving_grid_hessian_available and the Hessian share), H2 / STO-3G LDA (264,000 bytes) is refused before the SCF by the explicit route and runs fd bit for bit under auto one byte below the cap, and the moving-grid Hessian at the cap; auto with the default RI-J (PBE) and RI-J + RI-K (B3LYP) fitting bases (H2O / STO-3G) passes the predicate and runs the moving-grid Hessian; a non-converged SCF under auto returns ScfConvergence; the size check dropped from the predicate fails the cap test; every refusal (Hartree-Fock, UKS, ROKS, meta-GGA, VV10, range-separated hybrid, ECP, point charges / field, pruned grid in the CLI, dispersion, and the RHF Hessian's configuration refusals: non-Coulomb operator, COSX, solvation, embedding, fractional occupations, cDFT, MOM) is named in both bindings and fires before the SCF (no SCF iteration in the run log); run_saddle refuses the value; mutants: the fixed-grid Hessian in its place misses by 4.63 (LDA) and 2.34 (PBE) cm⁻¹ in both bindings and fails the per-commit routing check, the route sent to FD fails the source check, a binding ignoring the knob fails the fd-stays-selectable check, auto routed back to FD for KS fails the auto = analytic-moving-grid check, auto sent to the moving-grid route without the support predicate fails the auto = fd fallback check, the predicate not called fails every pre-SCF refusal. Not covered: bases above cc-pVDZ, systems above 3 atoms in the full-Hessian anchor, pruned grids (refused), comparison with another code's grid-response Hessian, and the binding surface above STO-3G water | 1e-10 Ha/Bohr² (Becke weight Hessian); 1e-9 Ha/Bohr² (skeleton) and 1e-10 Ha/Bohr (F^x); 2e-9 Ha/Bohr² (per-commit full Hessian, STO-3G and LDA / 6-31G); 2e-8 Ha/Bohr² (validation tier: PBE / 6-31G noise, cc-pVDZ, hybrids, RI); 1e-12 (sum rule of the pieces); exactly 0 (one atom); every mutant ≥ 10 bars; bindings: bit-for-bit equality, 3e-6 cm⁻¹ and 3e-9 Ha/Bohr² (CLI vs FD), 3e-6 cm⁻¹ (Python vs FD) | ks_moving_grid_frequencies.rs (CLI), validation_ks_moving_grid_frequencies.rs, test_ks_moving_grid_frequencies.py, ks_moving_grid_frequencies.rs (library), ks_moving_grid_hessian.rs, validation_ks_moving_grid_hessian.rs, becke_hessian.rs |
| Analytic UHF Hessian (skeleton one- and two-electron, overlap/W and coupled α/β CPHF response terms) and its harmonic frequencies | OH (²Π), NH2 (²B1), CH2 (³B1) / cc-pVDZ; tilted OH and off-C2v CH2 / STO-3G, 6-31G | PySCF hessian.uhf on the lowest internally stable UHF (analytic, and its skeleton partial_hess_elec + hess_nuc); PySCF finite differences of its analytic gradient; ferric's own UHF gradient differenced; ferric's RHF Hessian for a closed-shell UHF | total Hessian ≤ 3.8e-7 Ha/Bohr² vs PySCF analytic (OH; ≤ 7.7e-8 for NH2 and CH2); skeleton ≤ 1.4e-10; frequencies ≤ 1.4e-4 cm⁻¹; on closed-shell water the UHF Hessian equals the RHF one term by term (response 2.8e-17) | 2e-6 Ha/Bohr² (total), 1e-8 (skeleton), 2e-3 cm⁻¹, 1e-12 (vs RHF) | validation_uhf_hessian.rs, uhf_hessian_fd.rs, gen_uhf_hessian.py |
Analytic ROHF Hessian (skeleton with the ROHF energy-weighted density W = sym(D_α F_α D_α + D_α F_β D_β), plus the closed-virtual / open-virtual / closed-open orbital response in commutator form) and its harmonic frequencies; a semicanonicalized result is mapped back to ROHF orbitals | NH2 (²B1), CH2 (³B1), HO2 (²A″) / cc-pVDZ; distorted NH2, CH2, HO2 and singlet water / STO-3G, 6-31G | central FD of PySCF's analytic ROHF gradient (PySCF has no ROHF Hessian), h and 2h; ferric's own ROHF gradient differenced with the SCF re-converged; ferric's RHF Hessian for a closed-shell ROHF; the skeleton alone and the UHF Hessian at ROHF orbitals as negative controls | total Hessian vs PySCF FD 6.6e-7 (NH2), 5.6e-7 (CH2), 1.0e-6 (HO2) Ha/Bohr² against PySCF's own h-vs-2h gap of 0.7e-6 to 2.6e-6; frequencies ≤ 0.0032 cm⁻¹; vs FD of ferric's gradient ≤ 3.2e-10; singlet water equals the RHF Hessian to 1.1e-11 (a real ROHF solve and a relabelled RHF result); against PySCF's FD the Hessian without the response misses by 0.35 (NH2), 0.47 (CH2), 0.83 (HO2) (frequencies by 1.3e3 to 2.5e3 cm⁻¹) and the UHF Hessian at ROHF orbitals by 7.8e-2 (NH2) and 6.0e-2 (CH2), while on HO2 that UHF Hessian refuses with a typed error (non-positive-definite CPHF); against ferric's own FD (NH2, CH2 / 6-31G, HO2 / STO-3G) the misses are 0.30, 0.36, 1.1 (no response) and 5.9e-2, 4.8e-2, 3.9e-2 (UHF at ROHF orbitals); eight response mutants each miss by ≥ 1e6 × the bar. External potentials and COSX are refused, not validated (ECPs: see the ECP Hessian row) | 3e-6 Ha/Bohr² (vs PySCF FD; about 3× the largest measured 1.0e-6, because PySCF's own h-vs-2h gap reaches 2.6e-6), 1e-8 (vs ferric FD), 1e-10 (singlet anchor), 0.03 cm⁻¹; control ≥ 10× the bar | validation_rohf_hessian.rs, rohf_hessian_response.rs, rohf_hessian_skeleton.rs, gen_rohf_hessian.py |
Analytic RHF/UHF Hessian with external point and Gaussian-smeared charges (QM/MM, QM–QM block, charges fixed; hessian.rs, hessian_smeared.rs): charge–nucleus repulsion Hessian, charge-attraction second derivatives with the charge-centre blocks discarded, ∂V_ext/∂x in the CPHF Fock derivative; smeared charge-attraction second derivatives by a fourth-order rigid-atom stencil of the first-derivative three-centre blocks | H2O + 10 charges / cc-pVDZ (RHF), CH3OH + 20 charges / def2-SVP (RHF), H2O⁺ + 10 charges / cc-pVDZ (UHF, doublet); H2O and H2O⁺ + 10 smeared charges (widths 0.7/1.1/1.6 Bohr) / cc-pVDZ | central FD (1e-3 Bohr) of PySCF's analytic qmmm.mm_charge gradient (radii= for the smeared rows), QM atoms only (PySCF has no QM/MM Hessian); the same FD without the charges as negative control | max |analytic − FD| 3.4e-7 (H2O), 8.0e-7 (CH3OH), 5.5e-7 (H2O⁺), 3.3e-7 (H2O smeared), 5.5e-7 (H2O⁺ smeared) Ha/Bohr²; the gas-phase Hessian misses by 7.7e-3 to 1.6e-2; vs FD of ferric's own embedded gradient 5.5e-7 (RHF), 3.4e-7 (UHF); translational invariance including the QM–MM cross block 3.6e-9; smeared: vs FD of ferric's own smeared gradient 5.5e-7 (RHF), 3.4e-7 (UHF), width → 1e-3 Bohr vs the point-charge Hessian 6.3e-9 (a 1.0 Bohr width misses by 2.3e-2), cross-block row sums 1.4e-8; an empty potential is bit-identical | 1e-5 Ha/Bohr²; control ≥ 10× the bar | validation_qmmm_hessian.rs, qmmm_hessian_fd.rs, gen_qmmm_hessian.py |
Analytic RHF/UHF Hessian in a uniform external field (hessian_field.rs; the field–nuclear energy is linear in R, so only the electronic E·r term enters: skeleton Σ D ∂²(E·r) and ∂(E·r)/∂x in the CPHF Fock derivative; libint2 has no emultipole1 derivatives, so the dipole integrals are differentiated by a fourth-order stencil on the integrals, on one- and two-atom sub-molecules) | H2O (RHF) and OH (UHF, doublet) / STO-3G, field (0.02, −0.03, 0.025) a.u. | fifth-order second differences of ferric's converged SCF energy (no gradient involved), and central FD of ferric's analytic gradient; zero-field Hessian as negative control | max |analytic − energy FD| 1.0e-7 on 8 elements; max |analytic − FD(gradient)| 5.4e-7 (RHF), 3.5e-7 (UHF); zero-field Hessian misses by 1.8e-2; neutral row sums 4e-12; zero field is bit-identical | 1e-6 (energy FD), 5e-6 (gradient FD); control ≥ 10× the bar | field_hessian.rs |
Analytic RHF/UHF/ROHF Hessian with ECPs (hessian_ecp.rs: skeleton Σ D ∂²V_ECP as a masked fourth-order stencil of the first-derivative ECP blocks, on the default quadrature backend, and ∂V_ECP/∂x in the CPHF Fock derivative; shells up to f) | HI, CH3I / def2-SVP (RHF); CH2I (UHF, doublet) / def2-SVP; Ag2 / aug-cc-pVDZ-PP (RHF) | PySCF hessian.rhf/uhf (analytic, with ECPscalar_ipipnuc, ipnucip, ipiprinv, iprinvip), its skeleton, the ECP-only skeleton rebuilt from those integrals, a fixed-density finite difference of PySCF's analytic ECP gradient term, and a finite difference of PySCF's analytic gradient; ferric's own analytic gradient differenced; the Hessian without the ECP skeleton as negative control | ECP skeleton vs the fixed-density FD of PySCF's ECP gradient term ≤ 5.95e-8 Ha/Bohr² (CH2I; HI 2.6e-10, CH3I 8.2e-9, Ag2 3.5e-10); total Hessian vs FD of PySCF's analytic gradient ≤ 4.4e-7 (Ag2 6.5e-8); vs PySCF's analytic Hessian Ag2 4.9e-9, HI 9.2e-9; at worst 3.7e-6 (CH2I), which is PySCF's own ECP second-derivative roughness (its analytic ECP skeleton misses the FD of its own gradient term by 2.5e-7 CH3I and 3.7e-6 CH2I); frequencies ≤ 7.2e-3 cm⁻¹; vs ferric's own FD of the analytic ECP gradient ≤ 3.8e-7 (RHF), 1.6e-7 (UHF); the Hessian without the ECP skeleton misses by 1.9e-2 to 3.6e-2 (frequencies by 36 to 178 cm⁻¹); a stencil step sweep |S(h) − S(2h)| 9e-12 (4.5e-9 on the libecpint backend); the Ag2 case takes about 30 minutes on a loaded box (four-centre second derivatives with f functions); two different ECP elements (H–Br–I cation / def2-SVP with a second, scaled ECP table on Br; H–Ag–I doublet / aug-cc-pVDZ-PP under --ignored, about 29 minutes): masked vs unmasked stencil 1.6e-13 and 6.3e-13, vs FD of ferric's analytic gradient 2.7e-8 and 9.2e-8, the Hessian without the skeleton misses by 3.5e-2 and 4.5e-2; ECP with point charges, Gaussian-smeared charges and a uniform field (HOI RHF; H–I–H doublet with all three): vs FD of the analytic gradient ≤ 2.5e-7, the environment terms removed misses by 1.4e-2 to 2.2e-2, an unscreened nuclear charge in the charge–nucleus Hessian by 1.3e-1 to 1.4e-1 (one-time measurement; the committed test asserts the screened-vs-bare-Z difference), an empty environment is bit-identical to none and zero-valued charges agree to 1e-10; dropping ∂V_ECP/∂x from F^x misses FD by 6.2e-2 (RHF) and 4.4e-1 (UHF); the Hessian bits of all-electron water (6-31G, def2-SVP) and NH2 are identical before and after the ECP change (FNV hash, a one-time measurement); libecpint backend, measured before the refusal existed (the entry points now refuse it, so these cannot be re-run): HOI full Hessian vs FD of its own gradient 2.3e-7 and HI, CH3I, CH2I within every PySCF bar, but Ag2 misses the ECP-skeleton bar (2.76e-6 against 5e-7; noise-dominated, roughly 1/h at small h with sign changes, plus a real libecpint second-derivative bias of about 1.5e-8 from h = 8e-3 up), so the probe refuses it; libecpint's Ag2 gradient misses PySCF by 8.0e-8 Ha/Bohr (quadrature 5.3e-10), inside the existing libecpint gradient floor, but no committed gradient anchor covers libecpint for Ag or any aug-cc-pVDZ-PP element and the finite-difference fallback under FERRIC_ECP_BACKEND=libecpint is not validated on Ag2 (estimated ~5e-7, unmeasured); the RHF Hessian bits of the all-electron systems drift between the ECP change and HEAD only because #417's incremental-Fock gate changes the SCF at tight energy_conv (6-31G water 15 to 14 iterations, E moves 1.9e-12); with FERRIC_SCF_INCREMENTAL=1 the Hessian hashes match the ECP-change commit bit for bit; ECP with density fitting and ROHF with an ECP (HOI RHF, H–I–H doublet UHF and ROHF / def2-SVP; per-commit ecp_df_hessian.rs, validation tier validation_ecp_df_hessian.rs, 264 s on two threads): vs FD of ferric's analytic fitted gradient 1.0e-7 to 3.7e-7 for RHF RI-J, RI-K and RI-JK, UHF RI-J, RI-K and RI-JK, ROHF exact, RI-J, RI-K and RI-JK (bar 1e-6, derived as 3× the floor and 10× below the smallest defect, the 1.0e-5 fitting signal); the fitted ECP gradient vs a 5-point FD of the fitted energy 3.7e-10; vs PySCF's density-fitted analytic Hessian (RHF HI, CH3I; UHF CH2I) 9.6e-9 to 2.8e-7 (bar 1e-6) and 3.7e-6 for CH2I UHF, which is PySCF's own analytic-vs-FD roughness (3.7e-6; bar twice that), and vs PySCF's FD (the only PySCF reference for ROHF) ≤ 4.4e-7; the exact-integral Hessian misses these references by 1.0e-5 (HI) to 1.1e-3 (rifit), so the fitting signal is present; the Hessian without the ECP skeleton misses FD by 1.2e-2 to 6.6e-2 and without ∂V_ECP/∂x in F^x by 8.3e-2 to 1.9, on every route; dropping the ECP skeleton from the shared RHF or the UHF-DF assembly fails the matching tests (mutation, one-time); the ROHF Fock-trust check rebuilt h without V_ECP (25 Ha deviation, typed refusal) until hcore_ecp replaced it. CONDITIONING: the ordinary def2-universal-jkfit on iodine has an RI-K metric of cond 4.9e8 (HI) to 1.4e9 (CH3I), above the code's 1e8 limit, so the probe refuses it for Auto (tested: Auto runs finite differences, 12 gradients for HI) and the jkfit comparisons are evidence AT that limit, labelled as such in the tests; the within-range evidence uses def2-svp-rifit (probe-accepted for HI, HOI, CH3I). An ECP with an external potential on the fitted routes stays refused | 5e-7 (ECP skeleton vs FD), 1e-5 (vs PySCF analytic), 2e-6 (vs PySCF FD), 5e-2 cm⁻¹ (5e-6 for the environment and two-element FD checks); control ≥ 10× the bar. Refused, not validated: shells above f (probe, before any SCF), the libecpint backend. Not covered: the synthetic Br table is iodine's scaled (same shells, n_core 28; the real H–Ag–I also has 28 on both), so table-structure defects and per-element n_core mix-ups are untested. A first-derivative libint is refused by the probe before any SCF; the ECP route needs only the one-body and four-centre second derivatives up to f, which the wheel's export carries, but a wheel build has not been run | validation_ecp_hessian.rs, ecp_hessian_combinations.rs, ecp_hessian_libecpint.rs, ecp_hessian.rs, ecp_df_hessian.rs, validation_ecp_df_hessian.rs, gen_ecp_hessian.py, gen_ecp_df_hessian.py, validation_ecp_hessian_combinations.rs |
| Harmonic frequencies (FD of analytic gradients; Cartesian Hessian and cm⁻¹) | H2O, NH3 × RHF, PBE, B3LYP / 6-31G, cc-pVDZ; UHF OH, CH3, HO2, UKS-PBE and ROHF CH3 / 6-31G | PySCF same-step FD of analytic gradients (KS with grid response); PySCF analytic hessian.rhf/uhf/rks/uks; thermo.harmonic_analysis with ferric's masses | same-step FD: 2.4e-7 Ha/Bohr², 7.5e-4 cm⁻¹; HF vs analytic 2.75e-5 Ha/Bohr², 0.10 cm⁻¹ (the 5e-3 Bohr step's truncation); KS vs PySCF's analytic Hessian differs by 5–35 cm⁻¹ because that Hessian has no grid response | 2e-6 Ha/Bohr², 5e-3 cm⁻¹ (same-step); 1e-4 Ha/Bohr², 0.5 cm⁻¹ (HF analytic) | validation_frequencies.rs, gen_frequencies.py |
| Meta-GGA gradient, closed shell (SCAN, r2SCAN) | H2O, NH3 / 6-31G, def2-SVP; RI-J (default) and exact J | PySCF RKS grid_response=True, same grid and density fitting; FD of ferric's own energy | ≤ 1.3e-9 Ha/Bohr (def2-SVP 5.0e-10); analytic vs own FD 1.0e-9 | 1e-8 Ha/Bohr | validation_mgga_gradients.rs, gen_mgga_gradients.py |
| Meta-GGA gradient, open shell (SCAN, r2SCAN) | UKS: HO2, NH2 / 6-31G; ROKS: NH2 / 6-31G | PySCF UKS grid_response=True + stability(); ROKS: central FD of PySCF ROKS energy; FD of ferric's own energy | UKS ≤ 6.2e-9 Ha/Bohr; ROKS 2.2e-10 vs the FD; energies ≤ 5.7e-13 Ha | 3e-8 Ha/Bohr (UKS); 1e-7 (ROKS) | validation_mgga_gradients.rs, gen_mgga_gradients.py |
Meta-GGA (τ-only) fxc response (ferric_dft::fxc::MggaFxcKernel): RKS/UKS internal stability, RKS/UKS Newton, RKS/UKS/ROKS KS Z-vector | Stability: NH2 (UKS), water (RKS) / 6-31G, r2SCAN, exact J, flat (75,110) Becke grid. Z-vector: water (RKS), NH2 (UKS), HCO and CH2 triplet (ROKS) / STO-3G, r2SCAN, through the MBD@rsSCS exact gradient. Kernel: closed shell, r2SCAN and SCAN | PySCF newton_ah.gen_g_hop_uhf / gen_g_hop_rhf dense orbital Hessians on the same grid; Newton: ferric's DIIS energy; Z-vector: central FD of the full SCF + MBD pipeline; kernel: FD of the production Vxc | dense spectrum and Davidson λ_min ≤ 1.5e-10 Ha; SCF energy 5.7e-14 Ha; Newton vs DIIS ≤ 1.4e-14 Ha; Z-vector exact gradient 8.0e-10 (RKS), 1.1e-11 (UKS), 2.0e-11 (ROKS) Ha/Bohr; kernel 5.4e-9 (r2SCAN), 3.6e-7 (SCAN) relative. ROKS Newton/AH and TRAH stay on DIIS for a meta-GGA; the analytic KS Hessian refuses it | 1e-8 Ha (eigenvalues); 1e-9 Ha (energy, Newton); 5e-9 (RKS), 5e-10 (UKS, ROKS) Ha/Bohr | mgga_response_vs_pyscf.rs, gen_mgga_response.py, mbd_scf_gradient.rs, mbd_scf_gradient_uks.rs, mbd_scf_gradient_roks.rs, unit tests in fxc.rs |
| COSX exchange, dense-grid limit | water / cc-pVDZ | direct K | 3.3e-7 | — | cosx_k_anchors.rs; see SCF: choosing how exchange is built |
| COSX SCF energy, (50,110)+fit | water / cc-pVDZ | direct K | 4.9e-6 Ha | — | ″ |
| COSX SCF energy, (50,110)+fit | butane / def2-SVP | direct K | 1.7e-4 Ha | — | ″ |
| COSX SCF energy, (50,110)+fit | butane / def2-TZVP | direct K | 1.2e-4 Ha | — | ″ |
| COSX, open shell | CH3 doublet / cc-pVDZ | direct K | 1.96e-5 Ha (UHF), 1.97e-5 Ha (ROHF) | — | k_builder_open_shell.rs |
| COSX analytic gradient (RHF, RKS, UHF; default overlap fit for RHF/UHF) | water / STO-3G, 6-31G; HO2 / STO-3G | finite differences of the COSX energy | 1.6e-9 to 4.3e-9 Ha/Bohr | 1e-6 / 3e-8 Ha/Bohr | cosx_gradient.rs |
COSX analytic gradient, open shell (UKS-B3LYP, ROKS-B3LYP, ROHF; fit off; flat (50,110) and pruned sgx (35,194) grids) | HO2 / STO-3G, NH2 / STO-3G | central FD of the COSX energy on the same grid (exchange-isolated against the direct-K run) | 1.7e-10 to 2.0e-9 Ha/Bohr; the exact-K gradient paired with the COSX energy misses by 6.1e-6 to 2.4e-4 | 1e-7 Ha/Bohr | cosx_gradient_open_shell.rs |
| MBD@rsSCS gradient on a UKS-PBE0 reference with COSX exchange (fit off) | NH2 / STO-3G | central FD of the full UKS(COSX) + MBD pipeline | 2.3e-11 Ha/Bohr; the exact-K Z-vector contraction misses by 4.6e-10 | 1e-10 Ha/Bohr | mbd_scf_gradient_uks.rs |
| COSX exchange against PySCF and against exact K: fit off with no screening on the identical grid; angular-grid convergence (75,110)→(75,590) with production knobs; analytic gradient with fit on (RHF) and off (B3LYP) | water / aug-cc-pVDZ (RHF, B3LYP); butane / def2-SVP (RHF); distorted water, NH3 for gradients | PySCF sgx on ferric's grid recipe (exact J, fit off, no screening); ferric exact-K energy; central FD of the COSX energy | same grid ≤ 1.2e-12 Ha at every grid; error shrinks 93x (butane) to 2576x (water) from 110 to 590 points; exact-K vs PySCF ≤ 2.1e-11 Ha; gradient ≤ 1.6e-8 Ha/Bohr | 1e-10 Ha; ≥ 30x; 1e-9 Ha; 2e-7 Ha/Bohr | validation_cosx.rs, gen_cosx.py |
| RIJCOSX energy (RI-J + COSX K): separation into RI-J error + COSX error | water / 6-31G (HF, B3LYP); HO2 / 6-31G (UHF) | the three other corners of (exact or RI) J × (exact or COSX) K, same grid | second-order cross term, 2.8–5.8 × e_cosx·e_rij (7.8e-10 Ha at (50,110), 2.3e-11 Ha at (75,302)) | 30 × e_cosx·e_rij, or 1e-10 Ha | cosx_rijcosx.rs |
| RIJCOSX analytic gradient (RI-J derivative + COSX derivative; flat and pruned grids; fit on and off) | water / STO-3G (RHF, B3LYP); HO2 / STO-3G (UHF) | central FD of the RIJCOSX energy | 1.7e-9 Ha/Bohr (RHF), 4.9e-10 (B3LYP, exchange-isolated), 3.6e-9 (UHF) | 1e-7 / 3e-8 Ha/Bohr | ″ |
Pruned COSX grid (prune = "sgx", peaks 194 and 302) against PySCF SGX with sgx_prune on the same radial grid and Becke partition, fit off, no screening | water / aug-cc-pVDZ; butane / def2-SVP (RHF) | PySCF 2.13.1 | ≤ 1.8e-12 Ha, identical point counts | 1e-10 Ha | validation_cosx.rs, gen_cosx_pruned.py |
| COSX final-grid pass | water / STO-3G, 6-31G; HO2 / STO-3G | the SCF-grid energy (final grid = SCF grid); SCF converged on the final grid | bit-identical; within 1.5e-7 Ha on eight molecules (site/src/methods/scf.md) | exact; — | cosx_rijcosx.rs, cosx_grid_sweep.py |
| Lebedev 194-point rule | unit sphere | exact monomial integrals; PySCF's rule | exact through degree 23; nodes and weights identical to PySCF's | 1e-12 relative | lebedev.rs |
| Periodic closed-shell range-separated hybrids at the Gamma point (HSE06, CAM-B3LYP; energy only), dense AFT and RS-GDF | H2 / STO-3G in a 4 Bohr cube and a triclinic cell, exxdiv ewald and none | PySCF 2.13.1 Gamma pbc.dft.RKS + AFTDF, UniformGrids 41³, cell.precision 1e-11 (8 pins) | ≤ 6.2e-13 Ha vs PySCF; Gamma = 1×1×1 k-mesh to 2.7e-15; RS-GDF − dense equals the plain-Coulomb fit error × 1.00–1.03; the attenuated-K fit as a matrix: 4.4e-14 / 2.1e-13 (cc-pVDZ-RI / def2-universal-jkfit) | 1e-10; 1e-10; 0.4 × fit error; 1e-11. Mutants moved E by ≥ 3.1e-4 (ω ×1.01, attenuated K dropped, c_sr/c_lr swapped, c_sr ×0.99, wrong exxdiv); a 1e-3 relative ω error in the fit alone moves the K matrix by 7.1e-6. Limits: the attenuated K is the k = 0 point of the k-point exchange, so the 1×1×1 agreement is not independent of the kernel (PySCF is); at a = 4 Bohr HSE06's erf piece is empty (ω = 0.11 < the smallest G), so only CAM-B3LYP exercises the attenuated fit; no range_split, and the nuclear force and the stress are the dense-AFT validation rows below (closed-shell force and stress, also reachable from the CLI and Python, then the open-shell UKS and ROKS force rows and the open-shell stress row) and, on k-point RS-GDF J/K, the closed-shell k-point RS-GDF row after them (the Gamma routes on an RS-GDF exchange have their own Gamma RS-GDF row); closed shell only (open-shell UKS is the next row); fixtures are H2/STO-3G only | pbc_grsh.rs, gen_pyscf_pbc_grsh_refs.py |
| Periodic closed-shell range-separated hybrids on a k-point mesh (HSE06, CAM-B3LYP, wB97X; energy only), dense AFT and RS-GDF | H2 / STO-3G, cubic and triclinic cells, meshes up to 2×2×2, exxdiv ewald and none | PySCF 2.13 pbc.dft.KRKS + AFTDF; k-mesh against the explicit supercell | ≤ 5e-13 Ha vs the PySCF pins; k-mesh = supercell / N to 4.4e-14 | 1e-10; 1e-8 (RS-GDF) | pbc_krsh_more.rs, pbc_krsh_rsgdf.rs, validation_pbc_krsh_rsgdf.rs |
Periodic closed-shell range-separated hybrid nuclear forces (HSE06, CAM-B3LYP; dense-AFT J/K; kxcgrad::kpoint_rks_gradient on a k-mesh, gamma_rsh::gamma_rks_rsh_gradient at the Gamma point through the 1×1×1 mesh) | H2 / STO-3G in a 4 Bohr cube, meshes 1×1×1 and 1×1×3 (non-TRIM), exxdiv ewald and none, an explicit 3-cell supercell | central FD of ferric's own energy (fresh SCF per displacement, h = 1e-4); c_sr = c_lr = c against the global hybrid with fraction c (two SCFs, zero XC); 1×1×3 mesh against the explicit supercell; Gamma against the 1×1×1 mesh. No PySCF force pin | analytic − FD ≤ 2.9e-9 Ha/Bohr (the h² floor: 1.16e-8 → 2.89e-9 → 7.14e-10 for h = 2e-4 → 1e-4 → 5e-5); ewald − none force ≤ 7.6e-13; equal fractions − global hybrid 5.6e-17; mesh − supercell ≤ 1.1e-14; Gamma − 1×1×1 ≤ 1.1e-16 | 1e-7 (FD); 1e-9 (anchors). Mutants (1×1×3 ewald, CAM-B3LYP / HSE06) miss the FD-validated force by 1.48e-1 / 1.64e-1 (kernel factor dropped), 1.01e-1 / 9.0e-2 (factor on J too), 3.0e-2 / 5.4e-2 (c_sr/c_lr swapped), 3.4e-2 / 5.3e-2 (plain instead of mixed Madelung in the M-term), 5.2e-4 / 1.45e-5 (ω × 1.01), 3.8e-2 / 1.2e-4 (erf piece dropped), 2.8e-2 / 5.1e-3 (Madelung M-term dropped). Limits: the largest erf weight on the 1×1×3 K set is 0.245 for CAM-B3LYP and 8.7e-4 for HSE06, but HSE06's derivative still resolves its erf piece (dropping it misses by 1.2e-4, 1200 FD bars; ω × 1.01 by 145 bars); wB97X is accepted but not exercised by a force test by the library (the Python and CLI derivative gate refuses it by name); the Gamma check is glue only (the kernel is shared with the k code); the stress is the next row, the open-shell UKS and ROKS forces the two rows after it, closed-shell RS-GDF J/K the row after those; H/STO-3G fixtures only (no p or d shells). Bindings (#498): the CLI task = "optimize" (k-mesh HSE06, Gamma CAM-B3LYP) logs the returned geometry and gradient in run_end, equal to the library at that geometry bit for bit (energy and force); Python run_rks_kpts / run_rks_gamma with_gradient vs the same library numbers: energy 0, force ≤ 5.6e-17 (bar 1e-12); Python k HSE06 force vs FD of its own energies 2.8e-9 (bar 2e-8); Python Gamma vs 1×1×1 force 1.1e-16, stress 1.7e-17 (bar 1e-10) | periodic_rsh_open_mgga_derivs.rs, test_pbc_rsh_open_mgga_derivs.py, pbc_krsh_force.rs, validation_krsh_force.rs, validation_pbc_kxcgrad_hybrid.rs |
Periodic closed-shell range-separated hybrid stress (HSE06, CAM-B3LYP; dense-AFT J/K; kxcstress::kpoint_rks_stress on a k-mesh, gamma_rsh::gamma_rks_rsh_stress at the Gamma point through the 1×1×1 mesh) | H2 / STO-3G in a 4 Bohr cube, meshes 1×1×1 and 1×1×3 (non-TRIM), exxdiv ewald and none, an explicit 3-cell supercell; the Madelung strains also on a triclinic lattice | central FD of ferric's own energy under each of the nine strain components (fresh SCF per strained cell, fixed fractional coordinates, h = 2.5e-5, shared grid cutoff); c_sr = c_lr = c against the global hybrid with fraction c (two SCFs, zero XC); 1×1×3 mesh against the explicit supercell; Gamma against the 1×1×1 mesh; ewald − none against −dv_M/dε from FD of the mixed Madelung constant on strained supercells (the density does not depend on the exxdiv, so E_ewald − E_none = −v_M per occupied orbital); dv_M^LR/dε and the mixed dv_M/dε against FD of the constants (prototyped in Python first). No PySCF pin (PySCF has no hybrid stress) | analytic − FD ≤ 5.6e-9 Ha per cell over all nine components, both functionals, both exxdiv (the worst is always ε_xx; every other component ≤ 1.3e-9); ε_zz residual 1.06e-8 → 2.40e-9 → 7.2e-10 for h = 1e-4 → 5e-5 → 2.5e-5 (h²); equal fractions − global hybrid 2.2e-16; mesh − supercell σ ≤ 3.5e-15; Gamma − 1×1×1 ≤ 7.8e-16; ewald − none vs −dv_M/dε 2.4e-9; Madelung strains vs FD 2.4e-9 (LR, h = 1e-4) and ≤ 6.3e-10 (mixed) | 3e-8 (FD); 1e-9 / 1e-10 (anchors); 1e-8 (exxdiv); 1e-7 (Madelung). Mutants (1×1×3 ewald, CAM-B3LYP / HSE06) miss the FD-validated stress by 1.17e-1 / 3.45e-3 (f′(K²) slope term dropped), 6.0e-2 / 5.8e-2 (kernel on J too), 1.03e-1 / 1.52e-1 (c_sr/c_lr swapped), 3.44e-1 / 5.94e-1 (plain instead of mixed Madelung strain), 2.92e-1 / 5.10e-3 (v_M^LR strain dropped); the primitive-cell Madelung strain misses the supercell by 6.2e-3. Limits: ε_xx has a discrete energy step at h ≥ 1e-4 (2.1e-5 at h = 2e-4) from the atom grid's hard neighbour_cutoff (#495), which the global hybrid PBE0 shows identically, so FD runs at 2.5e-5 (where the ε_xx residual is FD-floor noise, not h²); wB97X is accepted but not exercised by the library (the Python and CLI derivative gate refuses it by name); open shell (UKS, ROKS) is the open-shell RSH stress row four rows below (closed-shell RS-GDF J/K: three rows below); bindings (#498): the CLI [cell] stress = true HSE06 tensor (H2, 1×1×2 Gamma-centred and Monkhorst-Pack) equals the library bit for bit in periodic_kpoint_stress.rs, Python with_stress runs the same library call; H/STO-3G fixtures only (no p or d shells); the Gamma check is glue only (the kernel is shared with the k code) | pbc_krsh_stress.rs, validation_krsh_stress.rs |
Periodic open-shell (UKS) range-separated hybrid nuclear forces (HSE06, CAM-B3LYP; dense-AFT J/K; kuxcgrad::kpoint_uks_gradient on a k-mesh, gamma_rsh::gamma_uks_rsh_gradient at the Gamma point through the 1×1×1 mesh): K_σ = K[D_σ] with exchange scale 1, both spin exchange walks weighted by the kernel factor, J bare, the mixed Madelung constant in the per-spin M-term Σ_σ D_σ S D_σ | H3 doublet and H4 triplet / STO-3G in the triclinic cell of the UKS energy row (polarized), meshes 1×1×1 and 1×1×2, exxdiv ewald and none; H2 singlet (4 Bohr cube, 1×1×3); an explicit 2-cell supercell (multiplicity 3) | singlet k-UKS against the k-RKS RSH force (separate SCFs, per part); central FD of ferric's own polarized energy (fresh SCF per displacement, every component of both cells, both functionals, h sweep); c_sr = c_lr = c against the global-hybrid UKS force with fraction c (two SCFs, zero XC, polarized H3); 1×1×2 mesh against the supercell; Gamma against the 1×1×1 mesh; ewald against none. No PySCF force pin | singlet UKS − RKS ≤ 2.3e-13 per part; analytic − FD ≤ 3.8e-9 (H3) / 4.7e-9 (H4 triplet) Ha/Bohr at h = 1e-4 (CAM-B3LYP H3 (0,2): 1.50e-8 → 3.82e-9 → 9.16e-10 for h = 2e-4 → 1e-4 → 5e-5, h²); equal fractions − global hybrid 1.7e-16; mesh − supercell ≤ 7.2e-15; Gamma − 1×1×1 ≤ 8.5e-14; ewald − none ≤ 5.6e-14 | 1e-7 (FD); 1e-9 (anchors). Mutants on the polarized H3 doublet (1×1×2 ewald, CAM-B3LYP / HSE06) miss the FD-validated force by 2.6e-3 / 2.8e-3 (α density in the β exchange walk), 2.0e-2 / 1.6e-2 (β exchange dropped), 1.2e-2 / 3.9e-3 (M-term on the total density), 2.7e-2 / 1.1e-2 (α K in the β Fock), 1.3e-1 / 1.2e-1 (kernel factor on J), 4.3e-2 / 7.2e-2 (plain instead of mixed Madelung), 3.6e-2 / 3.3e-2 (c_sr/c_lr swapped), 9.6e-2 / 1.0e-1 (kernel factor dropped), 1.3e-2 / 2.7e-6 (erf piece dropped), 2.7e-4 / 5.1e-7 (ω × 1.01); the first four (spin bookkeeping) are 0 on the singlet. Limits: the equal-fraction anchor shares the M-term density sandwich Σ_σ D_σ S D_σ with its global-hybrid side, so it cannot see an M-term spin error (only FD does: a cross-spin M-term mutant misses FD by 2.3e-2); the largest erf weight on the H3 1×1×2 K set is 0.172 for CAM-B3LYP and 3.6e-5 for HSE06, so HSE06's erf and ω defects are resolved only at 27 and 5 FD bars on this cell (CAM-B3LYP carries the erf assertion); wB97X accepted, not force-tested; RS-GDF is the open-shell RS-GDF row below; the open-shell stress is its own row below (ROKS force: the next row); the bindings of all of them are the open-shell RSH bindings row below; bindings (#498): the CLI task = "optimize" (k-mesh HSE06 H3 1×1×2, Gamma CAM-B3LYP H3) logs the returned geometry and gradient in run_end, equal to the library at that geometry bit for bit (energy and force; PBE0 / HSE06 siblings differ by 4.2e-4 / 3.6e-3 Ha/Bohr); Python run_uks_kpts / run_uks_gamma with_gradient vs the same library numbers: energy ≤ 8.9e-16, force ≤ 1.0e-15 (bar 1e-12); Python k HSE06 UKS force vs FD of its own energies 4.1e-10 (bar 2e-8); Python Gamma vs 1×1×1 UKS CAM-B3LYP force 3.7e-12 (bar 1e-10); the binding gate (kxcgrad::check_deriv_functional with Deriv::Force / Deriv::Stress, dft::check_gamma_uks_request) refuses by name wB97X (the UKS stress, the UKS force on RS-GDF and the ROKS range-separated derivatives refused here at the time are wired since, the open-shell RSH bindings row below) (periodic_rsh_open_mgga_derivs.rs, test_pbc_rsh_open_mgga_derivs.py); H/STO-3G fixtures only (no p or d shells) | pbc_kuks_rsh_force.rs, validation_kuks_rsh_force.rs, validation_periodic_rsh_open_mgga_derivs.rs, validation_pbc_kuxcgrad.rs |
Periodic restricted open-shell (ROKS) range-separated hybrid nuclear forces (HSE06, CAM-B3LYP; dense-AFT J/K; kuxcgrad::kpoint_roks_gradient on a k-mesh, gamma_rsh::gamma_roks_rsh_gradient at the Gamma point through the 1×1×1 mesh): the UKS RSH assembly of the row above on the ROHF spin densities, with W_RO and the ROHF orbital-gradient gate; the routed Gamma force's Fock guard compares the rebuilt spin Focks with the Gamma result's stored spin Focks (rohf_spin_focks; the Roothaan F_eff only as a fallback for a result without them) | H3 doublet and H4 triplet / STO-3G in the triclinic cell of the UKS row, meshes 1×1×1 and 1×1×2, exxdiv ewald and none; H2 singlet (4 Bohr cube, 1×1×3); an explicit 2-cell supercell (high-spin triplet) | singlet k-ROKS against the k-RKS RSH force (separate SCFs, per part); central FD of ferric's own ROKS energy (fresh SCF per displacement, every component of both cells, both functionals, h sweep; PySCF has no periodic ROKS gradient); c_sr = c_lr = c against the global-hybrid ROKS force with fraction c (two zero-XC ROKS SCFs); 1×1×2 mesh against the supercell; Gamma against the 1×1×1 mesh; ewald against none | singlet ROKS − RKS ≤ 1.2e-13 per part; analytic − FD ≤ 4.5e-9 (H3) / 4.6e-9 (H4 triplet) Ha/Bohr at h = 1e-4 (CAM-B3LYP H3 (0,2): 1.79e-8 → 4.51e-9 → 1.11e-9 for h = 2e-4 → 1e-4 → 5e-5, h²); equal fractions − global hybrid ≤ 3.4e-16; mesh − supercell ≤ 4.2e-15; Gamma − 1×1×1 ≤ 5.1e-15 (spin-Fock guard ≤ 1.0e-15); the Gamma guard refuses another grid, exxdiv or functional and an equal-and-opposite spin-Fock tamper (whose diagonal-block part F_eff cannot see); the F_eff fallback gives the identical force; ewald − none ≤ 5.8e-16 | 1e-7 (FD); 1e-9 (anchors). Mutants on the H3 doublet (1×1×2 ewald, CAM-B3LYP / HSE06) miss the FD-validated force by 3.1e-3 / 2.6e-3 (α density in the β exchange walk), 2.2e-2 / 1.8e-2 (β exchange dropped), 1.2e-2 / 3.8e-3 (M-term on the total density), 2.8e-2 / 1.2e-2 (α K in the β Fock), 1.3e-1 / 1.2e-1 (kernel factor on J), 4.4e-2 / 7.3e-2 (plain Madelung), 3.7e-2 / 3.5e-2 (c_sr/c_lr swapped), 9.9e-2 / 1.0e-1 (kernel factor dropped), 1.4e-2 / 2.8e-6 (erf piece dropped), 2.8e-4 / 5.3e-7 (ω × 1.01), 8.5e-2 / 7.2e-2 (W_RO cross term dropped); the first four are 0 on the singlet. The unrestricted W = Σ_σ D_σ F_σ D_σ is an identity at convergence (1.1e-14, H4 triplet); on a loose H4 triplet state (orbital gradient 3.6e-5) the gate refuses it; with the gate off neither form is right there (both miss the converged force, by 1.0e-5 and 1.5e-5) and the two differ by 4.9e-6, so the gate's refusal is the protection. Limits: the H3 doublet is inversion-symmetric, so its closed-open block (and the W_RO − unrestricted-W difference) vanishes for any state, hence the H4 triplet for that test; the equal-fraction anchor shares the M-term sandwich and W_RO with its global-hybrid side (spin errors in them are seen by FD only); HSE06's erf and ω defects are resolved at only 28 and 5 FD bars on this cell (asserted instead on the singlet, 1.2e-4 / 1.5e-5; CAM-B3LYP carries the open-shell erf assertion); wB97X accepted, not force-tested; RS-GDF is the open-shell RS-GDF row below; the open-shell stress is its own row below; a Gamma ROKS on a caller-supplied grid is not covered; wired to Python and the CLI by #498 (the open-shell RSH bindings row below); H/STO-3G fixtures only | pbc_kroks_rsh_force.rs, validation_kroks_rsh_force.rs |
Periodic closed-shell range-separated hybrid nuclear forces and stress on k-point RS-GDF J/K (HSE06, CAM-B3LYP; kxcgrad::kpoint_rks_gradient and kxcstress::kpoint_rks_stress on a KRsGdf::build_rsh source). The plain fit's exchange pieces are scaled by c_sr, the attenuated fit of erf(ωr)/r is differentiated by the plain fit's LR and metric passes at ω on |K| ≤ 2ω√ln(1/ε) (no SR walks, no c0 G = 0 terms; a q class the build left empty contributes zero) scaled by c_lr − c_sr, and the M-term and Madelung strain carry the SCF's mixed constant (prototyped in Python first) | H2 / STO-3G with an even-tempered s,p aux on the atoms, 1×1×2 mesh, RS-GDF split ω₀ = 1: a triclinic cell (CAM-type ω = 0.33) and a 6.5 Bohr cube (HSE-type ω = 0.11, where q = 0 carries K vectors); KS rows in a 4 Bohr cube | exactness anchor: the attenuated fit at ω = 1e7 (cut 24) against the plain fit, fixed density; the attenuated fit alone against central FD of its fixed-density energy; the zero-XC range-separated exchange energy (no grid) and the KS energy, central FD with fresh build and SCF per displacement (forces) and per strained cell (nine stress components); RS-GDF against the dense-AFT RSH force on the same cell, scaled by the plain-Coulomb (PBE0) fit gap. No PySCF force pin | anchor: energy 1.4e-15, force 6.6e-14; attenuated alone (lindep 1e-6) 5.8e-9 / 5.2e-11 / 9.0e-12 at h = 1e-3 / 1e-4 / 1e-5; zero-XC force 1.11e-8 / 2.78e-9 / 6.6e-10 (CAM-type) and 9.7e-9 / 2.44e-9 / 6.25e-10 (HSE-type) at h = 2e-4 / 1e-4 / 5e-5 (h²); KS force 2.15e-9 / 5.8e-10 at h = 1e-4 / 5e-5 (both functionals, the same as dense AFT); zero-XC stress 1.26e-8 / 3.2e-9 (CAM-type) and 8.1e-9 / 2.1e-9 (HSE-type) at h = 1e-4 / 5e-5; Σ F ≤ 1.2e-15; RS-GDF − dense force 1.60e-5 to 1.79e-5, within 5% of the PBE0 gap of the same aux and mesh | 1e-11 (anchor); 1e-9 (attenuated alone, h = 1e-4); 1e-8 (zero-XC force, h = 5e-5) and 2e-8 (KS force); 3e-8 (stress); 1.5 × the PBE0 gap. Mutants (CAM-type / HSE-type) miss the FD-validated force by 2.8e-2 / 6.1e-2 (c0 kept in the attenuated derivative), 2.5e-2 / 8.4e-4 (c_lr − c_sr dropped), 4.8e-2 / 1.3e-2 (c_sr v_M only), 2.2e-2 / 1.6e-2 (c_lr v_M), 6.3e-2 / 1.2e-2 (mixed v_M scaled again by c_sr), 7.6e-2 / 9.9e-2 (plain v_M), 7.7e-2 / 1.6e-2 (no Madelung M-term), and the stress by 1.3e-1 / 1.6e-2 (scale dropped), 1.1e-1 / 3.2e-3 (attenuated strain dropped), 2.5e-1 / 2.5e-1 (plain Madelung strain), 1.6e-1 / 4.2e-3 (LR Madelung strain dropped); a derivative at the Ewald split ω₀ is refused by the rebuilt-fit check. Conditioning: the fit derivative applies the metric inverse in its eigenbasis, so kept eigenvalues just above the default lindep 1e-10 do not degrade the force: at fixed density a rigid translation of 1e-3 moves the CAM-type attenuated force by 3.6e-11 (lindep 1e-10; the energy by ≤ 1.1e-16), the attenuated fit alone matches FD at 5.8e-9 / 6.8e-11 / 7.8e-12 for h = 1e-3 / 1e-4 / 1e-5 at lindep 1e-10, the CAM-type zero-XC force at lindep 1e-10 shows the same h² truncation as at 1e-6 (1.11e-8 / 2.74e-9 / 6.2e-10), and with the #512 control (a near-duplicate aux shell whose 1.4e-11 eigenvalue is dropped at 1e-10) the attenuated fit's translation drift falls from 2.1e-8 to 4.9e-11 and its flat 3.9e-8 FD miss becomes the h² truncation (5.7e-9 / 1.9e-11 at h = 1e-3 / 1e-4), while its exchange-only plain fit was already clean (4.8e-14; the report's ~4e-6 plain-fit miss is not reproduced on that path). The ≤ 1e-10 translation target of #512 is not met when a near-duplicate eigenvalue is kept just above the cut (1.3e-10): the plain and attenuated forces still move by 1.29e-10 / 1.79e-10 (per commit, bar 2e-9; 9.2e-7 / 1.05e-7 with the explicit metric inverse). The energy itself moves by 5.4e-11 under that translation and FD of the plain fit grows as 1/h (6.1e-8 / 5.5e-7 / 4.9e-6 at h = 1e-3 / 1e-4 / 1e-5), so the residual is attributed, provisionally, to integral roundoff in the near-singular fit (shown only in an exact-integral Python model). The HSE06 KS row on the 4 Bohr cube is wiring only (erf weights ~3e-6, q = 0 empty). The atom grid adds FD steps on the triclinic and 6.5 Bohr cells that are identical on dense AFT, hence the zero-XC FD rows; the Gamma route (gamma_rks_rsh_gradient) differentiates an RS-GDF exchange too (the Gamma RS-GDF row below); open shell (the UKS and ROKS forces on RS-GDF) is the next row (the open-shell stress is the row after it; both wired, the open-shell RSH bindings row); bindings (#498): the CLI task = "optimize" CAM-B3LYP force (H2, 1×1×2, cc-pVDZ-RI) and [cell] stress = true CAM-B3LYP tensor equal the library bit for bit (B3LYP sibling on the same fit 5.7e-4 Ha/Bohr force, 5.7e-4 stress; the dense-AFT CAM-B3LYP stress differs by 2.8e-6), Python run_rks_kpts(jk="rsgdf") with_gradient / with_stress match the CLI values (differences 0 on the measuring machine, not a cross-platform guarantee: the Python test bar for the RS-GDF force and stress is 1e-6 because a different OpenBLAS kernel moves them by up to 1.25e-8 and 1.65e-9; the energy and the dense rows keep 1e-12); the RS-GDF range-separated stress is roundoff-limited: a ~1e-17 Å change in the xyz digits of the same geometry moves σ_zz by 2.8e-9, so the Python anchor reads the same xyz text as the CLI run (periodic_kpoint_stress.rs, test_pbc_rsh_open_mgga_derivs.py); H/STO-3G fixtures only | pbc_krsh_rsgdf_force.rs, pbc_kfit_force_conditioning.rs, validation_krsh_rsgdf_force.rs, rsgdf_rsh_force_proto.md |
Periodic open-shell (UKS, ROKS) range-separated hybrid nuclear forces on k-point RS-GDF J/K (HSE06, CAM-B3LYP; kuxcgrad::kpoint_uks_gradient / kpoint_roks_gradient on a KRsGdf::build_rsh source). A composition of the closed-shell RS-GDF RSH force (plain fit × c_sr, attenuated fit × (c_lr − c_sr), mixed M-term) with the open-shell exchange terms (1, D_α), (1, D_β) (twice the closed-shell (½, D) per spin); no new derivative | H3 doublet / STO-3G, ET-sp aux, 1×1×2: triclinic TRI_A (CAM-type ω = 0.33, CAM-B3LYP, HSE06) and a 6.5 Bohr cube (HSE-type ω = 0.11); H2 singlet TRI2 | Singlet UKS ≡ #514's RKS RS-GDF force (separate SCFs) per part; c_sr = c_lr ≡ the global hybrid on the plain fit; central FD of ferric's own polarized energy (fresh build + SCF per point); RS-GDF − dense-AFT UKS force gap vs the PBE0 (plain-fit) gap. No PySCF oracle: PySCF 2.13 periodic KS gradients are FFTDF-only | Singlet: exactly 0 (bitwise D_σ = D/2; mutants move it 3.1e-2). Equal fractions: ≤ 3.6e-14 (UKS, ROKS; ewald and none). Zero-XC FD, all 9 components: 9.7e-10 / 1.0e-9 (CAM-type UKS / ROKS), 7.1e-10 (HSE-type) at h = 5e-5, 3.9× per halving. KS FD (3 components): ≤ 1.1e-9 at h = 5e-5. Gap: CAM-B3LYP 1.26e-5, HSE06 1.08e-5 vs PBE0 1.08e-5 | 1e-10; 1e-10; 1e-8 (zero XC) / 2e-8 (KS) at h = 5e-5 with a 3–6 h² ratio; 1.5 × the PBE0 gap. Mutants (spin factor, dropped / swapped β term, total-density M-term, Pulay K swap, attenuated scale and c0, Madelung constants, ROKS W cross term) miss by 3.5e-4 – 4.3e-2; source mutants of KSpins::exch_terms (½ per spin, β dropped) fail singlet and FD. Limits: HSE06's attenuated piece is nearly dead on TRI_A (2.7e-6), covered by the HSE-type cube rows; the open-shell stress is the next row; Python / CLI wire the RS-GDF open-shell RSH force (#498, the open-shell RSH bindings row); H / STO-3G only | pbc_kuks_rsh_rsgdf_force.rs, validation_kuks_rsh_rsgdf_force.rs |
Periodic open-shell (UKS, ROKS) range-separated hybrid stress (HSE06, CAM-B3LYP; dense-AFT and k-point RS-GDF J/K; kuxcstress::kpoint_uks_stress / kpoint_roks_stress on a k-mesh, gamma_rsh::gamma_uks_rsh_stress at the Gamma point through the 1×1×1 mesh). A composition, no new derivative algebra: the open-shell stress assembly (exchange terms (1, D_α), (1, D_β), M-term Σ_σ D_σ S D_σ, W or W_RO) with the closed-shell RSH stress's attenuated kernel slope (dense), the plain fit × c_sr plus the attenuated fit's LR strain × (c_lr − c_sr) (RS-GDF), and the mixed Madelung constant and strain; exchange scale 1 | H3 doublet / STO-3G in the triclinic TRI_A (polarized) and a 6.5 Bohr cube (HSE-type ω = 0.11), H2 singlet in TRI_A; 1×1×2 mesh, RS-GDF ET-sp aux with ω₀ = 1 and lindep 1e-6 (dense on the same basis, mesh and hcore); exxdiv ewald and none; an explicit 2-cell supercell (triplet); KS on an SSF (20, 50) atom grid with an explicit neighbour cutoff | singlet UKS against the RKS RSH stress (independent SCFs and entries, per part; zero XC and KS); c_sr = c_lr = c against the global-hybrid open-shell stress with fraction c (zero XC, polarized, UKS and ROKS, both J/K, both exxdiv); central FD of ferric's own polarized energy under strain (fresh build and SCF per strained cell, h = 1e-4 and 5e-5 and the Richardson combination (4 FD(h/2) − FD(h))/3): zero XC on all nine components (CAM-type ω = 0.33, both J/K, UKS and ROKS, ewald; UKS also under none) and three components of the HSE-type cube, and the KS HSE06 / CAM-B3LYP energy on three components (UKS and ROKS, both J/K); Gamma against the 1×1×1 mesh; 1×1×2 mesh against the supercell. No PySCF pin: PySCF 2.13.1 pbc/grad/krks_stress.py / kuks_stress.py raise NotImplementedError for hybrids, and it has no RS-GDF derivative | singlet UKS − RKS: exactly 0 per part (zero XC, both J/K; audited: the singlet UKS SCF lands on D_σ = D/2 and the two term lists differ by power-of-two scalings), KS ≤ 2.8e-16 (dense) and ≤ 1.2e-12 (RS-GDF ROKS CAM-B3LYP); equal fractions − global hybrid ≤ 4.8e-14; zero-XC FD all nine: worst 3.07e-8 / 7.0e-9 at h = 1e-4 / 5e-5 (the ε_zz row, ratio 4.4, h²), Richardson ≤ 8.9e-10 (dense) and ≤ 4.1e-10 (RS-GDF); HSE-type cube Richardson ≤ 3.2e-10 with the attenuated piece 4.5e-3 – 5.0e-3 (resolved ~1e7×); KS FD ≤ 2.7e-8 / 6.2e-9 at h = 1e-4 / 5e-5, Richardson ≤ 8.6e-10; Gamma − 1×1×1 ≤ 3.2e-15; mesh − supercell/2 ≤ 8.3e-14 | 1e-10 (anchors); 1e-6 at h = 1e-4 and 2e-8 Richardson (#553's stress bars); 1e-9 (Gamma, supercell). Mutation ledger on the polarized CAM-type cell, UKS and ROKS, dense and RS-GDF: β exchange term dropped 6.5e-2 – 6.6e-2, closed-shell factor ½ per spin 1.5e-1 – 1.6e-1, M-term on the total density 6.0e-2, α K in the β Fock 3.3e-1, f′ slope dropped 1.1e-1 (dense), kernel on J 2.6e-1 (dense), c_sr/c_lr swapped 1.4e-1 – 1.7e-1, plain Madelung strain 3.7e-1, v_M^LR strain dropped 2.5e-1, attenuated fit strain dropped 1.9e-1 (RS-GDF), its scale dropped 2.2e-1 (RS-GDF), W_RO cross term dropped 1.6e-1 (ROKS); smallest 6.0e-2 (≥ 3e6 Richardson bars). On the singlet the β-dropped and ½-factor defects move the stress by 7.5e-2 and the M-term spin defect by exactly 0; the equal-fraction anchor sees the spin defects (7.2e-2, 1.3e-1, 4.8e-2, 3.3e-1). Source mutants of KSpins::exch_terms (½ per spin; β dropped) fail the singlet and FD tests and pass the equal-fraction one (it shares the term list); a plain instead of mixed Madelung shift in the Fock rebuild is refused by the Fock guard in all 8 per-commit tests. Limits: H/STO-3G only (no p or d shells); the singlet anchor and the equal-fraction anchor share the term list / sandwich with their reference side, so FD is the spin-bookkeeping oracle; the HSE06 erf piece is nearly empty on TRI_A, hence the HSE-type cube rows; KS FD on three components (the atom grid's hard neighbour cutoff can step under strain, #495; none seen on these); wB97X accepted, not exercised; the Gamma ROKS RSH stress has no routed entry (kpoint_roks_stress on a 1×1×1 mesh); the Gamma routes on an RS-GDF exchange are the Gamma RS-GDF row below; wired to Python (k-point and Gamma UKS) and the CLI (k-point) by #498, the open-shell RSH bindings row | pbc_kuks_rsh_stress.rs, validation_kuks_rsh_stress.rs, validation_pbc_kuxcstress_ro_rsgdf.rs |
Periodic Gamma-point range-separated hybrid nuclear forces (RKS, UKS, ROKS) and stress (RKS, UKS) on RS-GDF J/K (CAM-B3LYP; gamma_rsh::gamma_rks_rsh_gradient / gamma_uks_rsh_gradient / gamma_roks_rsh_gradient / gamma_rks_rsh_stress / gamma_uks_rsh_stress handed a GammaRshExchange::build_rsgdf exchange). A routing change, no new derivative: the Gamma result goes to the 1x1x1 k-point force / stress with the k = 0 KRsGdf::build_rsh fit the SCF's exchange ran on (its config and aux are now kept with it); the routed Fock guard requires the Gamma J to be that fit's J | H2 (closed shell) and H3 doublet (open shell) / STO-3G in TRI_A, ET-sp aux, RS-GDF split ω₀ = 1, metric cut 1e-6, SSF (20, 50) atom grid | Gamma == an INDEPENDENT 1x1x1 k SCF + force / stress (own hcore, J, grid, SCF); central FD of ferric's own Gamma RS-GDF energy (fresh fits + SCF per point, forces and strained cells); Gamma RS-GDF − Gamma dense-AFT RSH force against the PBE0 1x1x1 RS-GDF − dense force gap; a dense Gamma J under an RS-GDF exchange (and the reverse) must be refused. No PySCF oracle (no RS-GDF derivatives, no hybrid stress) | Gamma − 1x1x1: force 4.4e-16 (RKS), 1.9e-14 (UKS), 1.1e-14 (ROKS) under ewald, 0 / 1.0e-13 / 1.2e-14 under none; dE/dε 1.8e-15 / 0 (RKS), 1.4e-14 (UKS, H3 doublet, ewald); the routed Fock guard ≤ 5.6e-16 (the two SCFs reach the same density to roundoff on these cells, so this is a GLUE check). FD force (per commit, two components): RKS 2.9e-9 / 5.5e-11 at h = 1e-4; UKS / ROKS (three components) ≤ 3.9e-9 at h = 1e-4 → ≤ 7.5e-10 at 5e-5, Richardson ≤ 3.1e-10. FD stress (ε_zz, ε_xy, ε_yx): RKS (ewald and none) 7.0e-8 / 1.57e-8 at h = 4e-4 / 2e-4 (h²), Richardson ≤ 5.0e-9; UKS (ewald; | dE/dε_zz | ≈ 1.3) 4.0e-7 / 1.0e-7 on ε_zz (ratio 4.0), Richardson ≤ 1.6e-10; below h = 2e-4 the atom grid adds a ~7e-9 non-monotone floor that the dense-AFT Gamma route shows identically. Fit gap: CAM-B3LYP 1.77e-5 / 7.57e-5 / 7.58e-5 vs PBE0 1.71e-5 / 6.92e-5 / 6.93e-5 (ratios 1.03–1.09). Foreign J refused at a Fock mismatch of 4.5e-5–5.1e-5. An RS-GDF Gamma J on another fit than the exchange's (only the 1e-8 Fock guard ties them): metric cut 1e-8 is the same J (every plain-metric eigenvalue ≥ 5.3e-3; ΔE 0, force Δ 0, accepted), ω₀ = 0.9 is the same J to the integral precision (ΔE 6.7e-15, force Δ 2.8e-16, accepted), metric cut 1e-2 (drops 2 of 20; ΔE 1.5e-7, Fock Δ 1.26e-6) and an extra aux s shell (ΔE 4.1e-6, Fock Δ 5.4e-6) are refused; a fit change below the 1e-8 Fock bar would pass with a force error of that order (known limit, not constructed); an aux prepared on other centres is refused by the rebuilt-B check (‖B‖ mismatch 1.95e-4) |
Bindings of the open-shell range-separated hybrid derivatives and the Gamma RS-GDF ones (#498; HSE06, CAM-B3LYP): Python run_uks_kpts / run_roks_kpts with_gradient / with_stress and the CLI k-point task = "optimize" / [cell] stress = true for UKS and ROKS on dense-AFT and RS-GDF J/K; the Gamma ROKS force (gamma_rsh::gamma_roks_any_keep + gamma_roks_rsh_gradient; Python run_roks_gamma, CLI Gamma optimize) and the Gamma RKS / UKS force and stress on RS-GDF (Python run_rks_gamma / run_uks_gamma; the CLI Gamma optimize, no CLI Gamma stress by design); the Gamma UKS stress on either J/K (Python). The gate (kxcgrad::check_deriv_functional, dft::check_gamma_{rks,uks,roks}_request) accepts every validated RSH derivative and refuses by name wB97X and the Gamma ROKS stress (no library entry) | H3 doublet (UKS, ROKS) and H2 (RKS) / STO-3G, 4 Bohr cube, 1×1×2 Gamma-centred and the Gamma point, omega 0.8, XC 20×50, exxdiv ewald; RS-GDF cc-pVDZ-RI; a p-shell cell (STO-3G + one p shell, ζ = 1, 12 functions for H3, basis size asserted on both sides) for the k ROKS HSE06 force, the k UKS HSE06 stress and the k UKS CAM-B3LYP RS-GDF force; a three-step Gamma ROKS HSE06 optimization | the library at the same settings (CLI run_end record, bit for bit; Python against those CLI numbers, or for the Gamma UKS and RKS stress the library values printed by the validation file); central FD of the CLI's own energy (k ROKS HSE06 force, k UKS HSE06 σ_zz from a uniaxial strain); Python Gamma against its own 1×1×1 mesh | CLI = library bit for bit on all 15 CLI anchors (11 routes on H/STO-3G: k ROKS HSE06 / CAM-B3LYP-RS-GDF force, k UKS CAM-B3LYP RS-GDF force, Gamma ROKS CAM-B3LYP / HSE06-RS-GDF force, Gamma RKS HSE06 RS-GDF and UKS CAM-B3LYP RS-GDF force, k UKS HSE06 / CAM-B3LYP-RS-GDF and ROKS HSE06 / CAM-B3LYP-RS-GDF stress; 3 on the p-cell; the moved-geometry Gamma ROKS optimize); FD 6.6e-10 Ha/Bohr (force) and 7.5e-10 Ha/Bohr³ (σ_zz); Python − library on 7 force and 7 stress pins (Gamma RKS HSE06 RS-GDF stress included): energy ≤ 3.8e-15, dense force ≤ 2.3e-15, dense stress ≤ 7.5e-17, Gamma RS-GDF force ≤ 9.1e-15 / stress ≤ 2.4e-15, k RS-GDF force ≤ 3.8e-9 and stress ≤ 2.0e-10 (RS-GDF roundoff on the Python repr-Å vs the CLI 12-decimal xyz, as for the closed-shell RS-GDF row), Gamma − 1×1×1 1.2e-14 (ROKS force) / 1.7e-13 (UKS stress); siblings: HSE06 vs CAM-B3LYP 3.6e-3–5.2e-3 force, 7.6e-4–8.0e-4 stress; RS-GDF vs dense 6.0e-6–3.8e-5 force, 1.1e-7–3.7e-6 stress | bit-equal (CLI); 1e-7 (FD force), 1e-8 (FD stress); Python 1e-12 on every dense and Gamma pin, 1e-7 (k RS-GDF force) and 1e-8 (k RS-GDF stress), both below the RS-GDF − dense gaps; 1e-10 (Gamma − 1×1×1). Mutants (all killed): ROKS force handed a state with β = α density (Fock guard 1.27e-1), force × (1 + 1e-6) (FD alone: miss 3.7e-7), transposed or dropped CLI UKS stress, the Gamma ROKS route ignoring its kept exchange, the gate accepting every RSH (wB97X runs), the Python Gamma UKS / RKS stress transposed (2.7e-4 / 1.4e-4). Limits: the fixtures are H only (s and p shells; no d shells or heavy atoms for these routes, #561 covers those for the earlier ones); no Gamma ROKS RSH stress | periodic_open_rsh_derivs.rs, validation_periodic_open_rsh_derivs.rs, test_pbc_rsh_open_mgga_derivs.py |
| Periodic derivative BINDINGS with p, d and Li shells (CLI and Python vs the library): closed-shell RSH force (k-point HSE06, Gamma CAM-B3LYP), k-point RKS HSE06 stress, k-point UKS SCAN force and SCAN/r2SCAN stress, k-point UKS HSE06 force, Gamma UKS CAM-B3LYP force | H2 / H3 cells with STO-3G + one p shell (ζ = 1), the same + one pure d shell, LiH and the LiH2 doublet / STO-3G; dense AFT, exxdiv ewald | The library at the same settings (CLI and Python must be bit-equal), and a central FD of the code's own energy along a generic direction (force) or per strain component (stress) | CLI = library bit for bit on all 14 rows; FD misses 3.2e-11 to 2.0e-8 on p, d and Li (the H-cell floor); Python binding − CLI ≤ 1.6e-15 on the seven p-shell rows it compares (five force, two stress); the d-shell and Li Python tests are FD-only. Mutation: dropping the p lowering term of the x-derivative (pair_ft/residues.rs) misses FD by 8.5e-6 to 2.6e-4 (stress 6.3e-6 to 1.1e-4), which the s-only H anchors and the Gamma-vs-1×1×1 check cannot see | Bit-equal; FD 5e-8 (Rust per commit, Gamma RKS CAM-B3LYP force), Python force 1e-8, stress 1e-7. Limits: the per-commit gate is 2 bit-equal anchors and 1 FD anchor; the rest is the validation tier (~70 min CI) and the nightly Python job. No d shell on a heavy atom (dense-AFT cost) | periodic_heavy_anchors.rs, validation_periodic_heavy_anchors.rs, test_pbc_heavy_anchors.py |
Periodic open-shell k-point UKS with the pure meta-GGAs SCAN and r2SCAN (energy only; dense-AFT J/K; library solve_kuks, Python run_uks_kpts and the CLI UKS route) | H3 doublet (cubic, 4 Bohr) and H4 triplet (triclinic) / STO-3G, meshes 1×1×1 and 1×1×2 (singlet anchors also 1×1×3, 2×2×2) | PySCF 2.13.1 pbc.dft.KUKS + AFTDF, UniformGrids 61³, cell.precision 1e-12 (8 polarized pins); singlet k-UKS against k-RKS and gamma_rks; polarized k-mesh against the explicit supercell; V_σ against a finite difference of E_xc per spin | ≤ 7.4e-13 Ha vs PySCF; singlet k-UKS = k-RKS exactly (0 on every case), 1×1×1 = gamma_rks to 2.7e-15; k-mesh = supercell / N to 8e-14; V_σ vs FD ≤ 1.2e-8 relative | 1e-9; 1e-10; 1e-8; 2e-7 relative. Tau mutants (swapped τ_α/τ_β, dropped β V^τ, total τ in both channels, spin-summed evaluator) miss the pins by ≥ 2.0e-5 Ha (the energy is only second order in a dropped β V^τ); the FD check fails under swapped and dropped-β V^τ by ≥ 5.5e-4 relative. CLI: the UKS energy (H3 doublet, Gamma-centred and Monkhorst-Pack 1×1×2) equals solve_kuks to 1e-12 (measured 0). Limits: forces and stress are the "k-point UKS analytic forces and stress" row further down, k-ROKS in its own row below, RS-GDF in its own row below, no meta-GGA hybrids or TPSS; the Gamma-point UKS / ROKS meta-GGA energy is the next row (library only); no level shift was needed, but only on these 3 to 4 orbital minimal-basis cells; the singlet anchor shares chunk_tau between both sides | validation_pbc_kuks_mgga.rs, pbc_kuks_mgga.rs, gen_pyscf_kuks_mgga_refs.py, periodic_kpoint_open_shell.rs, validation_periodic_kpoint_open_shell.rs, validation_pbc_kuks.rs |
Periodic Gamma-point open-shell meta-GGA energy, UKS and ROKS, SCAN and r2SCAN (library dft::gamma_uks / rohf::gamma_roks, dense-AFT J; per-spin τ_σ from D_σ via ferric-dft's eval_tau_uks into the unchanged polarized kernel, wiring only). Wired to Python run_uks_gamma / run_roks_gamma and the CLI Gamma UKS / ROKS routes by #498 (dense-AFT J/K only; the RS-GDF J/K refused by name before any integral; CLI energy equals the library bit for bit, crates/ferric-cli/tests/periodic_open_mgga_energy.rs, UKS SCAN and ROKS r2SCAN with a p shell on every H); its force and stress refuse it by name | H3 doublet (cubic, 4 Bohr) / STO-3G (⟨S²⟩ 0.7527); singlets H2 (cubic, 4 Bohr) and H4 (triclinic) | the k-point UKS on a 1×1×1 mesh (complex Bloch AOs, independent τ construction, same atom grid); Gamma RKS for the singlets; PySCF 2.13.1 Gamma pbc.dft.UKS / pbc.dft.ROKS + AFTDF, UniformGrids 61³, cell.precision 1e-12 (4 pins; the PySCF Gamma UKS SCAN reproduces the existing KUKS 1×1×1 pin to 1.1e-13); V_σ against a central FD of E_xc along each spin's HOMO→LUMO rotation | Gamma UKS = 1×1×1 k-UKS to 2.0e-15 Ha (E_xc 6.2e-12); singlet UKS / ROKS = RKS to 1.8e-15; ≤ 2.9e-13 Ha vs PySCF (UKS and ROKS, SCAN and r2SCAN); V_σ vs FD ≤ 6.1e-9 relative (h = 2e-5, scales as h²) | 1e-10; 1e-10; 1e-9; 1e-7 relative. Tau mutants (τ_β from D_α, τ omitted, swapped τ, dropped β V^τ) miss the 1×1×1 anchor by ≥ 2.0e-5 Ha and the PySCF UKS / ROKS pins by ≥ 1.2e-5 Ha (dropped β V^τ is second order in the energy); the FD check fails under swapped τ and dropped β V^τ by ≥ 0.26 relative; the singlet anchors are blind to all of them by construction. Limits: energy only (no Gamma open-shell meta-GGA force or stress), dense-AFT J only (RS-GDF J/K refused by name), ROKS with the DIIS solver only (the second-order solver, whose XC Hessian is a finite difference along an arbitrary ΔD, refused by name), SCAN / r2SCAN only (TPSS refused), one 3-orbital minimal-basis cell. The β channel holds ONE orbital, so τ_β = τ_W,β exactly and an arbitrary ΔD_β meets SCAN's clamped iso-orbital kink (measured 7.95e-2 relative FD miss); the FD therefore uses an idempotent rotation | pbc_gamma_open_mgga.rs, validation_pbc_gamma_open_mgga.rs, gen_pyscf_gamma_open_mgga_refs.py, validation_pbc_rohf.rs |
| Periodic k-point RKS (PBE, PBE0) on an all-electron cell with a 1s core (LiH, no ECP) and on a 2×2×1 mesh | LiH (triclinic-ish cell, a = 5, 5, 6 Bohr with a 0.4/0.3 tilt) / PySCF STO-3G, Gamma PBE; H2 (cubic, 4 Bohr) / STO-3G, 2×2×1 Gamma-centred and shifted, PBE and PBE0, plus an orthorhombic 4.0 × 4.4 × 4.0 cell (PBE) | PySCF pbc.dft.KRKS + AFTDF, cell.precision 1e-10; LiH on a 35³ XC grid with an 80³ J mesh, H2 on 31³ | LiH Gamma PBE: ferric − PySCF = −6.5e-9 Ha; H2 2×2×1: ≤ 2.1e-12 Ha (4 cubic rows; orthorhombic rows see the run) | LiH bar 1e-7, 2×2×1 bar 1e-7. Sensitivities (ferric against ferric; no PySCF pin behind the LiH ones): dropping exxdiv moves LiH PBE0 by 2.6e-1 Ha and H2 2×2×1 PBE0 by 9.2e-2 Ha; dropping the mesh shift moves LiH PBE by 3.3e-3 Ha (thresholds 1e-3). Limits: PySCF's default AFT J mesh (35³) is 4.8e-3 Ha off for the Li 1s density (55³ 2.2e-5, 80³ 6.5e-9 from ferric), so the LiH pin requires the 80³ J mesh and only one LiH row exists (no shifted or PBE0 pin); no cell heavier than Li; the cubic H2 cell is a↔b symmetric, so an a↔b swap is seen only by the orthorhombic 4.0 × 4.4 × 4.0 PBE rows; the open-shell UKS pins in pbc_kuks.rs stay at 61³ (issue #356 item not met: ferric at 31³ misses the 61³ PBE triplet pin by 3.8e-8 Ha, over its 1e-8 bar; the 3.8e-8 at 31³ does not fit the earlier record that 40³ was 1.2e-5 off (non-monotone in mesh size; not reconciled: the 40³ run is not on record and the two may differ in system and functional); a same-grid PySCF 31³ LDA re-pin converged to another SCF state) | validation_pbc_kks_core.rs, pbc_krks_2x2x1.rs, gen_pyscf_kks_core_refs.py, gen_pyscf_kks_h2_31_refs.py |
Periodic open-shell k-point UKS analytic forces and stress with the pure meta-GGAs SCAN and r2SCAN (library kpoint_uks_gradient / kpoint_uks_stress, dense-AFT J/K only; wired to Python run_uks_kpts and the CLI by #498: CLI force and stress equal the library bit for bit (SCAN / r2SCAN optimize, H3 1×1×2; stress SCAN Gamma-centred, r2SCAN Monkhorst-Pack), Python vs library force ≤ 5.6e-17, Python force vs FD of its own energies 7.6e-10 / 3.8e-10): the per-spin tau terms (v_τσ with D_σ in the AO-derivative and strain moments, the closed-shell tau kernels reused per spin; V^τ_σ in the Pulay Fock and the Fock guard) | H3 doublet (cubic, 4 Bohr; 1×1×3 non-TRIM and 1×1×2) and H4 triplet (triclinic, 1×1×2) / STO-3G; H2 singlet 1×1×3; atom grid 20×50 (neighbour cutoff 14 Bohr) and uniform 61³ | singlet k-UKS vs k-RKS per part; fixed-density XC part vs central FD of the polarized E_xc (no SCF) on every component with an h sweep; FD of ferric's own converged energy (every force component, all nine strain components, h sweep); 1×1×3 vs the explicit 3-cell supercell (multiplicity 4); PySCF 2.13.1 pbc.grad.kuks_stress (FFTDF + UniformGrids 61³, its k-point UKS force refuses meta-GGA, so the force has no external pin) | singlet = RKS to 8.9e-16; PBE through the tau path = PBE exactly; fixed-D XC force 6.5e-10 (h 1e-4) → 7.8e-11 (h 2.5e-5, falling to the roundoff floor of the fixed-density energy, not h²-limited), XC stress 5.5e-8 → 3.5e-9 (the h² stencil, 4x per halving); force vs SCF FD (9 components, H3) 8.4e-9 / 2.0e-9 / 7.7e-10 at h = 2e-4 / 1e-4 / 5e-5, H4 triplet (12) 8.9e-9; dE/dε vs SCF FD (9, H3) 6.4e-8 / 1.5e-8 / 5.3e-9 at h = 1e-4 / 5e-5 / 2.5e-5, H4 triplet 3.5e-8; supercell force per part ≤ 1.1e-13, σ ≤ 1.8e-14; σ vs PySCF 5.8e-11 / 7.7e-11 Ha/Bohr³ (SCAN / r2SCAN; 3.7e-9 / 4.9e-9 Ha per 64 Bohr³ cell), of which the fixed-density XC part accounts for essentially all (XC part vs PySCF get_vxc 3.7e-9 / 5.1e-9 Ha); PySCF's own analytic σ vs central FD of its own strained energy (h = 1e-4, --fd) 1.3e-9 / 1.4e-9 Ha/Bohr³, run-to-run PySCF reproducibility ≤ 7e-13 | 1e-11; exact; 1e-7; 1e-7 (stress asserted at the smallest h, plus a > 2.5x fall per halving from h = 1e-4); 1e-9 / 1e-10; 1e-9 Ha/Bohr³ (σ), 1e-7 Ha (XC part). Spin/tau mutants on the polarized cell (swapped τ_α/τ_β, dropped β tau term, total τ in both libxc channels, dropped β V^τ, all spin kernels swapped, spin-averaged form, tau term dropped, doubled or with transposed strain indices) miss the FD anchors by 6.8e-4 to 9.9e-2 and PySCF by ≥ 1.9e-4; every spin mutant is invisible in the singlet anchor. Limits: H-only minimal-basis cells, no p/d shells, no ECP or heavy atom, SCAN's α ≈ 1 region not reached, RS-GDF J/K in the next row, the ROKS meta-GGA energy, force and stress are their own row below, the (75, 302) production grid not FD-checked | validation_pbc_kuxc_mgga.rs, pbc_kuxc_mgga.rs, gen_pyscf_kuks_mgga_stress_refs.py, validation_periodic_rsh_open_mgga_derivs.rs, validation_pbc_kuxcstress.rs |
Periodic open-shell k-point UKS SCAN / r2SCAN on the RS-GDF J/K: energy, analytic force and stress (library solve_kuks with jk = rsgdf, kpoint_uks_gradient / kpoint_uks_stress with KGradJk::RsGdf / KStressJk::RsGdf; issue #492; the energy is wired to Python run_uks_kpts(jk="rsgdf") and the CLI UKS route by #498, CLI equal to the library bit for bit, periodic_open_mgga_energy.rs; the force and stress are not wired, the bindings' derivative gate refuses them on RS-GDF) | H3 doublet in the cubic 4 Bohr cell: one s per H (alpha 0.5) with the exact pair-midpoint aux (energy), STO-3G with an even-tempered s+p aux (derivatives); H2 singlet; 1×1×1 and 1×1×3; uniform 32³ / 24³ and atom 20×50 (cutoff 14 Bohr) grids | exactness anchor: exact-aux RS-GDF energy vs dense-AFT (incomplete-aux negative control); PySCF 2.13.1 pbc.dft.KUKS FFTDF + UniformGrids 32³ at that exact-aux limit; singlet UKS vs RKS on RS-GDF (energy, force, stress); FD of the converged RS-GDF SCF energy on all 9 force and all 9 strain components (weekly; one polarized strain component plus the translation sum per commit) | anchor ≤ 2.1e-13 Ha; vs PySCF ≤ 1.5e-13 Ha (4 pins); singlet ≤ 2.6e-15; force vs FD 2.0e-9 / 1.9e-9 (SCAN / r2SCAN, h = 1e-4); dE/dε vs FD 7.8e-8 / 5.8e-8 (h = 1e-4, the h² stencil) | 1e-10; 1e-9; 1e-10; 1e-7 (force), 5e-7 (stress). Spin/tau mutants and a one-spin Coulomb in the (route-independent) Pulay Fock miss the pin by ≥ 3.0e-5 Ha and the FD by ≥ 2.1e-3; the singlet anchor catches every asymmetric derivative defect (≥ 2.4e-3) but not the α/β swap (1.4e-15); a source mutant (τ_β from D_α) misses the pin by 4.1e-2 while the anchor and the singlet stay green. Limits: no PySCF force/stress pin (PySCF has no k-UKS meta-GGA force, and the k-point RS-GDF derivative needs atom-centred aux, so the exact-aux construction is energy-only); H-only minimal-basis cells; the closed-shell "RS-GDF − dense transfers from PBE" check holds only to ~12% on the polarized cell and is not used | validation_pbc_kuks_mgga_rsgdf.rs, pbc_kuks_mgga_rsgdf.rs, gen_pyscf_kuks_mgga_rsgdf_refs.py |
Periodic restricted open-shell k-point ROKS with the pure meta-GGAs SCAN and r2SCAN: energy, analytic force and stress (dense-AFT J/K, the RS-GDF J/K refused by name in the SCF; library solve_kroks, kpoint_roks_gradient, kpoint_roks_stress, issue #492; the energy is wired to Python run_roks_kpts and the CLI ROKS route by #498, CLI equal to the library bit for bit for SCAN with a p shell on every H and r2SCAN / STO-3G, crates/ferric-cli/tests/periodic_open_mgga_energy.rs; the force and stress are not wired, the bindings' derivative gate refuses them by name). The k-ROKS SCF runs on the k-UKS per-spin-tau evaluator KPeriodicXcPolarized (its own tau-less duplicate deleted); the force and stress are the k-UKS meta-GGA assembly on the ROHF spin densities with W_RO = sym(D_α F_α D_α + D_α F_β D_β) from the rebuilt spin Focks (which carry V^τ_σ). No new formula: wiring, plus lifting the derivative refusal | H3 doublet (N_α = 2, N_β = 1) in the cubic 4 Bohr cell, one s per H (α 0.5), uniform 32³; H2 singlet (one s per H) and H2 triplet (two s per H, exponents 0.8 / 0.3, N_β = 0); meshes 1×1×1 and 1×1×3 (complex Bloch AOs) | Energy: singlet k-ROKS against k-RKS (unpolarized kernel, total τ); polarized H3 doublet (STO-3G, atom grid 40×110) 1×1×1 k-ROKS against Gamma ROKS (eval_tau_uks on real AOs vs chunk_tau on Bloch AOs, pbc_gamma_open_mgga.rs); fully polarized (N_β = 0) k-ROKS against k-UKS (the ROHF and UHF spaces coincide); PySCF 2.13.1 pbc.dft.KROKS (its veff is its KUKS one on the ROHF occupations) + FFTDF 40³, UniformGrids 32³, nelec = (2 N_k, N_k). Force and stress (no external pin: PySCF has no k-point ROKS gradient and its KUKS force refuses meta-GGA): singlet k-ROKS vs k-RKS and fully polarized (N_β = 0) k-ROKS vs k-UKS per part (independent SCFs); every force and strain component on the H3 doublet 1×1×3 against the central FD of ferric's own converged k-ROKS energy (force h = 1e-4 / 5e-5; stress Richardson-extrapolated from h = 5e-5 and 2.5e-5, the diagonal strains carrying an h² stencil error of ~3e-8 at 5e-5, its ≥ 2.5× fall per halving asserted); the 1×1×1 k-ROKS SCAN force and stress against the central FD of the Gamma-point ROKS energy (real AOs, eval_tau_uks, its own SCF; the two energies are asserted equal to 1e-10 at the base geometry and at the 18 strained geometries of h = 5e-5; measured 2.2e-16 and ≤ 1.6e-14) | singlet ≤ 2.2e-16 (E_xc ≤ 1.1e-16); 1×1×1 vs Gamma ROKS 3.6e-15 / 5.6e-15 (SCAN / r2SCAN; E_xc ≤ 8.2e-15), Gamma-side τ mutants miss it by ≥ 1.2e-5 and k-side τ_β-from-D_α / swapped V^xc by 4.3e-2 / 2.3e-4; fully polarized ≤ 8.8e-15 (E_xc ≤ 2.6e-13); vs PySCF ≤ 5.4e-14 Ha on 4 pins, ⟨S²⟩ = 3.75 at 1×1×3; ROKS lies 1.4e-4 to 1.2e-3 Ha above PySCF KUKS on the same setup. Force / stress: singlet per part ≤ 4.4e-16 / 7.8e-16 (SCAN / r2SCAN), fully polarized ≤ 1.9e-12 / 5.5e-14; vs FD of the own energy (9 + 9 components, orbital gradient ≤ 1.5e-11) force 2.3e-9 → 8.5e-10 (SCAN, h = 1e-4 → 5e-5), 7.4e-10 (r2SCAN), dE/dε 7.3e-10 / 8.4e-10 (Richardson; raw 3.2e-8 / 2.7e-8 at h = 5e-5); vs FD of the Gamma ROKS SCAN energy force 7.4e-10, dE/dε 6.4e-10; per commit, one diagonal strain (1,1) raw at h = 5e-5: 3.2e-8 (bar 1e-7; the grid-weight volume term dropped misses it by 1.2) | 1e-10; 1e-9; 1e-9 (energy); 1e-11 (anchors), 1e-8 (FD). Derivative mutants (test hooks, against the polarized FD, SCAN / r2SCAN): v_τα ↔ v_τβ swapped 5.1e-3 / 4.9e-3 (force), 3.4e-2 / 3.5e-2 (stress); β tau term dropped 6.8e-3 / 7.3e-3, 3.3e-2 / 3.5e-2; total τ in both libxc channels 6.0e-3 / 5.3e-3, 3.3e-2 / 3.4e-2; W_RO cross term dropped 5.6e-2, 3.5e-1; tau term dropped or doubled 1.4e-2 / 1.5e-2, 7.1e-2 / 7.4e-2; V^τ dropped from the Pulay Fock with the guard and the ROHF gate off 2.0e-2 / 2.2e-2, 3.4e-2 / 3.7e-2; β V^τ dropped (guard off, gate on) is REFUSED by the ROHF orbital-gradient gate (g = 4.5e-3 / 4.6e-3, evaluated on the rebuilt Focks); the unrestricted-form W is an identity at convergence (≤ 8.5e-10); transposed tau strain indices are invisible on the symmetric uniform-grid stress (≤ 8.4e-10; caught on the atom grid of the UKS row). Reach: the singlet anchor is blind to the τ swap only, the fully polarized one to the β tau term, β V^τ and the cross term. Energy mutants (test hooks): τ_β from D_α misses the pins by ≥ 3.3e-2 (singlet blind, 2.2e-16; fully polarized: SCF does not converge); τ dropped from the kernel input ≥ 3.1e-2 and fails both anchors (≥ 1.5e-2); V^xc_α / V^xc_β swapped between the Roothaan spin Focks ≥ 1.6e-4 (singlet blind; fully polarized anchor 1.8e-2); bare α Fock diagonalised ≥ 9.7e-4 (both anchors blind). Source mutants (edited, run, reverted): τ_β from D_α in the shared evaluator misses the SCAN 1×1×3 pin by 3.3e-2 (singlet green); a Roothaan coupling term P_o†F_βP_c → P_o†F_αP_c misses it by 1.3e-3 (both anchors green); widening the ROKS gate to TPSS fails kroks_refuses_unvalidated_meta_gga. Limits: the derivatives are FD-checked on the uniform 32³ grid only (no grid response; the atom-grid response is the k-UKS one, validated in the UKS row); no external derivative pin; the Gamma-point ROKS meta-GGA force and stress still refuse; RS-GDF J/K refused by name in the SCF (no pin; removing the refusal lets the library run it, mutation-tested) and, defensively, in the ROKS meta-GGA force and stress (kroks_meta_gga_derivatives_refuse_rsgdf, mutation-tested); TPSS refused; H-only minimal-basis cells | validation_pbc_kroks_mgga.rs, pbc_kroks_mgga.rs, gen_pyscf_kroks_mgga_refs.py, pbc_gamma_open_mgga.rs |
| Periodic closed-shell meta-GGA energy, TPSS (k-point RKS and Gamma-point RKS; its forces and stress are the next row), and SCAN / r2SCAN / TPSS on the RS-GDF J/K | H4 / STO-3G triclinic cell (two occupied orbitals, so SCAN, r2SCAN and TPSS all differ), meshes 1×1×1 and 1×1×2 and the single Gamma point; RS-GDF: H2 / STO-3G (4 Bohr cube) at 1×1×1 and 1×1×3, aux cc-pVDZ-RI and def2-universal-jkfit, and H2 with one s per H plus the 24 pair-midpoint aux s functions (the exact-fit anchor) | PySCF 2.13.1 pbc.dft.KRKS and pbc.dft.RKS (TPSS = MGGA_X_TPSS + MGGA_C_TPSS) + AFTDF, UniformGrids 61³, exxdiv ewald, cell.precision 1e-13; RS-GDF: ferric's dense-AFT J/K on the same grid, with the fixed-density Coulomb fit error E_J^fit(D) − E_J^dense(D) at the dense SCF density as the oracle | TPSS ≤ 2.5e-13 Ha vs PySCF (Gamma 2.4e-13, 1×1×1 2.5e-13, 1×1×2 1.6e-14); 1×1×1 = gamma_rks to 4.9e-15; V_xc vs FD of E_xc 2.5e-7 (1.1e-7 relative). RS-GDF: exact-fit anchor 3.4e-14 Ha; gap minus fixed-density fit error ≤ 1.7e-15 at 1×1×1, −8.8e-8 (1.1e-3 of the fit) at 1×1×3 cc-pVDZ-RI, −3.5e-10 with def2-universal-jkfit | 1e-9; 1e-8; 1e-6 relative; RS-GDF anchor 1e-10, relaxation term non-positive and < 1e-2 of the fit. TPSS is 1.4e-2 Ha from SCAN on this cell (asserted). The incomplete-aux anchor control misses by ≥ 2.3e-4 Ha. Mutants: TPSS mapped to the SCAN pair misses the pins by 1.4e-2 Ha; V^tau zeroed misses by 2.4e-5 Ha (1×1×1); V^tau zeroed on the RS-GDF route only moves the exact-fit anchor by 6.5e-5 Ha (the symmetric 1×1×1 H2 fit cases are blind to it). Limits: open-shell TPSS not validated; no meta-GGA hybrids; minimal-basis cells only. The PBE (plain-functional) RS-GDF gap matches the meta-GGA gap exactly only at 1×1×1, where the density is fixed by symmetry; at 1×1×3 it is 4 to 10 % off (the fit error of a different density) | pbc_krks.rs, pbc_krks_mgga_rsgdf.rs, gen_pyscf_pbc_tpss_refs.py, validation_pbc_krks.rs |
Periodic closed-shell meta-GGA forces and stress: TPSS on a k-point mesh (kpoint_rks_gradient / kpoint_rks_stress, dense-AFT J; Python run_rks_kpts with_gradient / with_stress, CLI k-point optimize and [cell] stress), and SCAN / r2SCAN / TPSS at the Gamma point routed through the 1×1×1 mesh (library gamma_mgga::gamma_rks_mgga_gradient / gamma_rks_mgga_stress, dense-AFT J; Python run_rks_gamma with_gradient / with_stress, CLI Gamma optimize) | H2 / STO-3G in the 4 Bohr cube ((20, 50) atom grid, 1×1×3 k-mesh and the Gamma point; uniform 24³ and 61³ grids at 1×1×2 / 1×1×3 for the stress); weekly: the two-orbital H4 / STO-3G triclinic cell at the Gamma point (SCAN, r2SCAN, TPSS) and at 1×1×2 (TPSS, 1.4e-2 Ha from SCAN), and the 3-cell supercell | PySCF 2.13.1 pbc.grad.krks_stress with xc = TPSS (FFTDF, UniformGrids 61³, 1×1×2, exxdiv ewald; PySCF's own analytic-vs-FD agreement 2.3e-10; PySCF's KRKS nuclear gradient raises NotImplementedError for every meta-GGA, so there is no PySCF force pin); central FD of ferric's own SCF energy (k-point TPSS; the Gamma SCF energy for the routed Gamma derivatives, an independent construction); the native Gamma force / stress for PBE through the same route; Gamma = 1×1×1; k-mesh = supercell | TPSS stress vs PySCF 7.5e-11 Ha/Bohr³; k-point TPSS force vs FD 2.7e-9 Ha/Bohr, dE/dε vs FD 1.1e-8; Gamma routed force vs FD of the Gamma energy 2.3e-9 (SCAN, r2SCAN, TPSS alike: the h² truncation of the bond-direction third derivative, 9.2e-9 at h = 2e-4), dE/dε 3.0e-8; routed PBE vs native Gamma force 1.3e-15, dE/dε 2.1e-14; Gamma vs 1×1×1 force ≤ 1.4e-16 (Fock guard ≤ 8.9e-16); weekly: H4 triclinic Gamma force vs FD 1.6e-10 / 5.1e-10 / 1.1e-9 (SCAN / r2SCAN / TPSS), k-point TPSS 3.0e-9, dE/dε 1.6e-8; TPSS 1×1×3 vs supercell force 1.2e-14, dE/dε 6.9e-13 | PySCF 1e-9; force FD 1e-7; k stress FD 5e-7; Gamma stress FD 3e-7; glue 1e-9. Mutants (miss vs the FD or pin): TPSS mapped to the SCAN pair in the libxc name table 7.8e-5 vs the PySCF stress pin (FD of ferric's own energy is BLIND to it, 2.5e-9; the per-commit force tests also fail it through the Fock guard and a SCAN-vs-TPSS gap of 1.6e-3 / 8.2e-3); tau AO term dropped / doubled ≥ 1.7e-2; tau left out of the point motion 3.5e-3 (k) / 4.1e-3 (Gamma); V^tau zeroed in the Pulay Fock 5.8e-3 (k) / 1.4e-3 (Gamma) force, 2.3e-2 / 3.0e-2 stress (with the guard on it is refused); grid weight response dropped ≥ 1.1e-1; tau strain dropped / doubled ≥ 3.0e-2; StressTauPath is an equivalent mutant on a meta-GGA (0, asserted). Refused (typed, asserted): an RS-GDF Gamma SCF (Fock guard, 9.5e-6 > 1e-8; the bindings refuse jk = rsgdf before the SCF), another grid or functional in the config, an open-shell result, range-separated functionals or kernels. Limits: closed shell only (open-shell and Gamma UKS/ROKS meta-GGA refused); on the one-orbital H2 cell SCAN and r2SCAN coincide exactly (α = 0 everywhere, 5.6e-17, asserted), so only the H4 rows separate them; TPSS and the Gamma route on the dense-AFT J only (the bindings refuse a TPSS force / stress and a Gamma meta-GGA force / stress on jk = "rsgdf" by name, before the SCF); minimal-basis H cells; the 1×1×1 kernels cost 32 nao⁴ bytes on top of the Gamma ERI | pbc_gamma_mgga_derivs.rs, validation_pbc_mgga_derivs.rs, pbc_kxcgrad_mgga.rs, pbc_kxcstress_mgga.rs, gen_pyscf_kks_stress_refs.py, periodic_kpoint_ks_optimize.rs, periodic_kpoint_stress.rs, validation_periodic_kpoint_ks_optimize.rs, validation_pbc_kxcstress_mgga.rs |
Periodic XC grid neighbour switch (PeriodicGridConfig::neighbour_switch W, CLI [cell] neighbour_switch; default 0 = the hard image cutoff, bit-identical): a quintic taper of each image atom's presence over [D − W, D] in the SSF/Becke partition, with its derivative in the Gamma and k-point KS force and stress | H2/STO-3G, cubic 4 Bohr, PBE, atom grid (20, 50), D = 10 Bohr, W = 2 Bohr, dense AFT, Gamma and 1×1×3; kernel: a mixed Li/H/C cluster (SSF and Becke); grid level: Li–H cell, (12, 26), D = 6.5, W = 1.5 (SSF and Becke), plus a sparse 9 Bohr Li–H cell at D = bound + W (Becke) | central FD of ferric's own SCF energy at h = 1e-4 and 5e-5 (6 force, 9 strain components), hard vs switched; dense scans (61 points, step 1e-4) of ε_11 and an H x-coordinate, fourth differences; kernel vs FD and vs the unswitched kernel; grid functional Σ W F vs fourth-order FD under displacement and strain | switched: force vs FD 2.7e-9 (k) / 2.3e-9 (Gamma), stress 2.5e-8 / 2.9e-8 at h = 1e-4, 5.9e-9 / 7.1e-9 at 5e-5 (h²); hard on the same cells: stress 2.0e-6 / 2.9e-6 at h = 1e-4 (a straddled jump). Scans: hard max | Δ⁴E | 6.7e-7 (strain) / 4.8e-8 (displacement), largest single-step jump ~2.2e-7 / 1.6e-8 Ha; switched 2.4e-13 / 7.6e-14. Energy vs hard D = 20 (Gamma): hard D = 10 +3.9e-6, D = 12 −4.8e-8; switched (D, W = 2) = (10) +1.1e-5, (12) +1.7e-7, (14) −3.8e-9, each between the hard energies at D − W and D (asserted). Grid build (Li–H, (75, 302), 3 threads): hard D = 10 0.29 s, switched D = 10 / W = 2 0.46 s, automatic D = 12 / W = 2 0.87 s. Kernel: all-present limit = unswitched to 1e-14; derivative vs FD 2.6e-10; grid functional force / stress vs FD 2.3e-10 |
Periodic XC grid neighbour switch on the open-shell, meta-GGA and RSH derivative paths (#495), and the Python neighbour_switch keyword | Gamma UKS / ROKS PBE (native grad.rs / stress.rs), k UKS / ROKS PBE 1×1×2 (kuxcgrad / kuxcstress), Gamma RKS SCAN (1×1×1 route), k UKS SCAN 1×1×1, Gamma RKS CAM-B3LYP force + stress, Gamma UKS / ROKS CAM-B3LYP force (1×1×1 route); H2 / H3 doublet STO-3G, cubic 4 Bohr, (20, 50), D = 10, W = 2 Bohr, exxdiv none, dense AFT. Python: the six KS bindings on H2 / H3 | W = 1e-9 against the hard cutoff (exactness anchor, against the unswitched kernel); central FD of ferric's own switched SCF energy (all force and nine strain components) at h = 1e-4, 5e-5 and their Richardson combination. Python: automatic cutoff with W = explicit 10 Bohr + W (the library adds W, so the Å → Bohr conversion is visible), the Gamma energy shift E(W) − E(hard) against the library's, Gamma = 1×1×1 mesh with the switch, refusals | anchor ≤ 7.9e-15 on every path; force vs FD ≤ 4.8e-9 (h = 1e-4), Richardson ≤ 9.9e-10 (the force sits at the ~1e-9 SCF noise floor, not h²-limited); stress vs FD ≤ 3.3e-7 at h = 1e-4 (k UKS SCAN), falling ≈3.7–4.4× per halving on every row, Richardson ≤ 2.5e-9. Python: cutoff identity < 1e-12, shift +7.237e-6 vs library +7.236e-6, Gamma − 1×1×1 < 1e-10 | anchor 1e-12; force 1e-7 (h = 1e-4); stress 1e-6 (h = 1e-4; the H3 cells' h² truncation is 3× #504's); Richardson 2e-8; Python shift 2e-8. Mutants: dropped image presence derivative misses on all nine paths by 1.7e-6–4.0e-6 (force) and 2.7e-5–5.5e-5 (stress), the same at every h; 1 − f s cell factor fails every anchor (≥ 0.37 Ha); binding unit slip and dropped W fail 7 and 10 of the 36 Python tests. Limits: H-only cells, minimal basis, one W, (20, 50) only; the open-shell RSH stress was not a library path when this row was measured (it landed later, library only, with its own row; the binding gates still refuse it by name); ROKS meta-GGA has no SCF; Python is compared with the library through the shift and the cutoff identity, not bit for bit (cdylib, no Rust harness) | validation_pbc_grid_switch_paths.rs, test_pbc_neighbour_switch.py |
Periodic restricted open-shell range-separated hybrids (ROKS: HSE06, CAM-B3LYP; energy only) at the Gamma point (gamma_roks_rsh) and on a k-mesh (solve_kroks), dense AFT and RS-GDF | The UKS row's cells: H3 doublet and H4 triplet / STO-3G, triclinic, Gamma and 1×1×2 (and 1×1×1), exxdiv ewald and none | PySCF 2.13.1 pbc.dft.ROKS / KROKS + AFTDF (mf.nelec = (N_a N_k, N_b N_k); every row converged in PySCF), 11 energies read from the committed JSON; closed-shell limit against RKS (dense: H2 and an H4 singlet with SCF freedom; RS-GDF: only H2, cubic a = 4 Bohr, HSE06, 1×1×1, where HSE06's erf part is empty, pbc_krsh_rsgdf.rs); 1×1×1 = Gamma; 1×1×2 mesh = 6-atom supercell / 2; E(ROKS) ≥ E(UKS) on the 9 rows with both PySCF pins (sanity, not a bar) | ≤ 4.4e-13 Ha vs PySCF; ROKS − RKS ≤ 4.0e-15; 1×1×1 − Gamma 0.0; mesh − supercell/2 ≤ 1.7e-14; ROKS − UKS +5.1e-6 to +3.1e-3; RS-GDF − dense 0.92–1.84 × the PBE0 UKS plain-Coulomb fit error | 1e-11; 1e-11; 1e-11; 1e-10; ROKS − UKS sign only; RS-GDF < 5 × the fit error. Mutants on the polarized CAM-B3LYP H3 cell moved E by ≥ 7.4e-4: α K on the β Fock 1.2e-2, β K dropped 1.5e-1, halved Madelung 2.2e-1, plain instead of mixed Madelung 5.0e-1, attenuated K scaled by c_sr 3.9e-1, bare α Fock instead of Roothaan 7.4e-4 (α-on-β and bare-α are 0.0 in the closed-shell limit); source mutants (plain Madelung at Gamma, unattenuated K at Gamma, k-point dense J/K built without the attenuation) moved the pins by 0.21–0.75. Limits: as the UKS row (the G ≠ 0 erf kernel is live only for CAM-B3LYP on this cell; the kernel is shared with RKS/UKS, so only PySCF is independent of it; H/STO-3G only; wB97X is accepted but no test runs ROKS with it); bindings (#498): Python run_roks_kpts / run_roks_gamma and the CLI ROKS route (gamma_rsh::gamma_roks_any, solve_kroks); the CLI energy equals the library bit for bit (k-mesh HSE06 and Gamma CAM-B3LYP, H3 doublet, cubic 4 Bohr), Python vs library 4.8e-14 / 7.1e-14 (bar 1e-12), PBE0 / B3LYP siblings 2.5e-4 / 2.5e-2 away; no range_split (the ROKS force and the open-shell stress have their own rows, wired by #498) | pbc_roks_rsh.rs, validation_roks_rsh.rs, gen_pyscf_roks_rsh_refs.py |
| Periodic open-shell range-separated hybrids (UKS: HSE06, CAM-B3LYP; energy only) at the Gamma point and on a k-mesh, dense AFT and RS-GDF | H3 doublet and H4 triplet / STO-3G in a triclinic cell (polarized, non-degenerate: ⟨S²⟩ 0.753 and 2.000), Gamma and 1×1×2 (and 1×1×1), exxdiv ewald and none | PySCF 2.13.1 pbc.dft.UKS / KUKS + AFTDF (mf.nelec = (N_a N_k, N_b N_k)), 16 energies read from the committed JSON; closed-shell limit against RKS (H2 and an H4 singlet with SCF freedom); 1×1×1 = Gamma; 1×1×2 mesh = 6-atom supercell / 2 | ≤ 4.4e-13 Ha vs PySCF (PBE0 on the same cell 3.7e-13); UKS − RKS ≤ 4e-15 (H4 singlet) and 0.0 (H2, symmetry-fixed: tests the energy assembly only); 1×1×1 − Gamma 3.3e-15; mesh − supercell/2 ≤ 4.7e-15; RS-GDF − dense equals the plain-Coulomb fit error × 1.000 | 1e-11; 1e-11; 1e-11; 1e-10. Mutants on the polarized CAM-B3LYP cell at Gamma moved E by ≥ 1.1e-2 (α K on the β Fock, β K dropped, halved Madelung) and by 1.3e-3 to 2.5e-1 (ω ×1.01, ω ×0.01, c_lr := c_sr, c_sr ×0.99); on the 1×1×2 mesh by ≥ 1.9e-2 (also spin swap, unmixed Madelung, K from the total density). Only α-on-β and the spin swap are invisible in the closed-shell limit (2.2e-16). Limits: the attenuated G ≠ 0 kernel is exercised only by CAM-B3LYP (ω 0.33; for HSE06, ω 0.11, the largest erf weight on this cell is 4.2e-16 at Gamma and 1.4e-4 at 1×1×2, so its erf piece acts through the mixed Madelung constant and the semilocal ω); no open-shell wB97X pin; the bindings (Python run_uks_gamma / run_uks_kpts, the CLI UKS route at Gamma and on a mesh) call gamma_rsh::gamma_uks_any and solve_kuks, and the CLI energy equals the library to 1e-12 (HSE06, Gamma and 1×1×2, dense; the run log holds exactly the library value, and the 1-ulp residual the test sees at 1×1×2 is serde_json's float parse; RS-GDF measured 0 against a 1e-8 bar); open-shell wB97X (HYB_GGA_XC_WB97X) and any other libxc range-separated (CAM) GGA hybrid are accepted, not pinned; no range_split (the dense-AFT UKS force and the open-shell stress have their own rows; ROKS: previous row); the attenuated K is shared with the closed-shell code, so only PySCF is independent of the kernel; H/STO-3G fixtures only (no p or d shells) | pbc_oshell_rsh.rs, validation_oshell_rsh.rs, gen_pyscf_oshell_rsh_refs.py, periodic_kpoint_open_shell.rs |
Periodic k-point forces and stress (Hartree-Fock, library kpoint_rhf_gradient/kpoint_uhf_gradient/kpoint_rhf_stress/kpoint_uhf_stress): open-shell s+p on the fitted (RS-GDF) path, the dense H4-triplet supercell, and with a LANL2DZ ECP plus a rotating basis the range-split RS-GDF, the fit-acting force mutants and a shifted (Monkhorst-Pack) mesh | H2 triplet (2α/0β) and H4 triplet (3α/1β), H STO-3G s + one p, cubic cells, Gamma-centred 1×1×1 and 1×1×3, exxdiv none and ewald; HI (H cc-pVDZ, I s/p general contractions, LANL2DZ ECP) in diag(6, 6, 7), Gamma-centred 1×1×3 and MP 1×1×2, exxdiv none | 1×1×1 = Gamma (the Gamma code; independent in the LR part and the phase / residue binning, the SR derivative walks are shared); k-mesh = explicit 3-cell supercell / 3; central finite differences (h = 1e-4) of ferric's own k energy; range-split = unsplit fit; rotated = unrotated walk | RS-GDF open shell: 1×1×1 − Gamma total ≤ 8.8e-13 (stress), 3.1e-13 (force), per part ≤ 9.9e-12; 1×1×3 − supercell/3 ≤ 5.0e-12 (H2), 8.2e-11 (H4) stress, ≤ 3.0e-11 force; FD (H2, all nine strains) 2.5e-8 stress, 2.5e-9 force, FD(2h)/FD(h) 4.04. Dense H4 1×1×3 − 12-atom supercell/3: 5.5e-13 (stress). HI: range-split − unsplit 5.2e-14 force, 1.3e-12 stress; FD of the split energy 3.4e-9 / 2.7e-8; RS-GDF FD force 3.5e-9; MP mesh FD 3.2e-9 / 1.2e-8, rotated − unrotated 1.5e-14 / 5.6e-13 | 1e-11 (1×1×1 total), 1e-10 (part), 1e-9 (supercell), FD 3e-8 force / 3e-7 stress (H) and 4e-8 / 3e-7 (HI), 2e-11 (split), 1e-11 (rotation), 6e-12 (dense H4). Every mutant that can act misses its anchor by ≥ 1e-4 (H supercell / Gamma anchors, smallest 8.7e-3) or moves the HI analytic total (all components) by ≥ 1e-5 (smallest 2.1e-4, SplitGammaKernel); the smallest overall is NoAuxFt 3.4e-5 (bar 1e-5, visible to the FD only); EcpMeshCountModuli, WrongQ and GammaMadelung are identities on the C2 system (0). Newly reachable: FitCrossSpin on a p-shell / empty-β density (≥ 1.98e-1 at 1×1×1, ≥ 6.2e-1 at 1×1×3), the fit-acting force mutants with an ECP (RotFitWrongResidue 1.5e-1, RotSwapPair 2.5e-2, FitAuxPhase 1.7e-3), and the shifted-mesh ECP residue mutants (EcpMeshCountModuli 2.0e-1 force, 8.0e-1 stress; identities on Gamma-centred meshes). Limits: the k RS-GDF derivative imports the Gamma SR walks (sr2_force, sr3_force, sr2_strain_with, sr3_strain_with, aux_ft_strain_shells), so 1×1×1 = Gamma and supercell anchors check only the LR part and the binning independently; that shared SR kernel is anchored by FD on the H2 triplet (both exxdiv), the H4 triplet (exxdiv none: all nine strains 7.2e-8, force 7.5e-10, FD(2h)/FD(h) 3.78–4.28, bar 1e-6 / 3e-8) and the H3 s+p quartet (3α/0β, exxdiv none: zz, xx, xy, xz 4.5e-8, force 2.6e-9, ratio 3.86; its 1×1×1 − Gamma 1.1e-12 stress, 4.2e-13 force); the HI mutant movers are measured on the analytic total, while the FD anchors cover a subset of components; orthorhombic cells only for the split ECP anchor; the H4 RS-GDF supercell floor is 16× the H2 one, attributed to the 4-H aux metric but not tested. Open-shell ECP, several ECP elements, other multiplicities, the 1×1×3 triclinic ECP mesh, other shifted meshes and the external (PySCF) force comparison are in the next row | validation_kstress_followups.rs, pbc_kstress.rs, pbc_kpoint_ecp_rotation.rs, validation_pbc_kstress.rs, validation_pbc_kgrad.rs, validation_periodic_kpoint_optimize.rs |
| Periodic k-point forces and stress (Hartree-Fock): an open-shell septet with two ECP elements, the 1×1×3 triclinic ECP mesh, shifted meshes beyond 1×1×2, and k-point forces against PySCF | AsSb septet (8α/2β, As and Sb LANL2DZ ECPs, rotating s/p general contractions, s/p/d aux) in diag(6, 6, 10), Gamma-centred 1×1×1 and 1×1×3, k-UHF RS-GDF; HI (H cc-pVDZ, I LANL2DZ) triclinic rows (6,0,0), (1,6,0), (1.2,−0.8,7) at Gamma-centred 1×1×3, MP 2×2×1 on rows (8,0,0), (1.3,8,0), (1.2,−0.8,7), MP 1×1×4 and MP 1×1×3 on diag(6, 6, 7), k-RHF RS-GDF, rotation on; H2 STO-3G and s+p triplet, cubic 4 Bohr, dense AFT, Gamma-centred 1×1×3 (exxdiv none and ewald) and MP 1×1×2 | 1×1×1 = Gamma; 1×1×3 = explicit 3-cell supercell / 3; central FD (h = 1e-4) of ferric's own k energy; rotated = unrotated walk; MP 1×1×3 = Gamma-centred 1×1×3 (the same k set: an odd MP axis is not shifted); PySCF 2.13.1 pbc.grad.krhf / pbc.grad.kuhf forces (FFTDF 41³, all-electron gate lifted, per-k aufbau pinned for KUHF; the lifted gate checked against FD of PySCF's own energy, 7e-11..3e-9) and PySCF AFTDF energies with lanl2dz | AsSb: 1×1×1 − Gamma 2.4e-14 force, 2.9e-13 stress; 1×1×3 − supercell/3 9.6e-14 force, 1.5e-12 stress; FD (all nine strains) 5.6e-8 stress, 7.8e-10 force. Triclinic HI 1×1×3: supercell 2.8e-13 / 8.1e-13, FD 2.9e-9 / 2.3e-8. MP 2×2×1: FD 1.2e-9 / 3.4e-8, rotated − unrotated 2.6e-14 / 1.3e-12; MP 1×1×4: FD 3.5e-9 / 2.6e-8, rotated − unrotated 5.3e-14 / 4.7e-13; MP 1×1×3 − Gamma-centred 1.9e-13 / 1.1e-12. vs PySCF: energies ≤ 6.1e-13, forces ≤ 2.5e-11 (RHF none/ewald, MP 1×1×2, s+p triplet UHF); ECP-cell energies 6.4e-6 (HI) and 3.1e-5 (AsSb septet) | 5e-12 (1×1×1), 3e-11 (supercell), FD 1e-8 / 1e-6 (AsSb) and 4e-8 / 3e-7–4e-7 (HI), 2e-11 (rotation, MP identity), PySCF 1e-11 energy / 3e-10 force, 3e-4 ECP energy. Every listed production-wired mutant misses its anchor or moves the total by > 1e-5, except WrongQ on MP 1×1×2, asserted as an identity (all q ≡ −q there); smallest EcpWrongResidue 2.6e-5 (AsSb force, supercell) and 3.1e-5 (MP 2×2×1 force); EcpCentreWrongAtom (centre derivative folded onto the next atom: here the other ECP atom; it also fires on single-ECP cells by folding onto H, so no mutant is specific to several ECP centres) 2.7e-3–6.9e-3; FitCrossSpin ≥ 7.9e-1; EcpMeshCountModuli 1.8e-3–2.1e-1 (force) and 2.5e-2–8.3e-1 (stress) on the shifted meshes, exactly 0 on MP 1×1×3. Limits: no external stress anchor exists (PySCF 2.13.1 has no k-point HF stress; its krks_stress is FFTDF pure-DFT only); no external force with an ECP (PySCF's k-point HF gradient takes GTH pseudopotentials only, ferric has none), so the ECP derivatives are anchored internally and the ECP cells only by energy; the external force pins rest on a one-line gate lift in PySCF (its own pseudo branch run for an all-electron cell); the AsSb−PySCF energy gap (3.1e-5, 5× the HI one) is not separated; the RS-GDF energies of the ECP cells carry a fit error vs dense of 2.9e-2 (HI) and 9.6e-3 Ha (AsSb), recorded by ecp_cell_dense_energies_match_pyscf (the anchors differentiate the fitted energy); with ECPs the high multiplicity is the closed-subshell septet (an IBr quartet/quintet with partly filled degenerate p shells did not converge in k-UHF); the all-electron H3 s+p quartet is in the previous row; the MP 2×2×1 k-RHF limit-cycles on the 6 Bohr triclinic rows, so it runs on 8 Bohr rows | validation_kstress_coverage.rs, gen_pyscf_kforce_pins.py |
| Periodic k-point Kohn-Sham (RKS: LDA, PBE, PBE0, SCAN, r2SCAN; UKS: LDA, PBE, PBE0; ROKS: PBE, PBE0) and ROHF analytic forces and stress on a cell whose basis carries an ECP, dense AFT and RS-GDF | HI (closed shell) and the H2I doublet in diag(6, 6, 7) Bohr, H STO-3G + compact I s/p, LANL2DZ ECP on I; Gamma-centred 1×1×3 (non-TRIM), atom grid (20, 50), shared 16 Bohr neighbour cutoff | central finite difference of ferric's own k-point energy (fresh SCF per displaced or strained cell): every force component, all nine strains for RKS LDA/PBE, UKS PBE and ROHF on dense J/K, three strains (zz, xy, xz) for the rest; the Python bindings (all four routes, 1×1×2) against FD of their own energies; ten production-wired ECP mutants (ECP term dropped, centre derivative dropped / sign-flipped / on the wrong atom, image weight at the transposed residue, home ECP image only, ket image weight) against the same FD | force ≤ 4.2e-10 Ha/Bohr, dE/dε ≤ 2.1e-9 Ha over all routes (the ECP parts are 6e-2 to 1.6e-1 and 1.5e-1 to 3.9e-1); every mutant misses by ≥ 3.1e-3 (force) and ≥ 2.1e-2 (stress) on RKS PBE (dense and RS-GDF), UKS PBE and ROHF (dense); Python bindings force ≤ 3.0e-10, two strains ≤ 6.6e-9. The strain FD uses h = 1.25e-5: the atom grid's hard neighbour cutoff puts one-step energy jumps (1e-11 to 1e-9 Ha) inside ±2.5e-5 on some components at 14, 16 and 20 Bohr cutoffs (FD misses of 6e-7 to 1e-5 that scale as 1/h, not h²); at 1.25e-5 every probed component is ≤ 2.1e-9, and a straddled jump would fail the anchor, not pass it | 3e-9 Ha/Bohr; 3e-8 Ha; mutants ≥ 1e3 × the bar. Not covered: a triclinic cell or shifted mesh with KS and an ECP (covered for HF in pbc_kpoint_ecp.rs / pbc_kpoint_ecp_rotation.rs, the same ECP walk), d/f shells on the ECP atom, more than one ECP atom, an external reference, B3LYP, SCAN/r2SCAN stress on RS-GDF with an ECP (accepted, not anchored: the ECP and RS-GDF SCAN stress anchors are separate), CLI numerics (the CLI tests check only that ECP cells pass the gate and reach the SCF) | validation_kks_ecp_derivs.rs, pbc_kks_ecp_derivs.rs, test_pbc_kpoint_open_shell_and_stress.py |
Periodic parallel, column-rotated and pair-symmetry-reduced walks are BITWISE their reference walks: RS-GDF SR/LR sums and k residue bins, Gamma and k-point SR nuclear attraction, the RS-GDF force and stress walks (Gamma RHF/UHF, k-point RHF, split), the overlap/kinetic and ECP force terms and the LR force pass across rayon thread counts and against frozen serial walks; the s2 (-r_L) k-pair bins against the ordered walk; the SR column rotation against the unrotated build | H2 / H3 / H4 cells, STO-3G to cc-pVDZ with ET and cc-pVDZ-RI aux, Gamma and 1×1×2 to 2×2×2 meshes, 1/2/6 threads (weekly); shrunk fixtures at 1/2 threads per commit | frozen serial walks and ferric's own unrotated / unsymmetrised builds (independent constructions of the same sums); the s2 and rotation mutants against the same builds | bit-for-bit (0 differing bits) | 0 bits; mutants must move the result. Per commit runs one shrunk canary per walk family (*_canary tests in the fast files); the full thread sets, larger cells and every mutant run weekly. Not covered: thread counts above 6, MPI ranks | validation_pbc_parallel_bitwise.rs, pbc_parallel_bitwise.rs, validation_pbc_lr_force_bitwise.rs, pbc_lr_force_bitwise.rs, validation_pbc_kpair_symmetry.rs, pbc_kpair_symmetry.rs, validation_pbc_sr_rotation.rs, pbc_sr_rotation.rs |
A "—" means the repository states no number for that cell. It is left empty on purpose rather than filled with an estimate.
Benchmark sweeps (GW100 and others) are kept in the project's working notes and are not reproduced here. Quoting a benchmark statistic from memory rather than from the record is the kind of unchecked claim this page exists to prevent.
Mixed-precision GPU kernels
[precision] mode = "mixed" (deprecated alias: [gpu] precision = "mixed") is opt-in (the default is f64) and applies only to
the kernels in mixed_kernels. A mixed result is not Proven: it is not
compared with an independent code, and it is not an f64 result. It is graded as
a measured error map: a deterministic bound derived from the operation
count, and the errors measured against the same run's f64 CPU energy on the
systems below. Whether that error is acceptable for a given use is not decided
here.
rimp2-energy (shipped). B_ov is stored on the device as f32 (half the
bytes of the f64 tensor); each block G_i = B_iᵀ·B_tail is formed by f32
panel products of width b = 64 (the shipped default) summed into an f64
accumulator; the pair arithmetic, denominators and all sums are f64. A finite
B_ov element beyond the f32 range, or one above sqrt(f32::MAX / b) (an f32
panel sum of products could overflow), or an unavailable flush kernel, runs the
f64 device path instead and is counted (mixed_fallback_f64); a NaN B_ov
passes through to a NaN energy as on the CPU; a pool too small for the f32
tensor runs the CPU path unchanged.
| Column | Value |
|---|---|
| Accumulation depth | n = naux per element, panels of b = 64 |
| κ_sum | measured per system (table): the energy-level κ_E = Σ fac|g|S/|D| / |E_os| and the p99 of the element κ = S_ab/|g_ab| on the first block (the element maximum is unbounded because g_ab crosses zero) |
| Bound | per element ε_G·S_ab + η with ε_G = (1+u₃₂)²(1+γ_b(u₃₂))(1+γ_⌈naux/b⌉(u₆₄)) − 1 + γ_naux(u₆₄) (u₃₂ = 2⁻²⁴, u₆₄ = 2⁻⁵³; the last term is the f64 reference's own rounding) and η the f32 underflow term; summed into an energy bound through the OS and SS terms |
| Mitigation | f64 panel accumulation, f64 pair arithmetic, f64 everything else |
| Precision | f32 storage, f32 panel products, f64 sums |
The measured error against the f64 CPU energy of the same B_ov at b = 64 (E_corr is the f64 RI-MP2 correlation energy; every reference is an RI-JK RHF with loose thresholds, the same orbitals on both sides of each comparison). OS and SS are signed and partly cancel in the total, so all three are given:
| System (frozen core) | nocc / nvir / naux | E_corr (Eh) | ΔE_OS (Eh) | ΔE_SS (Eh) | ΔE total (Eh) | total (kcal/mol) | bound OS / SS (Eh) | κ_E | p99 κ |
|---|---|---|---|---|---|---|---|---|---|
| H2O / cc-pVDZ | 5 / 19 / 84 | −0.204009 | −4.9e-9 | −2.1e-9 | −7.0e-9 | −4.4e-6 | 1.6e-6 / 2.8e-6 | 1.30 | 4.8e11 |
| n-butane / cc-pVDZ | 17 / 89 / 364 | −0.599582 | −2.8e-10 | −3.7e-10 | −6.5e-10 | −4.1e-7 | 7.2e-6 / 1.4e-5 | 1.95 | 4.6e11 |
| n-octane / cc-pVDZ | 33 / 169 / 700 | −1.183770 | +1.1e-10 | −8.3e-11 | +2.6e-11 | +1.6e-8 | 1.7e-5 / 3.3e-5 | 2.40 | 2.0e11 |
| n-dodecane / cc-pVDZ | 49 / 249 / 1036 | −1.767936 | −1.7e-10 | −9.1e-11 | −2.6e-10 | −1.6e-7 | 2.9e-5 / 5.6e-5 | 2.76 | 1.2e11 |
| H2O / aug-cc-pVTZ | 5 / 87 / 198 | −0.283553 | −6.1e-10 | +3.7e-10 | −2.4e-10 | −1.5e-7 | 2.5e-6 / 4.6e-6 | 1.50 | 1.2e11 |
| benzene / aug-cc-pVDZ (6) | 15 / 171 / 570 | −0.810690 | +5.1e-10 | +4.7e-10 | +9.7e-10 | +6.1e-7 | 7.8e-6 / 1.4e-5 | 1.65 | 2.1e12 |
| benzene / aug-cc-pVTZ (6) | 15 / 393 / 912 | −0.963520 | −8.7e-10 | −1.0e-10 | −9.7e-10 | −6.1e-7 | 1.0e-5 / 2.0e-5 | 1.84 | 3.0e12 |
Every component is inside its bound. The error splits into an f32-storage part (B_ov rounded to nearest through f32, recomputed in f64) and an accumulation part (the device minus that), each inside its own bound; at benzene / aug-cc-pVTZ the accumulation part (OS −9.4e-10) is the larger.
What the bound can and cannot catch. The energy bound is a worst case over
rounding signs, three to six decades above the measured energy errors (at the
worst single element the error reaches 7.1e-2 of its per-element bound at
benzene / aug-cc-pVTZ and up to 1.2e-1 at dodecane, but those elements carry
little of the energy). It catches gross defects, such as a wrong operand offset,
a dropped panel or a dropped pair weight (1e-1 relative error). It does not
catch a subtle degradation of the kernel: a plain SGEMM without the f64 flush,
an f32 B_ov with an f64 GEMM, a truncating instead of round-to-nearest upload,
or f32 accumulation across panels all stay inside it. They are pinned by other
checks:
- The rounding mode is guaranteed by a bit-exact test: the device f32
B_ovequalsas f32(round to nearest, ties to even) element for element. - The f64 flush across panels is guaranteed by a deterministic construction (panel sums +2²⁰, 2⁻⁹, −2²⁰ give exactly 2⁻⁹ on the device; summing the panels in f32 gives 0).
- The panel structure is checked by a measurement over every G block of
benzene / aug-cc-pVTZ (18.3 million elements): the per-element RMS of err/S is
1.80e-8 for the device and 1.78e-8 for the host twin of the same algorithm
(ratio 1.007), against 2.79× the twin for a plain SGEMM, 0.24× for an f32
B_ovwith an f64 GEMM, 1.29× for a truncating upload and 1.30× for f32 accumulation across panels. The accepted device/twin ratio lies between 0.49 and 1.13 (the geometric midpoints to the nearest measured defect on each side). This measurement is supplementary and device and system dependent: its margin (about 13%) is of the size of the device/twin spread between systems (0.87 at water / cc-pVDZ, 1.007 at benzene / aug-cc-pVTZ), so it is not a guarantee on another device or build. Measured against the shipped b = 64, a panel width of 96, 128 or 256 reads 1.27, 1.51 or 2.39 (above the band); a width of 32 reads 0.70 (inside it: a more accurate kernel, a performance difference that is not pinned).
How the error depends on size. The per-element RMS of err/S falls with the
auxiliary depth over the four cc-pVDZ systems: 4.9e-8, 2.5e-8, 2.1e-8, 1.8e-8
at naux 84, 364, 700, 1036; the fitted slope of ln RMS against ln naux is
−0.40 ± 0.03 (2 degrees of freedom; one-sided 95% upper limit −0.32; the
random-sign model predicts −0.5). This describes the shipped kernel; it is not a
law and not a defect gate: naux is confounded with the molecule (without water
the slope is −0.31), and a plain-SGEMM or truncating-upload emulation on the
same blocks also falls with naux. No size dependence is claimed for the total
energy error: its signed OS and SS parts scatter by two decades across systems
through cancellation. Test files:
crates/ferric-mp2/tests/rimp2_f32_storage_error_law.rs (no device; the name
predates this wording) and crates/ferric-mp2/tests/gpu_rimp2_mixed.rs
(device).
rimp2-energy, unrestricted (shipped, same kernel). The unrestricted
(open-shell) Coulomb RI-MP2 energy keeps one f32 B_ov per spin on the device
and forms every block of E_αα, E_ββ and E_αβ with the same f32 panel products of
width b = 64 and f64 accumulation; the antisymmetrised same-spin
K = g_ab − g_ba, the opposite-spin g², the denominators and all sums are f64.
The refusals and the mixed_fallback_f64 counter are the closed-shell ones.
The erfc and terfc unrestricted energies use the same kernel (next
paragraph); every other caller of the unrestricted pair kernels (the erf
operator, composite fitted operators, and direct calls of the pair kernels)
runs the f64 device kernel.
| Column | Value |
|---|---|
| Accumulation depth | n = naux per element, panels of b = 64 |
| Bound | the closed-shell per-element factor ε_G·S_ab + η, propagated through the SS term (K = g_ab − g_ba, perturbation δ_ab + δ_ba) and the OS term (g²), plus the f64 summation of the terms |
| Acceptance | |ΔE| ≤ 1.0e-3 Eh per heavy atom and per electron; the derived bound itself is below it on every system below |
| Mitigation | f64 panel accumulation, f64 pair arithmetic, f64 everything else |
| Precision | f32 storage, f32 panel products, f64 sums |
The measured error against the f64 CPU energy of the same B_ov at b = 64
(cc-pVDZ / cc-pVDZ-RI, all electrons correlated; every reference is an RI-JK UHF
with loose thresholds, the same orbitals on both sides):
| System | nocc α/β | nvir α/β | naux | E_corr (Eh) | ΔE_αα (Eh) | ΔE_ββ (Eh) | ΔE_αβ (Eh) | ΔE total (Eh) | per heavy atom | per electron | bound (Eh) | bound per heavy atom |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| O2 triplet | 9 / 7 | 19 / 21 | 112 | −0.348888 | −2.4e-9 | +1.1e-8 | +5.5e-9 | +1.4e-8 | 7.0e-9 | 8.7e-10 | 3.9e-6 | 2.0e-6 |
| CH3 doublet | 5 / 4 | 24 / 25 | 98 | −0.129040 | +1.5e-10 | +2.3e-10 | +1.3e-10 | +5.0e-10 | 5.0e-10 | 5.5e-11 | 1.6e-6 | 1.6e-6 |
Every block is inside its bound (largest error/bound 1.2e-2, O2 E_ββ). As in the
closed-shell case the energy bound is blind to a subtle kernel degradation: a
build that accumulates the whole contraction in f32 (one panel) stays inside it
on both systems (O2 ΔE +9.2e-9 Eh). That degradation is caught by a
deterministic construction through both the same-spin and the opposite-spin
blocks (an element of exact value 1 + 2⁻³⁰ split over two panels: the device
returns the f64 energy exactly; an f32 sum loses 2⁻²⁹ Eh, against an accepted
error of 2⁻³⁰) and by the panel counter (⌈naux/b⌉ panels per block). Test files:
crates/ferric-mp2/tests/gpu_u_rimp2_mixed.rs and
crates/ferric-mp2/tests/gpu_u_rimp2_mixed_pin.rs (which callers may use the
kernel).
rimp2-energy, erfc and terfc operators (shipped, same kernel). The RI-MP2
energy under the primitive erfc and terfc operators, closed-shell and
unrestricted, runs the same kernel: plain and attenuated RI-MP2
(att-rimp2, dense and QQR-screened), SCS-MP2 with two terfc operators and the
MP2 half of att-MP2+VV10. The per-element factor is the Coulomb one (a property
of the arithmetic), but S_ab = Σ_P |B_P,ia||B_P,jb| and the underflow term are
read from that operator's own dressed B_ov, whose metric is the attenuated
(P|Q); a worse-conditioned attenuated metric would appear as larger |B| and
larger S_ab / |g_ab| and is carried element by element. The bound assumes the
reference is the f64 energy of the same f64 B_ov: the attenuated RI fitting
error is common to both and is not part of it. The erf operator, composite
fitted operators (terfc_fit), SR-MP2 inside RS-MP2+RPA and OO-MP2 (the energy
kernel inside a larger algorithm whose sensitivity to this error was not
analysed) and kappa-regularised runs stay on the f64 device kernel.
Measured at the repository defaults (erfc ω = 0.420 Å⁻¹, terfc r₀ = 1.05 Å),
cc-pVDZ / cc-pVDZ-RI, all electrons correlated, against the f64 CPU energy of
the same B_ov (RI-JK RHF / UHF references). At these parameters the dressed
B_ov is as well scaled as the Coulomb one (water: max|B| 0.209 erfc, 0.211
terfc, 0.201 Coulomb; κ_E 1.306, 1.305, 1.304):
| System | Operator | naux | E_corr (Eh) | ΔE total (Eh) | per heavy atom | per electron | bound (Eh) | bound per heavy atom |
|---|---|---|---|---|---|---|---|---|
| H2O | erfc | 84 | −0.198624 | −1.7e-9 | 1.7e-9 | 1.7e-10 | 4.3e-6 | 4.3e-6 |
| H2O | terfc | 84 | −0.203297 | +6.1e-9 | 6.1e-9 | 6.1e-10 | 4.3e-6 | 4.3e-6 |
| O2 triplet | erfc | 112 | −0.337446 | −2.2e-9 | 1.1e-9 | 1.4e-10 | 3.8e-6 | 1.9e-6 |
| O2 triplet | terfc | 112 | −0.346018 | −4.5e-9 | 2.3e-9 | 2.8e-10 | 3.9e-6 | 2.0e-6 |
| CH3 doublet | erfc | 98 | −0.122053 | −6.5e-11 | 6.5e-11 | 7.2e-12 | 1.5e-6 | 1.5e-6 |
| CH3 doublet | terfc | 98 | −0.126625 | −1.8e-9 | 1.8e-9 | 2.1e-10 | 1.6e-6 | 1.6e-6 |
Every component (OS and SS, or αα, ββ and αβ) is inside its bound, and the
f32-storage part of the error is above the f64 device gate on every one, so a
caller that must stay f64 is told apart from a mixed one. These are small
systems at moderate attenuation; a strongly attenuated or poorly conditioned
metric is not measured here. Test files:
crates/ferric-mp2/tests/gpu_rimp2_mixed_attenuated.rs (needs
FERRIC_TERF_TABLE_DIR for the terfc rows) and
crates/ferric-mp2/tests/gpu_u_rimp2_mixed_pin.rs.
xc-vxc (opt-in, not in the default mixed set: it runs only when
mixed_kernels names it, under mode = "on"; on a GTX 1080 the device XC
path, f64 or mixed, was slower than the CPU, so there is no reason to run it
by default). It has run on hardware: on a GTX 1080 (Pascal, sm_61)
gpu_xc_mixed.rs passes (device error in the twin's class, no f64
fallback), with 2 extra SCF iterations on benzene and |ΔE| ≤ 3.4e-13 Eh. The Kohn-Sham XC
potential matrix of LDA and GGA functionals (closed- and open-shell; hybrids
included, their exact exchange is untouched): each grid batch's V_b = A·Xᵀ
is formed on the device from f32 A (weighted potential factors) and f32 X
(basis-function values and gradients, stored once per grid as f32 next to the
f64 copy) by f32 panel products of width b = 64 summed into an f64
accumulator. The density pass, the functional, E_xc and the V accumulation are
f64. Meta-GGAs never use it (meta-GGA τ is on the never-f32 list; the device
pass refuses mixed whenever τ is present, independently of the caller). A
precision ladder decides per Fock build: a build is mixed only when the
RMS change of some spin density since the previous build of the same SCF
exceeds τ = 1e-5; the first build of every SCF (the ladder is reset when an
SCF starts) and any one-shot V_xc are f64. At a converged exit whose last
build was mixed, that build's V_xc is replaced by an f64 rebuild at the same
density before the Fock matrix is diagonalised, so the Fock matrix and
orbitals a converged SCF returns come from f64 builds whatever density_conv
is and whichever exit accepted it (the energy needs no rebuild: E_xc comes
from the f64 density pass and functional). A wave whose panel sums could leave
the f32 range, or whose A does not round to f32, runs f64, and a missing
flush kernel, an AO value beyond the f32 range or a pool too small for the f32
copies keep the grid f64 on the device; all are counted (mixed_fallback_f64).
| Column | Value |
|---|---|
| Accumulation depth | k = ncols per element (the batch's point count, ≤ 128 by default), panels of b = 64 |
| Bound | per element |ΔV_ij| ≤ (2u₃₂ + γ_b(u₃₂) + γ_⌈k/b⌉(u₆₄))·(|A||X|ᵀ)_ij, plus the f64 reference's own γ_k(u₆₄); worst measured element 0.10 of it (water / cc-pVDZ, every 7th batch, with synthetic sine-wave potential factors in A, not functional output) |
| Energy | second order: the SCF energy is variational in the density, and only the Fock matrix of unconverged iterations carries the error |
| Mitigation | f64 panel accumulation, f64 density pass and functional, f64 closing iterations (the ladder), and an f64 V_xc rebuild at a converged exit whose last build was mixed |
| Precision | f32 operands, f32 panel products, f64 sums |
Why the ladder. With every build mixed, the f32 panel sums act as rounding noise
that changes with the density, and a tight SCF stalls on it: benzene /
cc-pVDZ PBE took 77 instead of 12 iterations at density_conv = 1e-9 and did not
converge in 200 at 1e-10. Rounding only the basis-function values (a fixed
perturbation) costs nothing; rounding only A costs 2 to 6 iterations; the
panel accumulation is the rest. τ = 1e-3, 1e-4 and 1e-5 kept 3-4, 4-5 and 6 of
about 10 builds mixed for at most 3, 3 and 2 extra iterations.
The measured effect on converged SCF energies (host twin of the kernel, the same algorithm on OpenBLAS, against the f64 SCF of the same input; default SCF and grid settings; measured before the f64 rebuild at a converged exit existed, which by construction changes neither: it runs after the exit decision and E_xc never depends on the V_xc precision; not re-measured):
| System | basis | functional | density_conv | iterations f64 / mixed | mixed builds | ΔE (Eh) |
|---|---|---|---|---|---|---|
| H2O | cc-pVDZ | PBE | 1e-6 | 9 / 9 | 6 | −2.8e-14 |
| H2O | cc-pVDZ | PBE | 1e-10 | 12 / 12 | 6 | +1.4e-14 |
| H2O | aug-cc-pVTZ | PBE | 1e-6 | 9 / 9 | 6 | +2.1e-13 |
| H2O | aug-cc-pVTZ | PBE | 1e-10 | 13 / 13 | 6 | +9.9e-14 |
| H2O | cc-pVDZ | r2SCAN (meta-GGA: f64) | 1e-10 | 13 / 13 | 0 | 0 |
| H2O⁺ (UKS) | cc-pVDZ | PBE | 1e-6 | 9 / 9 | 6 | +1.4e-14 |
| H2O⁺ (UKS) | cc-pVDZ | PBE | 1e-10 | 12 / 13 | 6 | 0 |
| benzene | cc-pVDZ | PBE | 1e-6 | 10 / 10 | 6 | −6.3e-13 |
| benzene | cc-pVDZ | PBE | 1e-10 | 13 / 15 | 6 | −5.1e-13 |
| benzene | cc-pVDZ | B3LYP | 1e-6 | 9 / 9 | 6 | +4.5e-13 |
| benzene | cc-pVDZ | B3LYP | 1e-10 | 13 / 15 | 6 | −2.8e-13 |
| n-octane | cc-pVDZ | PBE | 1e-6 | 11 / 11 | 7 | +9.1e-13 |
| n-octane | cc-pVDZ | PBE | 1e-10 | 18 / 18 | 7 | −9.1e-13 |
These are host-twin figures: the device kernel's own agreement with the twin
(same error class, within a factor 10 on max|ΔV|) and a device SCF against the
f64 host SCF are checked by crates/ferric-dft/tests/gpu_xc_mixed.rs, which
needs a device; it passes on a GTX 1080 (sm_61). Test
files: crates/ferric-dft/src/xc_batch.rs (element bound, no device),
crates/ferric-scf/tests/xc_mixed_vxc_error_map.rs (this table, and the
converged-exit Fock matrix against an independent f64 rebuild at a loose
density_conv, RKS and UKS, no device).
dfk-occ (opt-in, not in the default mixed set: it runs only when
mixed_kernels names it, under mode = "on"; on a GTX 1080 a mixed SCF saved
about 0.7 s (4%) of the wall time against the f64 device DF-K at def2-TZVP, nothing
measurable at def2-SVP, and was 3-6% slower with diffuse basis sets, so there is
no reason to run it by default). The DF-K occupied path with the dressed B
resident on the device as f32 and C_occ rounded to f32 on every build: each
Y_P = C_occᵀ·B_P is a k-panelled f32 product of width b = 64 flushed into f64,
and the SYRK and everything after it are f64. It runs behind the SCF precision
ladder (crates/ferric-scf/src/jk_ladder.rs), shared with dfj-pack: a build is
mixed only while the density moves by more than τ = 1e-5 and still contracts
(one way: once it settles or stops contracting the SCF stays f64), a build the ladder keeps f64 runs
the f64 device path on an f64 copy of B held beside the f32 one (rounded from
it on the device, bit-identical to rounding on the host; when the pool cannot
hold both copies, the f32 copy alone and the host f64 path), and a converged exit whose last build was mixed is refused
and the iteration redone in f64. The energy is first-order in the K error, so
that f64 exit, not a Fock-matrix swap, is what makes the returned energy and
density f64 builds. A B element beyond the f32 range or a flush kernel that
will not load runs the f64 device upload instead, counted (mixed_fallback_f64).
| Column | Value |
|---|---|
| Accumulation depth | stage 1: k = nbf per element, panels of b = 64; stage 2 (f64): k_chunk + nchunks |
| Bound | |K̂ − K| ≤ ε·S, S = Σ_{P,i} (|B_P||C|)(|B_P||C|)ᵀ, ε = k_error_factor_mixed (two f32 operand roundings, f32 panel sums, f64 flush), plus the f64 reference's own factor (crates/ferric-scf/src/df_k_gpu.rs) |
| Energy | first order in K, so a converged SCF's energy comes from f64 builds (the ladder's f64 exit); only unconverged iterations carry the mixed error |
| Mitigation | f64 panel flush, f64 SYRK, the precision ladder and its f64 exit |
| Precision | f32 B and C_occ, f32 panel products, f64 sums |
The measured effect on converged SCF energies: the device SCF with dfk-occ
mixed against the f64 device SCF of the same input (RI-JK, def2-universal-jkfit,
[gpu] mode = "on", default SCF settings, GTX 1080, the release CLI).
"f64 exit |dE|" is the f64 run's energy change at its last iteration, the
scale of its own termination noise:
| System | basis | method | atoms | nbf | iterations f64 / mixed | mixed K builds / total | |ΔE| (Eh) | |ΔE| per atom (Eh) | f64 exit |dE| (Eh) |
|---|---|---|---|---|---|---|---|---|---|
| H2O | cc-pVDZ | RHF | 3 | 24 | 10 / 10 | 7 / 22 | 4.3e-14 | 1.4e-14 | 1.1e-11 |
| n-butane | def2-SVP | RHF | 14 | 106 | 10 / 10 | 6 / 21 | 1.1e-13 | 8.1e-15 | 2.2e-11 |
| n-octane | def2-SVP | RHF | 26 | 202 | 10 / 10 | 5 / 21 | 0 (bit-identical) | 0 | 6.5e-11 |
| n-dodecane | def2-SVP | RHF | 38 | 298 | 10 / 10 | 5 / 21 | 1.4e-12 | 3.6e-14 | 5.9e-11 |
| n-hexadecane | def2-SVP | RHF | 50 | 394 | 10 / 10 | 5 / 21 | 2.3e-12 | 4.5e-14 | 9.0e-11 |
| n-icosane | def2-SVP | RHF | 62 | 490 | 10 / 10 | 5 / 21 | 3.2e-12 | 5.1e-14 | 1.1e-10 |
| benzene | aug-cc-pVDZ | RHF | 12 | 192 | 11 / 12 | 6 / 23 | 6.8e-13 | 5.7e-14 | 2.8e-13 |
| benzene | aug-cc-pVTZ | RHF | 12 | 414 | 10 / 12 | 6 / 25 | 3.4e-13 | 2.8e-14 | 7.5e-12 |
| OH (UHF) | cc-pVDZ | UHF | 2 | 19 | 10 / 10 | 7 / 22 | 2.8e-14 | 1.4e-14 | 2.2e-10 |
| OH (UKS) | cc-pVDZ | B3LYP | 2 | 19 | 24 / 23 | 6 / 35 | 5.7e-14 | 2.8e-14 | 5.7e-14 |
| benzene | def2-SVP | ωB97X (RSH) | 12 | 114 | 9 / 9 | 6 / 20 | 1.1e-13 | 9.5e-15 | 5.0e-10 |
The largest |ΔE| is 3.2e-12 Eh, against the 1.0e-3 Eh shipping bar, with no f64 fallback in any run. It is at or below the f64 run's own exit |dE| in every row but benzene / aug-cc-pVDZ (6.8e-13 against 2.8e-13, both at the 1e-12 scale of termination noise), so what it measures is the two runs' termination noise, not the kernel's error. The slope of ln |ΔE| against ln N is 2.29 ± 0.13 (standard error, 2 degrees of freedom) over the four alkanes with a nonzero |ΔE| and 1.27 ± 0.17 (8 degrees of freedom) over the ten such systems; it is the slope of that noise floor (the f64 exit |dE| grows with N too), not a growth law of the mixed error, which the ladder's f64 exit removes from the converged energy. With diffuse basis sets the mixed builds' own error holds the density change between builds at 5e-5 to 1e-4, above τ, while the DIIS error keeps falling. The ladder is therefore one way: the first build after a mixed one that finds the density settled (changed by at most τ) or not contracting (a change at least the previous build's) latches the SCF to f64 for good. Before the latch the ladder kept returning to mixed and DIIS stalled near τ: benzene / aug-cc-pVDZ took 44 to 103 iterations (the count depended on the last bits of the f64 K), and aug-cc-pVTZ 20, against 11 and 10 in f64; with it, 12 and 12.
Speed, n-dodecane RHF (RI-JK, the same card, 6 Rayon threads, two interleaved repeats each, whole-run wall time):
| basis | nbf | CPU | device f64 | device mixed dfk-occ |
|---|---|---|---|---|
| def2-SVP | 298 | 8.5, 7.2 s | 8.7, 7.5 s | 7.4, 7.5 s |
| def2-TZVP | 528 | 22.2, 22.1 s | 18.5, 18.3 s | 17.5, 17.8 s |
The def2-TZVP run spends 8.4-8.6 s in setup (the 3-index integrals and the
V^-1/2 dressing of the DF-K tensor), the same in every arm, and 6.6 s (f64)
against 6.0 s (mixed) building J and K over its 10 iterations. The first
build, before there are occupied orbitals, factors the guess density's
eigenvalues (314 of 528 kept) through the same occupied path.
A mixed iteration took 0.47-0.48 s against 0.60 s, the iterations the ladder
keeps f64 0.60-0.61 s (the f64 device path), and the first device build, which
uploads the f64 B and rounds its f32 copy on the device, 1.02 s against
1.12 s. Holding both copies costs device memory: 4.58 GB of dressed B at
def2-TZVP against 1.53 GB for the f32 copy alone and 3.05 GB in f64. On the
benzene rows above the mixed SCF took 1.76-1.82 s against 1.66-1.75 s
(aug-cc-pVDZ) and 4.9-5.0 s against 4.7-4.8 s (aug-cc-pVTZ).
The kernel itself (the derived K bound, the exact cross-panel flush and C_occ
rounding pins, the multi-chunk build, the operand-class twin gate, every
fallback) is checked by crates/ferric-scf/tests/gpu_dfk_mixed_kernel.rs, and
the laddered SCF energy against a bar derived from the f64 floor by
crates/ferric-scf/tests/gpu_jk_ladder.rs; both need a device and pass on a GTX
1080 (sm_61).
Known limits and negatives
- Analytic Hessians: closed-shell RHF, UHF and ROHF only (exact J/K, basis up to f functions, no solvent, no polarizable embedding; ECPs for RHF, UHF and ROHF (also with RI-J and/or RI-K) and external potentials for RHF and UHF but not ROHF; RI-J and/or RI-K for RHF, UHF and ROHF without an external potential). UHF states are not checked for internal stability or for being the lowest state: an unstable or excited UHF solution gets the Hessian of that solution. PySCF has no ROHF Hessian, so the ROHF reference is a finite difference of PySCF's analytic gradient. ROKS and every other method's frequencies take central finite differences of the analytic gradient.
- Local MP2: the integral-direct local MP2 (
rimp2with[local] integral_direct = true) has its correlation stage measured at about N1.24 (erfc) and N1.4 (Coulomb) on n-alkanes C20–C48 (6-31G / cc-pVDZ-RI, ε = 1e-4, frozen carbon cores, a calibrated pair gate; fitted to three points). That is one family of molecules in one basis: not shown to be linear, and not measured on 3-D or diffuse systems. Local MP2 withoutintegral_directstill builds the global 3-index tensor and makes no scaling claim. See The MP2 family. - DLPNO-MP2 PNO truncation is canonical and not compared like for like:
ferric's pair-driven DLPNO-MP2 with PNO truncation builds PNOs per
canonical occupied pair (
dlpno_mp2_from_b_ovdivides by a diagonaleps). A separate localized-occupied path (dlpno_mp2_localized_from_b_ov) does Boys-localized, Fock-coupled pair screening only, with no PNO or PAO truncation; ORCA localizes the occupieds and truncates PAO domains. The recoveries above are therefore reported side by side and never ranked. There is no DLPNO-CCSD or DLPNO-CCSD(T) energy driver, so ORCA's DLPNO-CCSD(T) figure is context only. The canonical pair screen is uninformative for n-butane to n-octane; with Boys-localized occupieds ferric's pair screen drops pairs (retention 0.585 frozen-core and 0.41 all-electron at C8,t_cut_pairs= 1e-4; the all-electron figure includes core pairs that the canonical screen also drops), measured only for these chains and below the locality onset. Library only. - Exact dRPA by the Riccati solve is a small-system path: its ring-product
plan holds no³·nv² numbers,
notimes the amplitudes themselves (C12 thrashed, then was killed for memory). The CLI andrun_drparefuse a run that cannot fit before the SCF; full-rankpdep-rpagives the same energy at far lower memory. - Laplace SOS-MP2, AO-sparse variant: the domain truncation is accurate,
and the radius it needs grows far more slowly than the molecule (chemical
accuracy at 3 to 5 Bohr on n-alkanes C2 to C12, radius/diameter falling from
0.52 to 0.17; a 71-atom drug molecule is within 0.05% at 4 Bohr, about 13%
of its diameter). The algebra is still dense, so
no speedup is claimed. The regression test
sos_ao_sparse_truncation_radius_is_transferable_across_sizespins the STO-3G butane/octane comparison (12 Bohr exact on both; octane worse at 3 Bohr). The C2-C12 sweep and the drug-molecule figure are measurements, not regression tests. - TDHF/RPAx C6: ~63% low regardless of gap. Do not use it for dispersion. Its static α is implemented as specified (it matches an independent build of the same kernel, see anchors) but is not physically established: water/cc-pVDZ at PBE with a 0.36 Ha scissor gives 5.20 a.u. against DOSD 9.64 (−46%). A matching static α says nothing about the dynamic response that C6 needs.
- TDDFT / TDA: closed-shell references only. Functionals without an fxc kernel (VV10, range-separated) are refused rather than run without it. B3LYP at aug-cc-pVDZ agrees with PySCF to 8.1e-4 eV on the default pruned grid and to 2.8e-6 eV on the reference's flat grid: the remainder is pruned-grid error. Meta-GGA excitations carry XC-grid error, so meta-GGA TDA/TDDFT defaults to a 150 × 434 grid: water r2SCAN TDA roots sit within 9.2e-5 eV (6-31G) and 5.5e-6 eV (aug-cc-pVDZ) of a flat 150 × 590 grid there, against 2.1e-3 eV on the 75 × 110 production grid (PBE: 4.4e-4 eV). SCAN is not grid-converged to 1e-4 eV on any grid tested: 3.6e-3 eV on the default, and flat 150 × 590 itself is 1.2e-3 eV from flat 200 × 590. See TDDFT.
- MP2 analytic polarizability (library only): one analytic path,
mp2_polarizability_analytic→analytic_alpha_relaxed(see anchors);frozen_mp2_c6_molecularand the attenuation sweeps use it. It and the relaxed dipole (static_relaxed_density_ao) refuse a frozen core. The frozen-amplitude C6 takes its frequency shape from HF α(iω), not from an MP2 dynamic response. - Periodic open-shell forces and stress refuse a non-stationary SCF
state: a ROHF/ROKS orbital gradient or a k-point UKS spin commutator above
1e-5 Ha, a stored Fock more than 1e-8 Ha from the rebuilt one, or SCF
k-points that are not the mesh's. Each tolerance and its provenance
(measured, round-off or arbitrary) is in section 6 of
crates/ferric-pbc/theory.md. - COSX gains at high angular momentum and does not gain with system size.
Measured on one thread at the flat
(50,110)grid with the overlap fit:- Against LinK on n-alkanes at def2-SVP it is slower at every size measured: 1.59× (C20), 1.09× (C32) and 1.22× (C48), with no trend toward parity. At def2-TZVP on C20 it is faster (0.67×); that is the only triple-zeta size measured.
- Against exact direct exchange on butane it is 3.7× slower at def2-TZVP (full SCF). At def2-QZVP its K build takes 90 s against about 400 s for the direct build's single J+K sweep, at a relative K error of 5.4e-5.
- Its K build grows as N1.29–N1.32 between C20 and C48 at def2-SVP.
- It applies to the Coulomb operator only: a range-separated functional
takes its exchange from density-fitted short- and long-range fitters and
ignores
k_builder = "cosx", with a warning. - Its analytic gradient is exact for RHF, RKS, UHF, UKS, ROHF and ROKS with
the overlap fit off, and for RHF and UHF with the default overlap fit, on
flat and pruned (
sgx) COSX grids. Fitted COSX with a KS functional, and fitted COSX for ROHF, are refused for gradient tasks, before the SCF runs.
Why the distinction is drawn so sharply
A quantum chemistry code can produce a plausible number in many ways that are wrong:
- a non-converged SCF returned as an ordinary result, because convergence is a flag rather than an error
- a method missing a physical term it does not mention (for example, TDDFT excitations computed without the fxc kernel)
- a fallback model silently substituted for one atom in a molecule, changing a partitioning without changing the shape of the output
- a screening or truncation threshold that happens to be safe for the test system and not for yours
None of these look like failures, and every one of them has occurred in this codebase. The remedy is to grade each capability separately and say which ones are checked against ground truth.
Testing discipline
Some properties are pinned by tests rather than asserted in prose:
- Bit-identity across thread counts for reductions and permutations:
results do not depend on
RAYON_NUM_THREADS - ERI 8-fold permutational symmetry: verified against the engine, not assumed, since the MP2 code relies on it to compute only ~1/8 of quartets
- Size-extensivity and rotational invariance of total energies
- Memory guards in both directions: a starved budget must be refused and an ample budget must still run, because an over-estimating guard is also a bug
New guards are mutation-tested: a defect is injected and the test confirmed to fail before the guard is trusted. This has caught guards that passed while proving nothing.
Related pages
- Input reference: every TOML key
- Examples: every shipped input file
- Python bindings
Input reference (TOML)
Every key the ferric CLI accepts, section by section.
Unknown keys are a hard error. The config structs in
crates/ferric-cli/src/config.rsare#[serde(deny_unknown_fields)], so a misspelled key or section aborts the run before anything is computed. That includes[external_potential], its point charges, and each[[scf.ladder]]rung.String values go through strict parsers, where an unknown value is an error at load time rather than a default. Most accept any capitalisation; the exceptions are noted per key.
This page is hand-maintained against crates/ferric-cli/src/config.rs at
commit 4b64e6ce. Where a default is applied at the point of use rather than
in config.rs, it was read from crates/ferric-cli/src/lib.rs at the same
commit. If the code and this page disagree, the code wins. For which
method.kind values exist and what each supports, see
Capabilities and validation.
Units follow the code: [molecule] geometries are Å (XYZ), point charges and
cutoffs named *_bohr are Bohr, keys named *_angstrom or documented as Å are
Å, and energies are Hartree.
A minimal file:
[molecule]
xyz = "testdata/molecules/water.xyz"
[basis]
name = "cc-pvdz"
[method]
kind = "rimp2"
[mp2]
auxbasis = "cc-pvdz-ri"
[molecule] (required)
| Key | Type | Default | Allowed values | Notes |
|---|---|---|---|---|
xyz | string | required | path | Standard XYZ in Å, relative to the working directory. Not read when [qmmm] is present (the PQR supplies the geometry). |
charge | integer | 0 | With [qmmm], applies to the QM region. | |
multiplicity | integer | 1 | ≥ 1 | Read by uhf, rohf and ksdft (UKS) for every task, and for task = "energy" only by rimp2/oo-rimp2 (UHF + unrestricted RI-MP2 / OO-RI-MP2) and the open-shell path of pdep-rpa/gw/mp2-v: UHF, or UKS when [rpa] xc is set (pdep-rpa, gw), or ROHF/ROKS for gw with [gw] reference = "rohf"; mp2-v stays UHF. Every other kind refuses > 1. See open shells. |
[basis] (required)
| Key | Type | Default | Allowed values | Notes |
|---|---|---|---|---|
name | string | — | a bundled name (case-insensitive) | e.g. sto-3g, 6-31g*, 6-311++g**, cc-pvtz, aug-cc-pvqz, cc-pvdz-pp, def2-tzvpp, def2-qzvppd, pcseg-1; the full list of 64 sets and their aliases is in Bundled basis sets. |
path | string | — | path to a Gaussian-94 file | Used only if name is absent. One of name/path is required. |
Auxiliary basis names used elsewhere (auxbasis, df_*_aux) come from the
same table: cc-pvdz-ri, cc-pvtz-rifit, aug-cc-pv{d,t,q}z-rifit,
def2-svp-rifit, def2-tzvp-rifit, def2-tzvpp-rifit, def2-qzvp-rifit,
def2-qzvpp-rifit, def2-universal-jkfit, cc-pvtz-jkfit, cc-pvqz-jkfit,
cc-pvdz-f12-optri.
cc-pvdz-rifit is an alias of cc-pvdz-ri; both names load the same set.
[method] (required)
| Key | Type | Default | Allowed values | Notes |
|---|---|---|---|---|
kind | string | required | rhf uhf rohf ksdft rimp2 mp3 oo-rimp2 att-rimp2 mp2-v scs-mp2 scs-mp2-2terfc laplace-mp2 laplace-sos-mp2 pdep-rpa rs-mp2-rpa gw bse-tda tdhf-static-polarizability ccsd ccd ccsd(t) linlccd drpa wb97x-l-v b2plyp dsd-pbep86 tda tddft | Any other value is an error. Smoke- and Spike-grade kinds print a [warning] grade line on stderr; Proven kinds and the ungraded laplace-sos-mp2 print none. rimp2, drpa and linlccd name the method and are computed exactly unless [local] sets a local approximation (see Capabilities and validation). |
task | string | "energy" | energy optimize frequencies | optimize: rhf ksdft uhf rohf rimp2 pdep-rpa only. frequencies: rhf ksdft uhf rohf only. |
[scf]
Read by every kind, because every kind runs an SCF first.
| Key | Type | Default | Allowed values | Notes |
|---|---|---|---|---|
max_iter | integer | 200 | Iteration cap; equals the library (RhfConfig) and Python default. | |
energy_conv | float | 1e-3 | Sanity bound, not a target. Convergence requires ΔP_rms < density_conv, ΔP_max < 10·density_conv and ΔE < energy_conv. ΔE has a noise floor that grows with system size, so a tight value is met only by chance and costs iterations; ferric prints a warning: [scf] energy_conv ... line when a value you wrote is below 0.3 times the estimated floor (see sharp bits); functionals, the LinK / COSX exchange builders, PCM / COSMO and double-hybrid kinds get no warning. | |
density_conv | float | 1e-6 | The convergence signal. | |
diis_size | integer | 8 | ||
diis | string | "pulay" | pulay adiis ediis (case-insensitive) | An unknown value is an error when the file is loaded. |
diis_switch_thresh | float | 1e-1 | Error level at which ADIIS/EDIIS hand over to Pulay. Ignored for pulay. If the density stops moving while the error is still above this level, the run is not reported converged: it restarts once on plain Pulay DIIS from a clean history, with a stderr warning (with inf, the level is 1e-2). A switch at or below the commutator floor (about 1e-6) can trigger the restart on a run that had converged, costing a few iterations. | |
smearing_sigma | float | none | Hartree | Fermi–Dirac smearing width. Absent means integer occupations. |
guess | string | "minao" | minao sad hcore (case-insensitive) | "sad" is an alias of "minao" (the MINAO projection guess); the free-atom-SCF SAD guess is not selectable from config. Any other value is an error. |
guess_basis | string | unset | bundled basis name | Secondary-basis (Q-Chem BASIS2) guess: the same SCF (method, functional, charge, multiplicity and [scf] settings) is converged in this basis first and projected into the target basis as the starting point. The one exception: the secondary SCF never runs the stability check or the stability descent (check_stability, stability_descent); those apply to the target solution only. An unknown basis is an error at load time, and so is a periodic [cell] system. symmetry = true refuses it. With it set, guess selects only the secondary SCF's starting point. A secondary SCF that does not converge (iteration cap, stall or divergence) does not stop the run: the target starts from its default guess, with a warning. It also runs inside the df_guess pre-stage, and again at every geometry of an optimization or finite-difference Hessian. The rimp2 and pdep-rpa optimizers (task = "optimize") build their own RHF reference without it, so that combination is an error at load time. The guess can change which SCF state is reached. See SCF. |
guess_projection | string | "fock" | fock overlap (case-insensitive) | How guess_basis is projected: fock (Q-Chem FOPPROJECTION, project the density and diagonalise the target Fock) or overlap (OVPROJECTION, project and re-orthonormalise the occupied MOs). Any other value, or setting it without guess_basis, is an error. |
soscf | bool | false | Enables the second-order (Newton) step in the SCF tail. | |
aurora | bool | false | Opt-in AURORA auxiliary-curvature accelerator. Closed-shell RHF only (kind = "rhf", multiplicity 1). Same fixed point as DIIS; a speed-up (fewer J/K builds), not a convergence rescue. | |
trah_trigger | float | unset | > 0 | Opt-in trust-region augmented-Hessian solver, engaged once the DIIS error falls below this value (rhf and uhf). A convergence rescue: fewer iterations, more wall time. |
integral_thresh | float | 1e-12 | Integral screening threshold. | |
eri_precision | float | 1e-20 | 0 to 1e-8 | libint primitive-screening precision for the SCF J/K integrals. Omitted: FERRIC_ERI_PRECISION if set, else 1e-20. 1e-14 costs up to 6e-9 Ha in E_J for atoms past Ne; 0 disables primitive screening (1.8–5.7× slower per J/K build). |
jk_storage | string | "auto" | auto memory disk direct split | Where the raw RI-J/K three-index tensor lives. auto keeps it in memory when n_aux × n_bf² × 8 bytes fit [memory] budget_gb and the shared memory pool, keeps the packed symmetric half (n_aux × n_bf(n_bf+1)/2 × 8 bytes plus 64 unpacked aux rows of scratch) when only that fits, and otherwise, on one rank with the Coulomb operator and orbital/aux angular momenta up to 4/6, builds J by Split-RI-J (as split, below; a notice says so). For a tensor still needed beyond that (DF-K's, or J's on several ranks or for another operator) it measures this machine (an untimed warm-up block, four timed integral blocks, and a cold write and read of up to 64 MiB in the spill directory) and picks the cheaper of disk and direct for a 30-pass SCF. When J and K share one auxiliary basis, the raw tensor that DF-K needs is built as before and, when it is not held in memory, freed after DF-K's copy is made, with J from Split-RI-J. A spill directory without room for the packed tensor plus headroom rules disk out. memory is an error when even the packed half does not fit. disk always spills to $TMPDIR. direct recomputes each block per pass and writes nothing. Omitted: FERRIC_JK_STORAGE if set; an invalid value in either place is an error. J from the packed, spilled and recomputed tensors is bit-identical (all three are consumed through the same packed block stream); the unpacked in-core J differs from them in the last bits (summation order, about 1e-12 Ha in the energy). This holds for J only: K and the other consumers of the tensor (the DF-K dressing follows the tensor's own block order, and the packed tier's aux block size differs from the spill file's) can differ between tiers at the level of summation order. The tier chosen by auto depends on the budget (default 0.8 × available RAM) and on the free bytes in the shared memory pool, so the same input can pick a different tier on a busier machine or with a different budget; set memory, disk, direct or split to pin it. split stores no RI-J tensor on one rank (with J and K on one auxiliary basis too: the raw tensor DF-K needs is freed once DF-K's copy is made); on several ranks it warns and stores the RI-J tensor as auto would, with that tensor's memory. Where it applies, J comes from Split-RI-J (the density, and the fitted coefficients, contracted with the three-centre integrals' Hermite intermediates as they are computed, so no (P|μν) block is formed), recomputed every build on one rank; it is RI-J to 1e-14 relative per term (terms with a large Boys argument are evaluated as point-multipole interactions, with a proven bound on that error), uses tens of MB instead of the tensor, and costs more time than a tensor held in memory: on danuglipron PBE/def2-SVP the whole SCF took 81 s against 46 s with the tensor in memory, and 584 s with direct and 600 s with disk (all four energies identical to 1e-10 Ha; peak memory 2.5, 6.4, 3.3 and 6.5 GB). With split any tensor K still needs is stored as under auto, and the raw tensor shared with DF-K is freed once DF-K's copy is made. Applies to the RI-J tensor, and to the RI-K tensor when both use the same auxiliary basis on one rank; the RI-K dressed tensor follows [memory] budget_gb. |
screening | string | "schwarz" | schwarz csb csam | csb is rigorous and never looser than schwarz. csam is not a bound. It is refused for erfc (short-range) operators. See SCF: screening. |
k_builder | string | "direct" | direct link cosx | Exchange builder. cosx with RI-J active (named, or the Kohn-Sham default) is RIJCOSX: J from RI-J, K from COSX, and the RI-K default is not applied; cosx next to an explicitly named df_k_aux is an error. link is ignored with a warning when DF-J/DF-K is active. Both are ignored with a warning for functionals with no exact exchange and for range-separated functionals. See SCF: choosing how exchange is built. |
cosx_grid | inline table | { radial = 35, angular = 194, prune = "sgx" } | angular ∈ 6/14/26/50/110/194/302/434/590; prune ∈ none sgx nwchem | The COSX SCF grid. A table without prune is flat; with prune = "sgx", angular is the peak order of the pruned rows (50/110/194/302/434/590). Only with k_builder = "cosx"; otherwise it is an error. The inner table is strict. |
cosx_final_pass | bool | true | Re-evaluate exchange once on a larger grid at the converged density and report that energy (the SCF-grid energy is printed and logged too). Without cosx_final_grid the grid is { radial = 50, angular = 302, prune = "sgx" }. Gradient tasks run without it; ROHF/ROKS skips it with a note (an explicit true there is an error). Only with cosx. | |
cosx_final_grid | inline table | none | as cosx_grid | The final-pass grid; setting it turns the pass on (cosx_final_pass = false with it is an error). RHF/RKS and UHF/UKS only. Only with cosx. |
cosx_overlap_fit | bool | true | Only with cosx; otherwise it is an error. | |
cosx_grid_schedule | bool | false | Run the first SCF iterations on a coarse pruned sgx (25,110) exchange grid and switch to cosx_grid once the largest density change falls below 1e-3. Convergence is accepted only on the production grid, DIIS restarts at the switch, and the final-grid pass is unchanged. RHF/RKS and UHF/UKS; ROHF/ROKS refuse it. Only with cosx; setting it otherwise is an error. | |
cosx_backend | string | "md3c1e" | md3c1e cosx-a | Only with cosx; otherwise it is an error. cosx-a is the slower cross-check kernel. |
cosx_screen_thresh | float | 1e-7 | ≥ 0 | Only with cosx and md3c1e. 0 disables the screen. |
cosx_half_transform | string | "sparse" | sparse dense | Only with cosx. |
cosx_fp64_multiplier | float | 1e5 with [precision] mode = "mixed" and cosx-kern in mixed_kernels, else 0 | ≥ 0 | Precision-router threshold: tau = cosx_fp64_multiplier × cosx_screen_thresh. A kept (shell pair, sub-batch) unit whose Hölder K bound is below tau is counted as f32-eligible; the exchange matrix is unchanged (every unit is still computed in f64), so the key only changes the route_* counters. Setting it requires k_builder = "cosx", [precision] mode = "mixed" and cosx-kern in mixed_kernels, and cosx-kern is not in the shipped kernel set, so in this build setting the key is always an error; a value above 0 also needs md3c1e and cosx_screen_thresh > 0. 1e5 is the multiplier recommended by Laqua, Kussmann and Ochsenfeld, J. Chem. Phys. 154, 214116 (2021). |
df_j_aux | string | none; def2-universal-jkfit for ksdft, pdep-rpa, rs-mp2-rpa, gw, bse-tda, tdhf-static-polarizability, tda, tddft | aux basis name, or "" | RI-J. With neither key set, rhf/uhf/rohf use exact 4-index J/K. df_j_aux = "" selects exact J for the kinds that default to RI-J (the SCF J/K log line then reads RI-JK via with a blank name). Unlike Python's run_dft, the CLI does not accept "exact", "none" or "off": any non-empty value is looked up as a basis name, and an unknown one fails the SCF. |
df_k_aux | string | as df_j_aux | aux basis name, or "" | RI-K. Use a JK-fit set. "" selects exact K. |
jk | string | the kind's own: four-centre, except rs-mp2-rpa (RI-JK) | exact rijk rijcosx | MP2-family kinds (rimp2, oo-rimp2, att-rimp2, mp2-v, scs-mp2, scs-mp2-2terfc, laplace-mp2, laplace-sos-mp2, rs-mp2-rpa) with task = "energy" only: how the reference SCF builds J/K. rijk = RI-J + RI-K on jk_aux; rijcosx = RI-J on jk_aux + COSX K (the cosx_* keys apply); exact = four-centre. Same rule as the Python scf_jk kwarg. Cannot be combined with df_j_aux/df_k_aux; a k_builder under rijk (including rs-mp2-rpa's RI-JK default), or other than cosx under rijcosx, is an error. oo-rimp2 accepts only exact (it rebuilds exact J/K at every orbital step; RI-JK in the OO Fock builds is not implemented), and refuses df_j_aux/df_k_aux for the same reason. A fitted mode refuses df_increments and an explicit df_guess = true, and skips the default DF-guess pre-stage. The run log's scf_jk / jk_aux / k_builder record the route the SCF takes. |
jk_aux | string | def2-universal-jkfit | bundled JK-fit basis name | The fitting basis for jk = "rijk"/"rijcosx". It is not the [mp2] auxbasis (an MP2-fit set for K costs mHa); a name without "jkfit" draws a warning. Setting it without jk, or with jk = "exact", is an error. |
level_shift | float | 0.0 | Hartree | Virtual-block shift. Left at 0 with a meta-GGA functional, the library applies 0.5. |
mom_after_iter | integer | 0 | Maximum-overlap occupation pinning after this many iterations. 0 = aufbau throughout. | |
verbose | bool | false | One line per SCF iteration. The CLI's --verbose/-v flag ORs into this. | |
df_guess | bool | on | Two-stage SCF: DF first, then exact. It applies only on the closed-shell, non-laddered path (rimp2 and the other correlated kinds). rhf/ksdft use the convergence ladder, which does not compose with it, and lib.rs prints a warning there whenever it is enabled, including by default. Mutually exclusive with an explicit df_increments = true. | |
df_guess_aux | string | def2-universal-jkfit | aux basis name | It is an error when df_guess is off. |
df_increments | bool | false | DF-corrected incremental Fock SCF. Same scope as df_guess, and warned and ignored on rhf/ksdft. | |
df_increments_aux | string | def2-universal-jkfit | aux basis name | It is an error when df_increments is off. |
check_stability | bool | false | Diagnostic only: warns if the solution is a saddle point and never fails the run. On an RHF/HF run it reports two verdicts: internal (the singlet channel: is this RHF solution an RHF minimum?) and external RHF→UHF (the triplet channel: does breaking spin symmetry lower the energy?). A stretched geometry is often internally stable and externally a saddle: water / 6-31G at r(OH) = 2.0 Å gives +1.97e-2 and −3.07e-1. For an external instability, run kind = "uhf"; re-converging RHF does not help. RKS gets the internal verdict only (no triplet XC kernel, printed as a skip); UHF/UKS get the internal verdict, which already spans the independent α/β rotations. ROHF/ROKS, range-separated and meta-GGA are skipped entirely with a printed reason. | |
stability_descent | bool | false | State selection: at a saddle of the orbital Hessian (UHF, or the RHF singlet channel), follow the downhill eigenvector and re-converge, keeping the lowest state. Turns check_stability on as well. Same as Python run_uhf(stability_descent=True). Only kind = "rhf", "uhf" or "ksdft" with task = "energy"; any other kind or task is an error. A KS functional with no stability verdict (range-separated, meta-GGA) skips the descent with a printed reason. Costs one Davidson per converged solve plus one SCF per descent. See examples/o2-uhf-stability-descent.toml. | |
symmetry | bool | false | Point-group-blocked closed-shell RHF or RKS: detect the Abelian subgroup, symmetrize and reorient the geometry, and diagonalize the Fock matrix per irrep. Only kind = "rhf" (Hartree-Fock, or RKS with [dft] functional) or kind = "ksdft", task = "energy", multiplicity 1, no [dft] dispersion, no [cell]/[qmmm]; anything else is an error (the result lives in a rotated frame, so gradients, optimizers and post-HF would be wrong; UHF/ROHF/UKS/ROKS blocking is not wired to the CLI). Under RKS the KS grid is built in the symmetry frame, where it is exactly symmetric, so no Fock or density symmetrisation is applied; a partially filled degenerate shell (stretched H2, singlet O2) is refused unless irrep_nelec chooses the occupation, because the grid can split such a shell. The energy is that of the SYMMETRIZED geometry; the printout and the run log give the point group, the per-irrep electron counts and the symmetrization shift (at most 1e-4 Bohr). No vector quantity is produced. The blocked SCF is one plain solve: [[scf.ladder]], df_guess, df_increments, soscf, aurora, trah_trigger, check_stability/stability_descent, smearing_sigma, mom_after_iter and k_builder = "cosx" are refused, and the default convergence-ladder escalation is not applied. Python: run_rhf(symmetry=True), run_dft/run_ksdft(symmetry=True) (return SymmetricRhfResult). | |
irrep_nelec | table | unset | even counts | Electrons per irrep, irrep_nelec = { A1 = 4, B2 = 2 }: pins the occupation instead of aufbau across irreps (unlisted irreps follow aufbau). Requires symmetry = true. Python: run_rhf(irrep_nelec={...}), run_dft(irrep_nelec={...}). |
ladder | array of tables | built-in ladder | see below | [[scf.ladder]] rungs. Read only by rhf and ksdft. |
[[scf.ladder]] rungs
Each rung overrides the flat [scf] settings. The rungs are walked in order,
and the ladder stops at the first converged rung. Unknown keys are an error,
as elsewhere.
| Key | Type | Default | Allowed values | Notes |
|---|---|---|---|---|
guess | string | "minao" | minao sad hcore | As [scf] guess: "sad" is an alias of "minao", and any other value (including sad-smallbasis) is an error. |
level_shift | float | inherits | ||
max_iter | integer | inherits | ||
df_j_aux, df_k_aux | string | inherits | ||
stall_window | integer | none | ||
divergence_tol | float | none | ||
restart | bool | false | true discards the incoming density. |
[dft]
| Key | Type | Default | Allowed values | Notes |
|---|---|---|---|---|
functional | string | "LDA" (for ksdft) | an XC name (LDA, PBE, B3LYP, wB97X-V, SCAN, r2SCAN, …) or a libxc name | Read by ksdft. wb97x-l-v ignores it with a warning. RPA/GW use [rpa] xc and TDDFT uses [tddft] xc instead. |
grid_prune | string | "nwchem-radial" | none off flat; nwchem nwchem-like nwchem_like; nwchem-radial nwchem_radial; defgrid1 defgrid2 defgrid3 | Prunes the main grid only; the default is nwchem-radial, and none is the flat grid (with grid_weight_cut = 0, the previous default exactly). Absent, it is the default scheme at whatever grid_radial / grid_angular set (flat at grid_angular = 50, which has no region table). nwchem: NWChem-style angular pruning by radial region, about 23% fewer points at 75×110 with energies within 1e-9 Ha of the flat grid. nwchem-radial: the same angular pruning plus fewer radial shells for light atoms (H and He 50/75 of grid_radial, Li–Ne 66/75, heavier atoms all of it), 42-47% fewer points on organic molecules; live PBE/def2-SVP SCFs moved by 2e-7 to 4e-6 Ha against the flat 75×110 grid, whose own error against a 99×590 grid is 4e-4 to 3e-3 Ha on C10-C32 alkanes. defgrid1/defgrid2/defgrid3: ORCA-style grids (ORCA 6.1 DEFGRID1-3: ORCA's radial count per element row, its five-region angular tables, H and He one angular level lower) that fix their own radial and angular sizes, so grid_radial and grid_angular are an error with them; defgrid2 is as fast as nwchem-radial and much coarser (PBE/def2-SVP: -1.2e-3 Ha on n-C20H42 and +4.2e-3 Ha on danuglipron against the flat 75×110 grid, the same range as ORCA's own DefGrid2 results). Optimize and frequency runs differentiate the pruned energy. Setting it on a meta-GGA tda/tddft run replaces that run's 150 × 434 default (see grid_radial). |
grid_radial | integer | 75 (meta-GGA TDDFT: 150) | > 0 | Radial points per atom on the main grid. Only on a run with a Kohn–Sham grid, and not with kind = "gw". Optimize and frequency runs differentiate this grid. Same as Python grid_radial=. On tda/tddft with a meta-GGA [tddft] xc (SCAN, r2SCAN, TPSS) and no main-grid key set, the default is 150 × 434 instead (see Meta-GGA response needs a finer grid); setting any of grid_radial, grid_angular, grid_prune, grid_weight_cut replaces it. |
grid_angular | integer | 110 (meta-GGA TDDFT: 434) | 6 14 26 50 110 194 302 434 590 | Lebedev order on the main grid. Same scope as grid_radial. An unsupported order is an error. Same as Python grid_angular=. On tda/tddft with a meta-GGA [tddft] xc (SCAN, r2SCAN, TPSS) and no main-grid key set, the default is 150 × 434 instead (see Meta-GGA response needs a finer grid); setting any of grid_radial, grid_angular, grid_prune, grid_weight_cut replaces it. |
grid_weight_cut | float | 1e-14 | finite, >= 0 (0 = no cutoff); e.g. 1e-14 | ORCA's WeightCut: drops every main-grid point whose final weight (radial × angular × Becke partition) is below this in absolute value. 0 turns the cutoff off (the grid is then bit-identical to the uncut one). At 1e-14, PBE/def2-SVP energies moved by under 1e-9 Ha and the XC time fell 12-15% (n-C20H42, danuglipron). A negative, NaN or infinite value is an error. Only on a run with a Kohn–Sham grid, and not with kind = "gw". Optimize and frequency runs differentiate the cut energy (the gradient drops the same points); it is discontinuous where a weight crosses the cutoff under displacement, by about cutoff × integrand. The VV10/NLC grid is not cut. Same as Python grid_weight_cut=. Setting it on a meta-GGA tda/tddft run replaces that run's 150 × 434 default (see grid_radial). |
dispersion | string | absent | d3bj, d3(bj), d3bj(<functional>), mbd, mbd(<functional>) (case-insensitive) | Only on a Kohn–Sham SCF (kind = "ksdft", or rhf/uhf/rohf with functional). task = "optimize" and task = "frequencies" run on RKS, UKS and ROKS references; frequencies uses finite differences of the KS + dispersion gradient (also under [frequencies] hessian = "auto"), so hessian = "analytic" and "analytic-moving-grid" are errors. There is no "off" value; omit the key instead. d3bj with a functional that has no D3(BJ) row in ferric's table (listed by ferric_d3::d3bj_tabulated_functionals(), the simple-dftd3 BJ table; M06-2X has only a zero-damping fit and is refused), or mbd with a functional that has no MBD@rsSCS β, is an error. |
lambda | float | 0.6 | Only for wb97x-l-v. | |
omega | float | 0.1 | Bohr⁻¹ | Only for wb97x-l-v. Note the unit differs from [mp2] omega. |
[mp2]
This section is shared by the whole MP2 family, ccsd, ccd, ccsd(t),
linlccd, drpa, the double hybrids and tda/tddft, all of which read
auxbasis and frozen_core from here. drpa, linlccd and a local rimp2
read nothing else from it (plus linlccd_variant for linlccd); any other
[mp2] key on them is an error.
| Key | Type | Default | Allowed values | Notes |
|---|---|---|---|---|
auxbasis | string | "cc-pvdz-ri"; "cc-pvdz-rifit" for tda/tddft | aux basis name | The two defaults name the same bundled set (cc-pvdz-rifit is an alias of cc-pvdz-ri). |
frozen_core | int, string or bool | 0 | integer ≥ 0, "auto", "none", true (= auto), false (= 0) | "auto" gives the standard small core for this molecule after the ECP is applied, and the run prints the resolved count. |
omega | float | 0.420 | Å⁻¹ | att-rimp2 (erfc), rs-mp2-rpa. Ignored with a warning when attenuator = "terf". An error on att-rimp2 with att_operator = "terfc". |
kappa | float | none | κ > 0, Hartree⁻¹ | κ-regularized MP2 for the exact rimp2; an error with [local]. Absent = plain MP2. |
c_os | float | 1.2 (scs-mp2), 1.27 (scs-mp2-2terfc), 1.3 (laplace-sos-mp2) | ||
c_ss | float | 1/3 (scs-mp2), 4.05 (scs-mp2-2terfc) | laplace-sos-mp2 warns and ignores it. | |
n_quad | integer | 7 | 3 5 7 | laplace-mp2, laplace-sos-mp2. Any other value is an error. |
sos_formulation | string | "mo" | mo ao ao-sparse | laplace-sos-mp2. mo and ao are exact and agree to round-off. ao-sparse is approximate and requires domain_cutoff_bohr. |
domain_cutoff_bohr | float | none | > 0, Bohr | Required by ao-sparse. An error with the other formulations. |
formulation | string | "delta-lr" | delta-lr coupled-rings | rs-mp2-rpa. |
attenuator | string | "erf" | erf terf | rs-mp2-rpa. terf needs FERRIC_TERF_TABLE_DIR. An error on att-rimp2 (use att_operator). |
r0 | float | 1.6828 (= 3.18 Bohr) | Å | rs-mp2-rpa with terf only. An error on att-rimp2 (use att_r0). |
terf_omega | float | linked, ω = 1/(r0√2) | Å⁻¹, > 0 | rs-mp2-rpa with attenuator = "terf" only (an error elsewhere). Sets the terf/terfc sharpness independently of r0. Same as Python run_rs_mp2_rpa(terf_omega=). |
att_operator | string | "erfc" | erfc terfc (case-insensitive) | att-rimp2 only (an error on any other kind). The short-range operator on the MP2 correlation; the SCF stays Coulomb. terfc is the Python run_terfc_rimp2 and needs FERRIC_TERF_TABLE_DIR. |
att_r0 | float | 1.05 | Å, > 0 | att-rimp2 with att_operator = "terfc" only; an error with erfc. |
att_omega | float | linked, ω = 1/(r0√2) | Å⁻¹, > 0 | att-rimp2 with att_operator = "terfc" only; an error with erfc (use omega) and on any other kind. Sets the terfc seam sharpness independently of att_r0, via Operator::terfc_with_omega; r0·ω = att_r0 × att_omega. Needs FERRIC_TERF_TABLE_DIR. With [local] integral-direct it takes the same schwarz_skip cut as the other operators. |
r0_sweep | array of floats | none | Å, > 0 | rs-mp2-rpa with terf only. Reuses one SCF for several r0 values. r0 is then ignored with a warning. |
r0_bonded | float | 0.75 | Å | scs-mp2-2terfc. |
r0_nonbonded | float | 1.05 | Å, > r0_bonded | scs-mp2-2terfc. |
linlccd_variant | string | "hh" | hh drivers-only full | linlccd only (an error on any other kind), exact and local alike: it selects the method. drivers-only equals RI-MP2; full adds the pp ladder (CCD-like VVVV memory). Any other value is an error. |
mp2v_r0 | float | 1.00 | Å, > 0 | mp2-v. Also sets the VV10 damping r0. It is correlated with mp2v_b in the published fit. |
mp2v_b | float | 11.0 | mp2-v. | |
mp2v_c | float | 0.0089 | mp2-v. Fixed in the paper. Changing it leaves the published parameterization. | |
mp2v_attenuator | string | "terfc" | terfc erfc | mp2-v. terfc needs FERRIC_TERF_TABLE_DIR. erfc is an unparameterized control. |
mp2v_omega | float | linked, ω = 1/(r0√2) | Å⁻¹, > 0 | mp2-v with terfc only. Setting it leaves the fitted parameterization. |
mp2v_vv10_damping | string | "terfc" | terfc none | mp2-v. none double-counts short-range correlation. |
mp2v_nlc_n_radial | integer | 50 | > 0 | mp2-v VV10 grid. |
mp2v_nlc_n_angular | integer | 50 | > 0 | mp2-v VV10 grid (unpruned). |
oo_max_iter | integer | 100 | ≥ 1 | oo-rimp2 only (closed and open shell). Orbital-optimization iterations. |
oo_grad_conv | float | 1e-4 | > 0 | oo-rimp2 only. Convergence threshold on the orbital-gradient norm. |
oo_level_shift | float | 0.1 | Hartree, ≥ 0 | oo-rimp2 only. Level shift on the approximate diagonal orbital Hessian. |
oo_diis_size | integer | 6 | ≥ 1 | oo-rimp2 only. DIIS subspace for the orbital rotations. The four oo_* keys match Python run_oo_rimp2(oo_max_iter=, grad_conv=, level_shift=, diis_size=); on any other kind they are an error. |
[cc]
Closed-shell CCSD knobs, read by ccsd and ccsd(t) only; on any other kind
the section is an error. The aux basis and frozen core stay in [mp2].
| Key | Type | Default | Allowed values | Notes |
|---|---|---|---|---|
ladder | string | "auto" | auto dense ri (lower case only) | How the particle–particle ladder is evaluated. dense holds the nv⁴ (ab|cd) block; ri rebuilds it from the RI 3-index tensors every iteration, holding at most one nv⁴-sized panel at full width (dense holds four) and narrower panels when the budget requires (same energy to round-off, less memory, slower per iteration); auto takes dense whenever its memory plan fits the budget and ri otherwise, and says so on stderr when it falls back. The run prints the route taken (ladder = dense (auto)). Python run_ccsd(ladder=), run_ccsd_t(ladder=). See Coupled cluster. |
[local]
The local approximation of a correlated method. method.kind names the
method (rimp2, drpa, linlccd or, integral-direct only, att-rimp2); this section says whether and how its
amplitudes are truncated. Without it (or with scheme = "none") the method is
computed exactly. On any other kind the section is an error. The local runs
are closed shell and task = "energy" only, and every printout and run-log
result carries the model: "<method> (exact)" with "local": null, or the
scheme, eps and kept fraction. See
The MP2 family and
Exact and local correlation.
| Key | Type | Default | Allowed values | Notes |
|---|---|---|---|---|
scheme | string | "none" | none amplitude-threshold | amplitude-threshold: drop pair amplitudes whose localized integral is at or below eps (single threshold). Any other value is an error. |
eps | float | required with amplitude-threshold | ≥ 0, finite | The threshold is part of the model and has no default. 0 keeps every amplitude and reproduces the exact method. An error with scheme = "none". |
eps_sweep | array of floats | none | each ≥ 0 | drpa only. Several ε on one SCF and one localized assembly; sorted and de-duplicated, one result block per point. Instead of eps, not with it. |
reference | bool | false | Also compute the exact method (canonical RI-MP2, canonical plasmon dRPA, exact LinLCCD) and print the local error against it. Costs the full exact calculation. An error with scheme = "none". | |
integral_direct | bool | false | rimp2 only. The integral-direct local MP2: never forms the global 3-index tensor. | |
aux_radius | float | 10.0 | Bohr, > 0 | Integral-direct only (an error otherwise). Aux fit-domain radius. |
virt_radius | float | 12.0 | Bohr, > 0 | Integral-direct only. Virtual domain radius. |
ao_tail | float | 1e-3 | ≥ 0 | Integral-direct only. 0.0 keeps every shell. |
schwarz_skip | float | 1e-5 | ≥ 0 | Integral-direct only. Cauchy-Schwarz cut on the batch 3-index triples: a triple is skipped when sqrt(max (P|P)) * sqrt(max (ab|ab)) < schwarz_skip, both factors from the run's own operator (Coulomb, erfc or terfc; the terfc factors carry a 1e-5 relative table-error inflation each, so the product is scaled by (1 + 1e-5)²). For terfc it is available for orbital shells up to g, r0 in [0.3, 2.5] Å and r0·ω ≤ 8.9, and errors outside that (set 0.0 to run anyway). See integral-direct local MP2. 0.0 evaluates every triple (bit-identical to a run without the cut). |
batch_merge | integer | 4 | ≥ 1 | Integral-direct only. |
gate_cal | float | none (gate off) | > 0 | Integral-direct only. Pair-gate calibration (~0.7 Coulomb, ~0.02 erfc ω = 1). |
virt_schwarz_kappa | float | none (off) | > 0 | Integral-direct only. ε-linked Schwarz virtual-candidate screen. |
[rpa]
Read by pdep-rpa, gw, bse-tda, tdhf-static-polarizability, and, for
trunc_thresh only, rs-mp2-rpa.
| Key | Type | Default | Allowed values | Notes |
|---|---|---|---|---|
auxbasis | string | "cc-pvdz-ri" | aux basis name | |
frozen_core | int, string or bool | 0 | as [mp2] | |
xc | string | none (HF reference) | XC name | Switches the reference to RKS, or to UKS when open shell. Required by tdhf-static-polarizability. bse-tda ignores it. pdep-rpa with task = "optimize" uses an RHF reference; xc applies only to task = "energy". |
n_quad | integer | 20 (energy runs); 16 (task = "optimize") | Python run_pdep_rpa also uses 20. | |
quadrature | string | "gauss-legendre" (CLI, Python and the Rust library QuadratureConfig::default()) | gauss-legendre gauss_legendre gl; minimax mini-max mm; chebyshev-tan chebyshev_tan chebyshev ct | |
u0 | float | 0.5 | Warn-and-ignore under minimax, which derives its own u₀. | |
trunc_thresh | float | 1e-4; 0.0 (full rank) for rs-mp2-rpa | PDEP truncation. | |
eigensolver_conv_thresh | float | 1e-6 (energy); 1e-8 (optimize) | Alias: davidson_conv_thresh. | |
chi0_sparsity | string | "dense" | dense, boys, boys:<thresh>, auto, auto:<cutoff>, auto:<cutoff>:<thresh>, each boys/auto form optionally suffixed @<radius_bohr> | |
run_diagnostics | bool | false | ||
export_eigpot_prefix | string | none | Writes <prefix>_eigpot_NNN.cube. | |
export_eigpot_count | integer | 10 | Capped at the number of eigenpotentials. | |
cube_spacing | float | 0.2 | Bohr | |
cube_margin | float | 4.0 | Bohr | |
export_npz | string | none | path | Turns on the NPZ property bundle. The compute_* keys below default to true only when this is set. |
compute_esp | bool | true | ESP at the nuclei. | |
compute_esp_surface | bool | false | ESP on a vdW shell. | |
esp_surface_vdw_scale | float | 1.4 | ||
esp_surface_n_angular | integer | 110 | Lebedev order | |
compute_polarizability | bool | true | alpha_tensor, the molecular static α. | |
compute_alpha_atomic | bool | true | alpha_atomic: the Krishtal–Senet–Van Alsenoy intrinsic per-atom α (JCP 125, 034312 (2006)), always Becke-partitioned; c6_partition does not affect it. Charge transfer between atoms is excluded; with compute_polarizability also on, the remainder alpha_ct = alpha_tensor − Σ_A alpha_atomic is exported. | |
compute_electric_field | bool | true | ||
compute_density_matrix | bool | true | ||
compute_dipole | bool | true | ||
compute_hirshfeld_charges | bool | true | Proatoms are free-atom SCF densities in the molecule's basis and SCF settings, as in Python's hirshfeld_charges. | |
compute_lowdin_charges | bool | true | ||
compute_mulliken_charges | bool | true | ||
compute_chelpg_charges | bool | true | ||
compute_resp_charges | bool | true | ||
compute_c6 | bool | true | With c6_source = "pdep", also exports alpha_ct_dynamic (nfreq, 3, 3): molecular α(iω) − Σ_A α^A(iω). | |
allow_partial_npz | bool | false | By default a bundle missing a requested property fails the run. | |
c6_source | string | "ts" | ts pdep mbd | |
c6_partition | string | hirshfeld for pdep, becke for ts/mbd | becke hirshfeld |
[gw]
Read by gw. bse-tda and tdhf-static-polarizability read frozen_core,
and tdhf-static-polarizability also reads scissor. The [rpa] section
supplies the screened interaction.
| Key | Type | Default | Allowed values | Notes |
|---|---|---|---|---|
method | string | "g0w0" | g0w0 cohsex evgw0 evgw (case-insensitive) | |
qp_mos | [lo, hi] | HOMO−2 … LUMO+2 | absolute MO indices, half-open | |
max_ev_iter | integer | 20 | evGW/evGW0 outer loop. | |
ev_conv_thresh | float | 1e-4 | Hartree | |
pade_npts | integer | 0 (= [rpa] n_quad) | ||
qp_newton_damp | float | 1.0 | ||
frozen_core | int, string or bool | falls back to [rpa] frozen_core | as [mp2] | Also overrides the PDEP frozen core, so W and Σ agree. |
scissor | float | 0.0 | Hartree | tdhf-static-polarizability only. At 0.0 some molecules hit a negative α diagonal and the run errors. The remedy is about 0.3–0.4 Ha. |
reference | string | "uhf" | uhf rohf (case-insensitive) | gw on an open-shell molecule only: the reference is UHF or ROHF (UKS or ROKS with [rpa] xc). An error on a closed-shell molecule or another kind. Same as Python run_u_gw(reference=). |
[tddft]
Read by tda and tddft. Also set [mp2] auxbasis (see above).
| Key | Type | Default | Allowed values | Notes |
|---|---|---|---|---|
n_roots | integer | 3 | ||
xc | string | none (HF reference: CIS or TDHF) | XC name | Selects the reference functional and the f_xc kernel. Meta-GGA, VV10 and range-separated functionals are refused. |
c_hf | float | the functional's short-range exact-exchange fraction; 1.0 with no xc |
[optimize]
Read when task = "optimize".
| Key | Type | Default | Allowed values | Notes |
|---|---|---|---|---|
max_steps | integer | 100 | ||
g_max_thresh | float | 4.5e-4 | Hartree/Bohr | |
g_rms_thresh | float | 3.0e-4 | Hartree/Bohr | |
e_conv | float | 1e-6 | Hartree | |
trust_radius | float | 0.1 | Initial step size. | |
coordinates | string | "cartesian" | cartesian cart; internal internals redundant-internal redundant_internal |
[frequencies]
Read when task = "frequencies".
| Key | Type | Default | Allowed values | Notes |
|---|---|---|---|---|
hessian | string | "auto" | auto; analytic; analytic-moving-grid; fd finite-difference | auto uses the analytic Hessian for RHF (closed shell), UHF (aufbau occupations) and ROHF (kind = "rohf" without [dft] functional) with the Coulomb operator and exact four-centre J/K (RHF, UHF and ROHF also with density-fitted J and/or K: [scf] df_j_aux and/or df_k_aux set, ω = 0 only, no external potential (an ECP is analytic with RI), a fitting-basis metric with no eigenvalue below 1e-10 (RI-K: none below 1e-7 and cond(V) at most 1e8 for auto), and RI-K memory that fits the [memory] budget (it grows like N⁴: about 10 GB for 30 atoms with 300 basis functions, the same for the two spin channels of UHF and ROHF); no range-separated RI-K, no COSX), a basis up to f functions and a libint2 with second derivatives. For RHF and UHF, ECPs (on the default quadrature backend; FERRIC_ECP_BACKEND=libecpint is not analytic), external point charges, Gaussian-smeared external charges and a uniform external field are analytic; for ROHF an external potential is not (an ECP is). Implicit solvent, polarizable embedding, fractional occupations, MOM and constraints are not analytic for any reference. For closed-shell Kohn-Sham (kind = "ksdft", or rhf with [dft] functional) auto uses the analytic-moving-grid Hessian wherever it applies (below); UKS, ROKS, every KS case it refuses and systems whose response AO table (basis functions × grid points × 4 for a GGA, 8 bytes each) exceeds 2 GiB (on the default 75×110 grid, a GGA above about 8,100 basis functions × atoms) use finite differences, decided before the SCF; an error it raises after the SCF (a non-converged SCF included) is returned, as for the Hartree-Fock routes. Anything else uses finite differences. analytic is the Hartree-Fock analytic Hessian and an error where it does not apply (every Kohn-Sham reference included). analytic-moving-grid is the analytic closed-shell Kohn-Sham Hessian on the production (moving) grid, Becke weight response included, so it is the Hessian of the surface the KS gradient differentiates and agrees with fd to the finite-difference error. It applies to kind = "ksdft" (or rhf with [dft] functional) on a closed shell, LDA or GGA functionals, pure or global hybrid, with exact or density-fitted J and K. It is an error, before the SCF, for Hartree-Fock, open-shell (UKS/ROKS) references, meta-GGA, VV10, range-separated hybrids, ECPs, external potentials, [dft] dispersion, and what the analytic RHF Hessian refuses (COSX, implicit solvent, fractional occupations, MOM). The output prints Hessian = analytic, analytic-moving-grid or finite-difference; the JSON run log carries a frequencies record with the full-precision wavenumbers and Cartesian Hessian. |
delta | float | 5e-3 | Bohr, finite and > 0 | Central-difference step, finite-difference Hessians only. Check the printed Hessian asymmetry: it is zero in exact arithmetic. |
[memory]
| Key | Type | Default | Allowed values | Notes |
|---|---|---|---|---|
budget_gb | float | auto | finite and > 0 | Precedence: this key, then FERRIC_MEM_BUDGET_GB, then the legacy FERRIC_OOC_BUDGET_GB/FERRIC_ERI3_BUDGET_GB, then 0.8 × available RAM, then 2 GiB. A value of 0, a negative value or NaN is an error; omit the key for auto. It bounds the ledgered allocations, not total process memory. |
three_index_budget_gb | float | — | Deprecated alias. budget_gb wins if both are set. |
[gpu]
Optional CUDA backend. A default build has no GPU code: there mode = "on" is an error and mode = "auto" prints a notice and runs on the CPU. A GPU run is deterministic run to run on one device but is not bit-identical to the CPU run (a different summation order, like FERRIC_BLAS_THREADS above 1).
| Key | Type | Default | Constraint | Meaning |
|---|---|---|---|---|
preset | string | "off" | off, auto, on, mixed, auto-mixed | One word that sets mode and precision together; see the presets table below. The root-level key gpu = "mixed" is the same as [gpu] preset = "mixed" (a file cannot hold both a root gpu string and a [gpu] table). A mode, precision or mixed_kernels key, or its env var, that disagrees with the preset is an error naming both; one that agrees is kept. device, memory_gb and min_flops combine with any preset. Env: FERRIC_GPU_PRESET. |
mode | string | "off" | off, auto, on | auto uses a device when one is usable and otherwise prints a notice and runs on the CPU; on makes an unusable device an error. The Kohn-Sham XC grid uses the device only under on: under auto it stays on the CPU, because it was measured slower on the device (GTX 1080; see installation). Env: FERRIC_GPU. |
device | integer | 0 | a CUDA ordinal | Which device to use. Env: FERRIC_GPU_DEVICE. |
memory_gb | float | 0.8 x free | finite and > 0 | Device-memory pool (decimal GB). A GEMM that does not fit runs on the CPU. Env: FERRIC_GPU_MEM_GB. |
min_flops | integer | 549755813888 | Smallest 2*m*n*k that an f64 einsum! matrix product sends to the device; the default is the rounded-up value of a crossover rule applied to products up to 6144³: the device does not clearly beat 6 CPU cores on a single f64 product at any shape measured (at best a 2-8% median win at 6144³ in two of three runs, a loss in the contested run, and at 4096³ and below the CPU won). Products of 2^39 FLOP or more go to the device, and that region is unmeasured. The device RI-MP2 energy does not consult it. Env: FERRIC_GPU_MIN_FLOPS. | |
precision | string | "f64" | f64, mixed | Deprecated alias of [precision] mode. It still works; given together with [precision] mode, the two must agree (a different value is an error). The run prints a deprecation note. Env: FERRIC_GPU_PRECISION. |
mixed_kernels | string array | the kernels this build ships | as [precision] mixed_kernels | Deprecated alias of [precision] mixed_kernels. It still works; given together with [precision] mixed_kernels, the two must agree (a different list is an error). Env: FERRIC_GPU_MIXED_KERNELS (comma-separated). |
gpu = "mixed" # same as [gpu] preset = "mixed": device on, mixed precision for every shipped kernel
GPU presets
A preset fills in every knob below that is not given; the filled-in knobs print with [source: preset] in the audit lines.
| Preset | mode | precision | mixed_kernels |
|---|---|---|---|
off | off | f64 | the build default |
auto | auto | f64 | the build default |
on | on | f64 | the build default |
mixed | on | mixed | every kernel this build ships (rimp2-energy: the Coulomb, erfc and terfc RI-MP2 energy, closed-shell and unrestricted) |
auto-mixed | auto | mixed | every kernel this build ships (rimp2-energy: the Coulomb, erfc and terfc RI-MP2 energy, closed-shell and unrestricted) |
preset = "off" is the same as no [gpu] key. A narrower mixed_kernels list is allowed under mixed and auto-mixed; naming a kernel the build does not ship is an error. Precedence for the preset itself: the command-line flag ferric --gpu <preset> input.toml (or --gpu=<preset>), then [gpu] preset or the root gpu key, then FERRIC_GPU_PRESET, then off. The flag labels its audit line [source: command line]; a mode, precision or mixed_kernels key or env var that disagrees with the flag is an error naming both, and --gpu given twice, without a value or with an unknown name is refused (exit code 2).
ferric --gpu mixed input.toml
[precision]
Which kernels may run in mixed precision, independent of the device. [gpu] chooses only device placement; its precision and mixed_kernels keys are deprecated aliases of the two keys here (either spelling works; both given with different values is an error). Every result records what ran in its precision provenance record (see Run logs). The default, no [precision] section, is f64 everywhere and bit-identical to a file without the section.
| Key | Type | Default | Constraint | Meaning |
|---|---|---|---|---|
mode | string | "f64" | f64, mixed | mixed runs the kernels in mixed_kernels with f32 storage and f32 panels accumulated in f64; every other contraction stays f64. A kernel that runs on a device needs [gpu] mode auto or on; with [gpu] mode = "off" (CPU-only mixed precision) only kernels with a CPU mixed path are accepted, and naming or defaulting to a device kernel is an error that lists the CPU kernels this build admits (dfj-f32, opt-in; cosx-kern is a CPU kernel but not shipped). A mixed result is not an f64 result: its error is bounded, measured and documented on the validation page, not validated against a reference code. Python: ferric.configure_precision(mode=...). Env: FERRIC_GPU_PRECISION. |
mixed_kernels | string array | the kernels this build ships | rimp2-energy, ccsd-amplitudes, dfk-occ, cosx-kern, dfj-pack, xc-vxc, dfj-f32 | Which kernels may run in mixed precision; requires mode = "mixed". This build ships rimp2-energy: with mode = "mixed" the device RI-MP2 energy keeps B_ov as f32 and accumulates its panels in f64 (the default precision stays "f64", so shipping the kernel changes nothing until you ask). rimp2-energy covers the RI-MP2 energy under the Coulomb, erfc and terfc operators, closed-shell and unrestricted (open-shell, one f32 B_ov per spin); the erf operator, composite fitted operators, SR-MP2 in RS-MP2+RPA, OO-MP2 and kappa-regularised runs use the f64 device kernel. xc-vxc (the Kohn-Sham XC potential matrix of LDA and GGA functionals from f32 operands with f64 accumulation, used only in SCF iterations whose density is still changing; see the validation page) is opt-in: it is admitted when the list names it, but it is not in the default set, so mode = "mixed" or gpu = "mixed" alone leaves it out; naming it under any [gpu] mode other than "on" is an error, since the XC grid stays on the CPU under auto (measured slower on the device; see installation). dfk-occ (the device DF-K occupied path with the dressed 3-index tensor resident as f32 and f64 panel accumulation, used only in SCF iterations whose density is still changing) is opt-in on the same terms, named in the list and under mode = "on": its converged energies match the f64 device SCF to within the f64 run's own termination noise, but a mixed SCF saved only about 4% of the wall time on the one device measured, and was 3-6% slower with diffuse basis sets (see the validation page). dfj-pack (the device RI-J with the packed raw 3-index tensor resident as f32, f64 accumulation) is named in this build but not shipped. dfj-f32 (the CPU RI-J with its stored 3-index tensor kept as f32, f64 accumulation, half the memory) is opt-in: it runs when the list names it, under any [gpu] mode; every SCF RI-J build reads the rounded tensor, so the converged energy carries a first-order error of the µEh scale (PBE/def2-SVP: -0.9 µHa on n-C20H42, +5.4 µHa on danuglipron, whose peak memory fell from 8.0 to 6.1 GB and J time from 5.7 to 3.5 s) (there is no SCF ladder and no f64 exit), Hessian, response and gradient RI-J builds stay f64, and a gradient after an f32 SCF is taken at the f32 density. Naming a kernel this build does not ship yet is an error, and a build that ships no mixed kernel refuses mode = "mixed" ("no mixed-precision kernel is available in this build"). Kernels that are not names here (SCF diagonalisation, DIIS, metric inverses, GW, grid construction and Becke weights, the XC density and the functional, meta-GGA τ) never run below f64. One kernel name per list item: an empty list or a comma inside an item is an error. Python: ferric.configure_precision(mixed_kernels=[...]). Env: FERRIC_GPU_MIXED_KERNELS (comma-separated). |
[gpu]
mode = "on"
[precision]
mode = "mixed"
mixed_kernels = ["rimp2-energy"]
[output]
| Key | Type | Default | Allowed values | Notes |
|---|---|---|---|---|
json | string or bool | <input-stem>.ferric.jsonl beside the input | a path, true (the default path), false (off) | On by default. See Run logs. --json <path> and --no-json override it. |
[qmmm]
When present, the PQR supplies both the geometry and the MM charges. It
cannot be combined with [external_potential]; doing so is an error.
| Key | Type | Default | Allowed values | Notes |
|---|---|---|---|---|
pqr | string | required | path | Geometry in Å and charges in e. The element is read from the atom name. |
qm_indices | integer array | [] | zero-based | Use this, or qm_seeds + qm_radius_angstrom, but not both. |
qm_seeds | integer array | [] | zero-based | |
qm_radius_angstrom | float | none | Å, > 0 | Requires qm_seeds. |
link_bonds | array of [qm, mm] | [] | Required when the cut crosses a covalent bond. | |
boundary_scheme | string | "delete-host" | keep delete-host rc rcd | Setting a non-default value without link_bonds is an error. |
See QM/MM. Smeared PQR charges, polarizable sites and MM
force fields are Python only. (Smeared charges outside QM/MM are available
through [external_potential] with width.)
[pcm]
IEF-PCM implicit solvent (ferric-pcm). Absent means vacuum. Honoured on
task = "energy" by rhf, uhf, rohf, ksdft and pdep-rpa (for
pdep-rpa the RPA correlation is evaluated on the solvated reference), and on
task = "optimize" by rhf, uhf, rohf and ksdft (their gradients carry
the analytic IEF-PCM term). Any other kind, any other task (frequencies:
there is no PCM Hessian), [cosmo] in the same file, or pdep-rpa with
[rpa] export_npz is an error. The solvent
table and checks are shared with Python run_rhf(solvent=...). See
examples/water-pcm.toml.
| Key | Type | Default | Allowed values | Notes |
|---|---|---|---|---|
epsilon | float | — | finite and > 1 | Dielectric constant. Give exactly one of epsilon and solvent. |
solvent | string | — | water (78.4) dmso (46.7) methanol (32.6) ethanol (24.9) acetone (20.7) dichloromethane/dcm (8.93) thf (7.43) chloroform (4.71) toluene (2.38) hexane (1.88), case-insensitive | Dielectric constants at 298 K. An unknown name is an error. |
lebedev_order | integer | 110 | 6 14 26 50 110 302 | Tesserae per atomic sphere. |
[cosmo]
Conductor-like implicit solvent, applied to every SCF variant. It cannot be
combined with [pcm]. task = "energy" only: COSMO has no analytic
gradient, so task = "optimize" and task = "frequencies" with [cosmo] are
errors. Use [pcm] (which has a gradient) for optimization.
| Key | Type | Default | Allowed values | Notes |
|---|---|---|---|---|
epsilon | float | required when the section is present | finite and > 1 | Default in code is 78.39, but serde has no default for this key. |
radius_scale | float | 1.17 | > 0 | Multiplies Bondi radii. |
lebedev_order | integer | 110 | 6/14/26/50/110/302 | |
s_matrix_kind | string | "GaussianSmeared" | GaussianSmeared PointCharge | Serde variant names, case-sensitive. |
[external_potential]
Strict, like every other section: an unknown key here or inside a point charge is an error.
| Key | Type | Default | Allowed values | Notes |
|---|---|---|---|---|
point_charges | array of { q, x, y, z, width } | [] | q in e; x, y, z in Bohr; width in Bohr, finite and > 0 | Written as [[external_potential.point_charges]] tables. width is optional: with it the charge is Gaussian-smeared, density ∝ exp(−r²/width²) and potential q·erf(r/width)/r (the Python smeared_charges=); without it the charge is a point. See examples/water-rhf-smeared-charge.toml. The order of the list changes the one-electron potential only by summation roundoff (reordering 6458 charges: 3e-15 to 1e-14 relative) and ferric does not reorder it; a different SCF iteration count was seen only for the closed-shell direct-J/K path, on rc7 and, on main, with FERRIC_SCF_INCREMENTAL=1 below the incremental noise floor (see Sharp bits, "energy_conv is a sanity bound"). |
field | [Ex, Ey, Ez] | none | atomic units | Uniform electric field. |
With both empty, the run is identical to a vacuum run.
[cell]
A 3-D periodic system. When present, the run goes to the periodic drivers
instead of the molecular ones. The [molecule] XYZ supplies the atoms of the
reference cell (in Å, like every XYZ). Energies are Hartree per cell.
method.kind must be rhf, uhf, rohf, ksdft, rimp2 (periodic MP2)
or pdep-rpa (periodic dRPA). rhf/uhf/rohf with [dft] functional run
RKS/UKS/ROKS. Any other kind is an error. At the Gamma point (no kmesh) RKS, UKS and ROKS also run range-separated (CAM) GGA hybrids with either jk: any libxc one is accepted (HSE06, or a libxc ID such as HYB_GGA_XC_CAM_B3LYP or HYB_GGA_XC_WB97X; the names CAM-B3LYP and wB97X do not resolve); HSE06 and CAM-B3LYP are pinned against PySCF, open-shell wB97X is accepted, not pinned (the attenuated exchange is the k = 0 point of the k-point range-separated exchange; PySCF Gamma pins agree to 1e-12; with jk = "rsgdf" the full-Coulomb fit is built twice, once for the Gamma J and once inside the attenuated exchange, so the build costs about 1.9× a plain Gamma run, and the exchange also contracts a J it discards every iteration; with jk = "dense" the tensors take 40 nao⁴ bytes, not 8, and max_eri_gb counts all of them); range_split with a range-separated hybrid is an error; task = "optimize" with HSE06 or CAM-B3LYP runs on the RKS, UKS and ROKS routes with either jk (the analytic forces of gamma_rsh::gamma_rks_rsh_gradient, gamma_rsh::gamma_uks_rsh_gradient and gamma_rsh::gamma_roks_rsh_gradient, the 1×1×1 k-point force on the SCF's own dense-AFT or RS-GDF exchange); other range-separated hybrids (wB97X) are energy only. The Gamma-point UKS and ROKS routes run the SCAN / r2SCAN energy with jk = "dense" (per-spin τ; jk = "rsgdf", task = "optimize" and any other meta-GGA are errors; kmesh = [1, 1, 1] is the Gamma point and runs the UKS SCAN / r2SCAN force; the k-point ROKS SCAN / r2SCAN force and stress are library-only, #498). task = "optimize" works at the
Gamma point for the SCF routes (RHF, UHF, ROHF, RKS, UKS, ROKS) with either
jk: it moves the atoms at a fixed lattice using the analytic periodic
force, and prints the final gradient (Hartree/Bohr per cell). A Gamma-point
RKS with SCAN / r2SCAN / TPSS optimizes too, with jk = "dense" only (the
force is the 1×1×1 k-point force fed the Gamma SCF; it builds the 1×1×1
dense kernels, 32 nao⁴ bytes on top of the 8 nao⁴ Gamma ERI, and max_eri_gb
counts all of them before the SCF);
jk = "rsgdf" with a meta-GGA and task = "optimize" is an error. With kmesh it
works for rhf, uhf and rohf (no functional) and for Kohn-Sham (ksdft,
or rhf/uhf/rohf with [dft] functional: RKS, UKS, ROKS) using the analytic
k-point force, with either jk on every route except the dense-only cases
named below (SCAN / r2SCAN on the UKS route need jk = "dense"; unsplit
RS-GDF: range_split with task = "optimize" is an error). The functionals are LDA,
GGA and global hybrids such as PBE0 and B3LYP on every Kohn-Sham route, plus
the pure meta-GGAs SCAN and r2SCAN on the closed-shell route (either jk) and
the UKS route (jk = "dense"), TPSS on the closed-shell route (jk = "dense"), and the range-separated hybrids HSE06 and CAM-B3LYP (HYB_GGA_XC_CAM_B3LYP;
other range-separated hybrids such as wB97X are energy only) on every
Kohn-Sham route (RKS, UKS, ROKS; either jk); the force
differentiates the energy's own XC grid (n_radial/n_angular/neighbour_cutoff)
and, for a hybrid, its exxdiv. It is an error for range-separated hybrids
other than HSE06 / CAM-B3LYP, VV10 functionals, TPSS on RS-GDF or an open-shell route, hybrid
meta-GGA, meta-GGA forces on ROKS (its SCAN / r2SCAN energy runs, jk = "dense"; the library k-point ROKS SCAN / r2SCAN force and stress exist but are not wired, #498) and UKS meta-GGA forces and stress on RS-GDF,
and for rimp2/pdep-rpa. Cells whose basis carries an ECP are supported by the
k-point force and stress of every route above (RHF, UHF, ROHF, RKS, UKS, ROKS). The lattice is fixed in task = "optimize" (no
stress relaxation). The analytic stress of a k-point SCF is printed by a
task = "energy" run with stress = true (see the key below): rhf, uhf,
rohf, ksdft and the [dft] functional routes (RKS, UKS, ROKS), either jk
except where the stress key below says jk = "dense",
the 3×3 tensor in Hartree/Bohr³ and in GPa, and the same two tensors in the
run log's run_end record (extra.stress_hartree_per_bohr3, extra.stress_gpa,
full precision; see run logs). It equals the Python
with_stress=True tensor bit for bit (Python: run_rhf_kpts, run_uhf_kpts,
run_rks_kpts, run_uks_kpts, run_roks_kpts, run_rohf_kpts), with the
gate shared: every route runs with either jk, the closed-shell SCAN /
r2SCAN stress and the range-separated hybrid (HSE06, CAM-B3LYP) stress of
every Kohn-Sham route (RKS, UKS, ROKS) included, except the UKS SCAN /
r2SCAN stress, which needs jk = "dense" (unsplit RS-GDF: range_split
with stress = true is an error). The k-point
force and stress use an explicit
functional allowlist that is separate from the energy gate
(ferric_pbc::kxcgrad::check_deriv_functional, per route and J/K, wrapped by
check_kpoint_deriv_functional, which the Python
bindings call too). The run_end record of an optimization carries the
returned geometry and the gradient there at full precision
(extra.final_geometry_bohr, extra.final_gradient_hartree_per_bohr). solve_krks (every k-point Kohn-Sham energy run, run_rks_kpts and the CLI included) clones S(k) and h(k) transiently during the SCF (2 N_k n² × 16 bytes, not charged to the budget); the periodic XC Bloch AO cache of the SCF does not follow [memory] budget_gb. Each optimizer step runs a fresh SCF from the core guess; at a k-point mesh with uhf it can land in a different spin solution between steps, which shows as a visible jump in the step energies (nothing is hidden). The force is taken through the hcore and J/K objects the SCF built, which stay in memory for the force: their bytes are charged against the same memory budget ([memory] budget_gb with jk = "rsgdf"), and with jk = "dense" the kernels count against max_eri_gb while they exist. task = "frequencies" is an error.
The periodic run reads only [cell], [scf] max_iter, [scf]
density_conv (Gamma point) or energy_conv (k-point mesh), [scf]
level_shift (k-point rohf / ROKS only), [dft] functional, [memory]
(RS-GDF only) and [optimize]. Any other section
or key is an error. A charged cell is an error.
| Key | Type | Default | Allowed values | Notes |
|---|---|---|---|---|
lattice | 3×3 float array | required | rows are the lattice vectors | In unit. |
unit | string | "angstrom" | angstrom bohr | Applies to lattice, omega and gdf_omega (as their inverse), neighbour_cutoff and neighbour_switch. |
kmesh | [n1, n2, n3] | none (Gamma point) | each ≥ 1 | Selects the k-point drivers: rhf, uhf, rohf (no functional); Kohn-Sham (ksdft, or rhf/uhf/rohf with [dft] functional): RKS with LDA, GGA, global-hybrid and SCAN / r2SCAN / TPSS functionals (and range-separated (CAM) GGA hybrids such as HSE06, HYB_GGA_XC_CAM_B3LYP and HYB_GGA_XC_WB97X, with either jk; forces and stress for HSE06 and CAM-B3LYP with either jk, wB97X energy only), UKS (ksdft with multiplicity > 1 included) with the same functionals as RKS except TPSS (SCAN / r2SCAN energy with either jk, forces and stress with jk = "dense"; the range-separated hybrids with either jk: the energy of any libxc CAM GGA hybrid, HSE06 and CAM-B3LYP pinned, open-shell wB97X accepted, not pinned, and the force and stress of HSE06 / CAM-B3LYP), and ROKS with LDA, GGA, global-hybrid and range-separated hybrid functionals (either jk; range-separated forces and stress for HSE06 / CAM-B3LYP as for UKS) and the SCAN / r2SCAN energy (jk = "dense"); rimp2 and pdep-rpa. task = "optimize" and stress = true run with HSE06 / CAM-B3LYP on RKS, UKS and ROKS (either jk) and with TPSS and UKS SCAN / r2SCAN on jk = "dense". Not yet supported with kmesh, each an error naming the gap: ROKS meta-GGA forces and stress, a ROKS meta-GGA with jk = "rsgdf", task = "optimize" or stress = true with a range-separated hybrid other than HSE06 / CAM-B3LYP, a UKS meta-GGA force or stress with jk = "rsgdf", VV10 functionals, TPSS on UKS/ROKS, task = "optimize" or stress = true with TPSS on jk = "rsgdf", meta-GGAs other than the pure SCAN / r2SCAN / TPSS, and task = "optimize" for rimp2/pdep-rpa (see task). Open-shell k-point KS and ROHF use ewald_start; [cell] n_radial/n_angular/neighbour_cutoff apply to the Kohn-Sham routes and, with a 1×1×1 mesh, the runs equal the Gamma-point ones. |
centring | string | "gamma" | gamma mp | Requires kmesh. gamma is the Gamma-centred mesh (contains Γ; PySCF make_kpts(n)); mp is the shifted Monkhorst-Pack mesh (PySCF with_gamma_point=False). Energies, task = "optimize" forces and stress = true all accept either. |
exxdiv | string | "ewald" | ewald none | Exchange G = 0 treatment. |
jk | string | "dense" | dense rsgdf | dense is a toy-scale dense AFT tensor. |
auxbasis | string | none | bundled basis name | Required by jk = "rsgdf"; an error with dense. |
range_split | bool or float | false | false, true (λ = 1), or a number > 0 (λ) | Opt-in RS-GDF range split: moves the short-range blocks whose Fourier transform converges in the long-range G sphere into G space. Orbital primitives with exponent ≤ λω²/2 and aux primitives with exponent ≤ λω² count as smooth, where ω is the RS-GDF split (gdf_omega, default 1 Bohr⁻¹; not the omega key). λ ≤ 1 adds no G vectors; the energy matches the unsplit build to fitting precision. Requires jk = "rsgdf" and task = "energy" (Gamma point or kmesh). An error with task = "optimize": the optimizer is not wired to the range-split forces. |
omega | float | min(2.5 √π / V^(1/3), 0.9636 × ω_gdf), ω_gdf = gdf_omega in Bohr⁻¹ | > 0, in unit⁻¹ | Nuclear-attraction Ewald split. Any value gives the same energy to truncation (~1e-11 Ha); the default balances the real- and reciprocal-space nuclear-attraction cost, and its cap keeps the reciprocal-space sphere inside the RS-GDF one (so the cap scales with gdf_omega). |
gdf_omega | float or "auto" | 1 Bohr⁻¹ | finite, > 0, in unit⁻¹; or "auto" | RS-GDF Ewald split ω. Any value gives the same energy to the fitting precision; it moves work between the short-range lattice sums (radii ∝ 1/ω) and the long-range G sphere (|G| ≤ 2ω √ln(10¹³)). The range_split thresholds and the RS-GDF forces follow it. Requires jk = "rsgdf" (an error with dense). Same as Python gdf_omega= (Å⁻¹). "auto" (opt-in; the default is unchanged) picks ω from a cost model of the cell: it prices the short-range triplet walk (the build's own screen, sampled) against the long-range G sum at ω = 0.25..2 Bohr⁻¹ and leaves 1 Bohr⁻¹ unless another value is predicted at least 10% cheaper. On the study cells (Gamma RHF, cc-pVDZ / cc-pVDZ-RI, 6 threads) sparse molecular crystals run 1.4-2.4× faster (dry ice 20.0 → 11.8 s) and dense cells keep 1 Bohr⁻¹ at a cost of ~0.2-0.5 s for the choice. Gamma-point task = "energy" only; an error with kmesh and task = "optimize". The chosen value is printed in the header and on stderr. Python: ferric.auto_gdf_omega(...) returns the value to pass as gdf_omega=. |
sr_column_rotation | bool | on for a jk = "rsgdf" run (Gamma or kmesh), off with jk = "dense" | true false | Column rotation of generally contracted shells (the separate s/p contraction columns of cc-pVXZ-style bases) in the short-range walks: the RS-GDF short-range 3-centre sum and the hcore short-range nuclear attraction run on rotated columns and are transformed back exactly (at a kmesh the back-transform acts on the short-range residue bins and on V_SR(k) before the Bloch phase sums, the long-range and G = 0 parts stay in the parent basis). The energy matches the unrotated build to the screening precision (≤ 1.5e-11 Ha/cell at gdf_omega ≥ 0.7 on dry ice and diamond, cc-pVDZ) with fewer short-range integral calls (dry ice 25.9 → 22.4 s, diamond 7.84 → 3.08 s at 6 threads, gdf_omega = 1, range_split = true). The analytic forces and stress (task = "optimize") differentiate the same rotated walks, with the derivative blocks evaluated on the rotated columns, so optimisations keep the saving; at the Gamma point forces and stress equal the unrotated ones to ≤ 1e-12 (Ha/Bohr, Ha) on the test cells; at a kmesh the gap to the unrotated derivative is set by the screen (force 1.5e-10 Ha/Bohr, stress 6.5e-9 Ha on H2 cc-pVDZ at hcore/fit precision 1e-9/1e-10, shrinking with the precision). A basis with nothing to rotate (STO-3G, Pople, segmented) runs the unrotated build bit for bit. Absent, it is on for a jk = "rsgdf" run (Gamma point or kmesh) and off with jk = "dense"; the header line sr_column_rotation = says which and why. false turns it off everywhere. true is an error with jk = "dense". The k-point analytic forces (task = "optimize" with kmesh) and the library's k-point stress differentiate the rotated walks (the derivative blocks on the rotated columns, the residue weights transformed with Tᵀ W T). The stage table counters report how many columns were rotated: hcore SR rotated columns and rsgdf SR3 rotated columns at the Gamma point, k hcore SR rotated columns and k rsgdf SR3 rotated columns at a kmesh (0 = unrotated). Same as Python sr_column_rotation= on the Gamma bindings (None = this default). |
max_eri_gb | float | 0.5 | > 0 | Dense tensor cap. An error with rsgdf. |
ewald_start | string | "staged" | staged direct | Open-shell SCF with exxdiv = "ewald" only. |
denominators | string | required for rimp2/pdep-rpa | shifted unshifted | An error on other kinds. |
frozen_core | integer | 0 | ≥ 0 | rimp2/pdep-rpa only. |
quad_points | integer | 40 | ≥ 1 | pdep-rpa only. Gamma point needs jk = "rsgdf". |
drpa_energy | string | "quadrature" | quadrature plasmon second-order | pdep-rpa with kmesh only. |
grad_conv | float | 1e-9 | > 0 | k-point SCF orbital-gradient threshold. Requires kmesh. |
stress | bool | false | true false | Compute and print the analytic stress σ = (1/Ω) dE/dε of the converged k-point SCF (Hartree/Bohr³ and GPa) and write it to the run_end record. Requires kmesh and task = "energy"; rhf, uhf, rohf, ksdft and the [dft] functional routes. An error, naming [cell] stress = true, with the Gamma point, task = "optimize", rimp2/pdep-rpa, range_split, range-separated hybrids other than HSE06 / CAM-B3LYP, TPSS and UKS SCAN/r2SCAN on jk = "rsgdf" (HSE06 / CAM-B3LYP run on RKS, UKS and ROKS with either jk; closed-shell TPSS and UKS SCAN/r2SCAN on jk = "dense"). ECP cells are supported. ROKS meta-GGA and hybrid meta-GGA are refused earlier by the k-point energy gate ([cell] kmesh with functional ...), which does not name stress. The run does one SCF through the retained hcore and J/K (the iteration printout of the plain energy run is replaced by the iteration count, energy and stress). The SCF must converge (the stress gate for restricted open shell is loose, 1e-5 in the orbital gradient: keep grad_conv well below it). |
n_radial | integer | 75 | Periodic XC grid. Kohn-Sham only. | |
n_angular | integer | 302 | 6 14 26 50 110 194 302 434 590 | Periodic XC grid. 434 and 590 are refinement grids; any other order is an error. Kohn-Sham only. |
neighbour_cutoff | float | max(10 Bohr, covering-radius bound) (with neighbour_switch W: that value + W) | > 0, in unit; ≥ bound + W | Periodic XC grid image cutoff D: image atoms within D of a grid point enter its partition weight. Kohn-Sham only. With the default neighbour_switch = 0 the cutoff is hard: the energy jumps wherever an image crosses D under a displacement or strain (measured: up to 2.2e-7 Ha on a (20, 50) grid at D = 10 on an H2 cell, and a 3.6e-5 Ha jump on an s+p triclinic cell at D = 10; the size depends on the grid and the cell) (a larger D does not remove the jumps; they get denser), and the analytic force and stress do not see the jumps, so a finite-difference step that straddles one misses by the jump / 2h. The default D also depends on the atom positions in a large sparse cell, so set it explicitly for task = "optimize" and for finite-difference checks. |
neighbour_switch | float | 0 (hard cutoff) | ≥ 0, in unit | Width W of a smooth taper on the image cutoff: an image at distance d from a grid point takes part with presence 1 for d ≤ D − W, falling to 0 at D (quintic smoothstep), and the analytic force and stress include the taper's derivative. This makes the energy continuous under displacement and strain. The taper costs the energy what lowering the cutoff toward D − W would (the automatic D adds W for that reason). Cost: the automatic D grows by W, so the image neighbours per point rise by about (1 + W/D)³; measured grid build on Li–H (75, 302): 0.29 s hard at D = 10, 0.87 s with W = 2 at the automatic D = 12. 0 is the hard cutoff, unchanged. Kohn-Sham only. Python: the neighbour_switch keyword (Å, default 0.0) of run_rks_gamma, run_uks_gamma, run_roks_gamma, run_rks_kpts, run_uks_kpts and run_roks_kpts; a negative or non-finite value is a ValueError. |
Periodic [scf] defaults are max_iter = 200, density_conv = 1e-10 and
energy_conv = 1e-12, not the molecular ones. k-point ROKS with a hybrid
(exact exchange, global or range-separated) applies a 0.05 Ha virtual level
shift by default, ramped to zero at convergence so the converged energy is
unchanged; LDA/GGA ROKS and ROHF get none. [scf] level_shift (finite,
≥ 0, Hartree) overrides it on the k-point rohf / ROKS routes; 0 turns it
off. Any other periodic route refuses the key.
Python bindings
import ferric gives you the same engine as the ferric CLI, as plain
function calls that return result objects holding floats and numpy arrays. To
install it, see Installation. The wheel is enough; you do
not need a clone of the repository to run anything on this page.
If you already know PySCF, For PySCF users maps the calls you know onto ferric's and lists where the two behave differently.
Every snippet below uses an inline geometry. Run them in order, since later
ones reuse water, o2, bs, bs_dz and aux from earlier ones. The
snippets through "Properties and charges" were run when this page was written
(2026-09-23), and any output shown is what they printed. The one-line calls in
the reference tables were not run.
Molecules and basis sets
import ferric
water = ferric.Molecule.from_xyz_string("""3
water
O 0.000000 0.000000 0.117790
H 0.000000 0.755453 -0.471161
H 0.000000 -0.755453 -0.471161
""")
bs = ferric.BasisSet.bundled("sto-3g")
The string is standard XYZ: an atom count on the first line, a comment
line, then one symbol x y z line per atom. The count must be the first line,
so start the string with """3. Starting it with """ and a newline gives an
empty first line, and parsing fails with bad atom count. Molecule.from_xyz(path)
reads the same format from a file. A leading @ on a symbol (@H) makes a
ghost atom, which carries basis functions but no nucleus or electrons.
Charge and spin belong to the molecule, not to the SCF call:
o2 = ferric.Molecule.from_xyz_string("""2
O2 triplet
O 0.0 0.0 0.0
O 0.0 0.0 1.208
""", charge=0, multiplicity=3)
multiplicity is 2S+1, so a triplet is 3. (PySCF's spin is 2S, so the same
triplet there is spin=2.) Both constructors default to charge=0, multiplicity=1. run_uhf and run_rohf take no spin argument; they read it
from the molecule. The geometry-changing drivers (run_frequencies,
run_saddle, run_irc) also accept a multiplicity= keyword.
Units
| Quantity | Unit |
|---|---|
XYZ input to from_xyz / from_xyz_string | Ångström |
Molecule.coords() | Ångström |
Molecule.coords_bohr() | Bohr (ferric stores Bohr internally) |
point_charges= / smeared_charges= on run_rhf, run_uhf, run_rohf, run_dft, … | Bohr, charges in e |
external_field= | Hartree atomic units |
esp_at_points(..., points) | points in Bohr; potential in atomic units |
omega on run_attenuated_rimp2, run_rs_mp2_rpa | Å⁻¹ (default 0.420) |
r0 on the terfc drivers | Å |
omega / r0 on compute_eri3_mo, compute_metric_2c | Bohr⁻¹ / Bohr (raw, unlike the run_* drivers) |
QmmmSystem(..., coords_angstrom) | Ångström |
QmmmSystem.point_charges() | Bohr |
| Energies | Hartree |
| Gradients | Hartree/Bohr |
print(water.coords()[0]) # (0.0, 0.0, 0.11779) Ångström
print(water.coords_bohr()[0]) # (0.0, 0.0, 0.22259084021251865) Bohr
The Sharp bits page has the full units table, including the QM/MM accessors.
Bundled basis sets
BasisSet.bundled(name) loads a basis compiled into the library. Names are
case-insensitive. An unknown name raises ValueError listing the known ones;
BasisSet.bundled_names() returns them, and bs.elements() gives the atomic
numbers a set covers. These 64 sets are available:
| Kind | Names |
|---|---|
| Pople | sto-3g, 6-31g, 6-31g*, 6-31g**, 6-31+g*, 6-31+g**, 6-31++g**, 6-311g**, 6-311++g** |
| Dunning | cc-pvdz, cc-pvtz, cc-pvqz, aug-cc-pvdz, aug-cc-pvtz, aug-cc-pvqz |
| Dunning with ECP (Z >= 29; lighter elements are the all-electron set) | cc-pvdz-pp, cc-pvtz-pp, cc-pvqz-pp, aug-cc-pvdz-pp, aug-cc-pvtz-pp, aug-cc-pvqz-pp |
| Karlsruhe (ECP inline for Z >= 37) | def2-svp, def2-svpd, def2-tzvp, def2-tzvpp, def2-tzvpd, def2-tzvppd, def2-qzvp, def2-qzvpp, def2-qzvppd |
| Jensen | pcseg-0, pcseg-1, pcseg-2, aug-pcseg-1, aug-pcseg-2 |
| Explicitly correlated (F12) | cc-pvdz-f12 (orbital), cc-pvdz-f12-optri (OptRI auxiliary) |
| RI (MP2/RPA/CC fitting) | cc-pvdz-rifit, cc-pvtz-rifit, cc-pvqz-rifit, aug-cc-pvdz-rifit, aug-cc-pvtz-rifit, aug-cc-pvqz-rifit, the six <set>-pp-rifit partners of the ECP sets, and def2-<x>-rifit for each of the nine def2 sets |
| JK (SCF fitting) | def2-universal-jkfit, cc-pvtz-jkfit, cc-pvqz-jkfit, aug-cc-pvdz-jkfit, aug-cc-pvtz-jkfit, aug-cc-pvqz-jkfit |
Aliases: the Pople stars also load as 6-31g(d), 6-31g(d,p), 6-31+g(d),
and so on; every RIFIT set also loads as <orbital>-ri and ORCA's
<orbital>/c (so cc-pvdz-ri is cc-pvdz-rifit); the JKFIT sets as
<orbital>/jk; and def2-universal-jkfit as def2/jk or PySCF's
def2-<x>-jkfit.
The aug-cc-pv{d,t,q}z-jkfit sets (Weigend, PCCP 4, 4285 (2002)) are the one
exception to "from the Basis Set Exchange": BSE does not carry them. They come
from Molpro's weigend_jkfit.libmol via Psi4's library (commit pinned in
provenance.json), checked bit-identical to PySCF's copy. Two caveats from
that source: their Ne, Ar and Kr functions are extrapolated, not published,
and aug-cc-pvdz-jkfit was never published at all; it is
aug-cc-pvtz-jkfit with the highest angular momentum dropped. For the
S66-type noncovalent work they exist for, aug-cc-pvtz-jkfit is the set to
use with aug-cc-pVTZ.
Each set covers what its source covers, and an element it lacks is an error
at basis setup that names the element and the covered range (for example
cc-pvtz has H-Ar and Ca-Kr, with no K; cc-pvtz-jkfit and cc-pvqz-jkfit
have only H, B-F, Al-Cl and Ga-Br; the -pp-rifit sets cover H-Ar (the aug ones
without Li and Na), Sc-Ni at TZ and QZ only, Y-Pd and Hf-Pt). It is never patched silently. The Pople sets follow
Gaussian's d convention: the 6-31G family has Cartesian d (6D), and the
6-311G family has spherical d (5D) on every element, including Na-Ar, which
the Basis Set Exchange marks Cartesian. Everything else is spherical, as the
Basis Set Exchange types it. See
For PySCF users for matching PySCF.
crates/ferric-core/src/basis/provenance.json records the source, version and
element list of every set. BasisSet.from_bse_json(path) loads any other
Basis Set Exchange JSON file.
Validation: every set is checked against PySCF fed the same basis
(crates/ferric-mp2/tests/basis_coverage_vs_pyscf.rs, at most 1e-8 Ha). Orbital
sets are checked by an RHF energy, RI sets by an RI-MP2 correlation energy, and
JK sets also by a density-fitted (RI-J + RI-K) RHF energy. For the BSE sets,
PySCF fetches the basis from BSE by name, so the comparison covers both the
shipped digits and ferric's parsing and normalisation. For the three aug-JKFIT
sets, PySCF uses its own copy of the data. The digits are covered instead by
the import's Psi4-vs-PySCF comparison, and the energy comparison covers parsing
and normalisation.
Most drivers take a BasisSet object. The drivers that move atoms
(run_optimize, run_frequencies, run_saddle, run_irc, run_qmmm,
run_optimize_qmmm) take the basis name as a string instead, because they
rebuild the basis at every geometry.
Ground state
rhf = ferric.run_rhf(water, bs)
print(rhf)
print(f"RHF: {rhf.energy:.10f} Ha, converged={rhf.converged}")
RHF Energy: -74.9631468000 Ha (converged: true, 8 iterations)
RHF: -74.9631468000 Ha, converged=True
Open shell, using the triplet o2 built above:
bs_dz = ferric.BasisSet.bundled("cc-pvdz")
uhf = ferric.run_uhf(o2, bs_dz)
rohf = ferric.run_rohf(o2, bs_dz)
print(f"UHF {uhf.energy:.10f} converged={uhf.converged}")
print(f"ROHF {rohf.energy:.10f} converged={rohf.converged}")
UHF -149.6276689907 converged=True
ROHF -149.6079865946 converged=True
PySCF 2.12 on the same geometry and basis gives −149.6276689907 (UHF) and
−149.6079865946 (ROHF). run_rohf returns a UhfResult: it has α and β
densities and orbital energies, which coincide for the spatial orbitals.
Kohn–Sham DFT:
dft = ferric.run_dft(water, bs_dz, functional="b3lyp")
print(f"B3LYP {dft.total_energy:.10f}")
run_dft is closed-shell only, and its default functional is LDA. It uses
density fitting for Coulomb by default (def2-universal-jkfit); run_rhf
does not. Pass df_j_aux="exact" for conventional four-centre Coulomb when you
compare against an exact-Coulomb code. dispersion="d3bj" adds Grimme D3(BJ);
the result then carries e_scf, e_dispersion and their sum in
total_energy. with_gradient=True also returns the analytic nuclear gradient
from dft.gradient(). There is no open-shell run_dft. Unrestricted
Kohn–Sham is reachable from Python only inside other drivers:
run_frequencies(reference="uhf", xc=...), run_u_gw(xc=...),
run_qmmm(method="uks") and run_cdft(functional=...).
run_rhf also takes implicit solvent (solvent=78.4 or solvent="water",
IEF-PCM), point charges and a uniform field. run_optimize and
run_optimize_uhf take the same solvent and pcm_lebedev_order and follow
the analytic IEF-PCM gradient; there is no Python COSMO option, and COSMO has
no gradient. See its docstring
(help(ferric.run_rhf)) for the full SCF knob set, which matches the CLI
[scf] section.
Naming the convergence keywords
A bare max_iter, energy_conv or density_conv belongs to a driver whose
method is the SCF itself (run_rhf, run_uhf, run_rohf, run_dft,
run_ksdft, run_qmmm). Every other driver solves a reference SCF as one stage
among others (MP2, CC, RPA, GW, TDDFT, orbital optimization, constrained DFT,
frequencies, geometry optimizers, saddle and IRC searches), so its SCF knobs are
scf_max_iter, scf_energy_conv and scf_density_conv, with the meaning and
defaults of run_rhf's. Another stage's iteration knob is stage-named
(oo_max_iter, max_outer, max_steps, eigensolver_*). The bare spellings
that older releases accepted on run_cdft, run_frequencies and
run_oo_rimp2 (max_iter there) still work, warn with a
DeprecationWarning, and raise if combined with the prefixed one. The CLI keeps
each key in its own section ([scf], [optimize], [gw], [rpa], [mp2] oo_*).
Check .converged, and know what it means
run_rhf, run_uhf, run_rohf and run_qmmm return a result whether or
not the SCF converged. A non-converged result has converged = False, and its
energy is a plausible, wrong number.
run_dft, run_ksdft and the correlated drivers (MP2, CC, RPA, GW, TDDFT)
behave differently: they raise if their reference SCF does not converge.
converged = True means the SCF reached a stationary point. It does not mean
the lowest one. The O2 triplet above with sto-3g instead of cc-pvdz shows
this. run_uhf with the default MINAO guess converges to −147.63397 Ha, which
is a saddle of the orbital Hessian 1.33 mHa above the UHF minimum at
−147.635296 Ha (PySCF's default guess lands on the same saddle). Without
stability_descent nothing in the result flags it.
run_uhf(o2, bs, stability_descent=True) (CLI: [scf] stability_descent = true on kind = "uhf") follows the downhill eigenvector and reaches the
minimum. guess="hcore" converges (converged=True) to a much
higher stationary point, −147.3789 Ha, which lies above ferric's own ROHF
(−147.63219 Ha). A UHF energy above the ROHF energy for the same molecule
cannot be a ground state, so comparing the two is a cheap check for open-shell
work.
Correlation
Correlated drivers run their own reference SCF internally, so they take the molecule and basis rather than an SCF result. They also take an explicit auxiliary (RI) basis. There is no automatic choice.
aux = ferric.BasisSet.bundled("cc-pvdz-ri")
rhf = ferric.run_rhf(water, bs_dz)
mp2 = ferric.run_rimp2(water, bs_dz, aux)
cc = ferric.run_ccsd_t(water, bs_dz, aux)
print(f"RHF {rhf.energy:.10f}")
print(f"RI-MP2 {mp2.total_energy:.10f} (corr {mp2.mp2_corr:.10f})")
print(f"CCSD(T) {rhf.energy + cc.correlation_energy + cc.t_correction:.10f} total")
print(f" {cc.correlation_energy:.10f} CCSD correlation")
print(f" {cc.t_correction:.10f} (T)")
closed-shell CCSD converged in 10 iterations. E_corr = -0.2135061893
RHF -76.0267679974
RI-MP2 -76.2308014541 (corr -0.2040334567)
CCSD(T) -76.2433412449 total
-0.2135061893 CCSD correlation
-0.0030670582 (T)
The first line is progress output that the CCSD solver prints to stdout.
CcResult holds only correlation_energy and t_correction (which is None
for run_ccd and run_ccsd). It carries no reference energy, so the total
above adds run_rhf(...).energy by hand. The MP2-family results all carry a
total_energy, and most also carry the reference energy.
Other members of the family use the same call shape:
att = ferric.run_attenuated_rimp2(water, bs_dz, aux, omega=0.420) # Å⁻¹
scs = ferric.run_scs_mp2(water, bs_dz, aux)
sos = ferric.run_laplace_sos_mp2(water, bs_dz, aux)
oo = ferric.run_oo_rimp2(water, bs_dz, aux)
mp3 = ferric.run_mp3(water, bs_dz, aux)
frozen_core= is accepted by every correlated driver. run_terfc_rimp2 has
no CLI method.kind of its own (the CLI reaches it through att-rimp2 with
att_operator = "terfc"); the reference table below marks the CLI kind of
every driver. run_rimp2, run_drpa and run_linlccd compute their method
exactly unless local="amplitude-threshold" and eps= are passed (see
Exact and local correlation).
Response and excited states
rpa = ferric.run_pdep_rpa(water, bs_dz, aux) # RPA correlation
gw = ferric.run_gw(water, bs_dz, aux) # G0W0@HF by default
tddft = ferric.run_tddft(water, bs_dz, aux, n_roots=3, method="tda")
print(rpa.total_energy, rpa.eigensolver_converged)
print(gw.mo_indices, gw.eps_qp) # quasiparticle energies, Hartree
print(tddft.excitation_energies) # Hartree
run_gw runs method="g0w0" on an HF reference unless you pass xc= (for
example xc="pbe"); by default it corrects HOMO−2 through LUMO+2. Check
gw.outer_converged and gw.qp_converged before using the numbers.
run_u_gw is the open-shell version.
run_tddft is closed-shell only. With functional=... it includes the
(ia|f_xc|jb) exchange-correlation kernel; VV10 and range-separated
functionals are refused because their kernel is not built. A τ-only meta-GGA
(SCAN, r2SCAN, TPSS) defaults to a 150 × 434 grid; result.grid reports the
grid the reference SCF and the kernel shared, and result.certificate lists it
as response-grid. With no
functional, it is CIS (method="tda") or TDHF (method="casida"). Both
methods match PySCF TDA/TDDFT to 1e-3 eV on the systems listed in
Capabilities and validation.
Properties and charges
The property functions take the molecule, the basis and a converged
RhfResult or DftResult. They work on closed-shell results.
import numpy as np
rhf = ferric.run_rhf(water, bs) # water / STO-3G
q_lowdin = ferric.lowdin_charges(water, bs, rhf)
q_resp = ferric.resp_charges(water, bs, rhf)
print(np.round(q_lowdin, 4), np.round(q_resp, 4))
# ESP 3 Bohr above each atom. Points are an (N, 3) array in Bohr.
pts = np.array(water.coords_bohr()) + np.array([0.0, 0.0, 3.0])
print(np.round(ferric.esp_at_points(water, bs, rhf, pts), 6))
[-0.2525 0.1263 0.1263] [-0.6176 0.3088 0.3088]
[-0.064684 -0.067154 -0.067154]
The charge family is mulliken_charges, lowdin_charges,
hirshfeld_charges, chelpg_charges and resp_charges, all returning one
charge per atom in units of e. hirshfeld_charges builds its proatoms from
free-atom SCF densities in the molecule's basis, as the CLI does;
proatom="slater" selects the single-exponential Slater proatom instead, which
is 0.23–0.72 e away on H2O, CO and CH3OH. resp_charges is a single-stage
restrained fit, not the multi-stage, multi-conformer RESP procedure. esp_at_atoms gives the
potential at each nucleus. hirshfeld_polarizability returns per-atom 3×3
polarizability tensors (Bohr³) and needs an RI basis.
Orbitals, density and cube files
eval_ao_on_points, eval_density_on_points and eval_mo_on_points evaluate
basis functions, the total electron density and restricted MOs at an (N, 3)
array of points. Points and cube grids are in Bohr, like esp_at_points.
eval_ao_on_points returns an (N, nbasis) array (PySCF eval_gto layout);
eval_mo_on_points takes 0-based mo_indices into mo_coefficients and
returns (N, len(mo_indices)). Orbital signs are the SCF's arbitrary phase.
write_cube writes the density (e/Bohr³) or one MO (what="mo", mo_index=)
on an axis-aligned grid, from the atoms' bounding box plus padding unless
origin and shape are given, and returns the (origin, shape) used.
import numpy as np
rhf = ferric.run_rhf(water, bs)
pts = np.array([[0.0, 0.0, 0.0], [0.0, 0.0, 1.0]]) # Bohr
print(np.round(ferric.eval_density_on_points(water, bs, rhf, pts), 4))
origin, shape = ferric.write_cube("homo.cube", water, bs, rhf,
what="mo", mo_index=4, spacing=0.3)
print(shape)
[10.2392 0.8103]
[28, 38, 32]
For NPZ export of ML-ready features (MO coefficients, PDEP eigenvectors, ESP,
charges, polarizabilities, C6 coefficients) in one run, use the CLI's
[rpa] export_npz section; see Input file (TOML).
What comes back
Results are Python objects with plain attributes for scalars and methods for arrays:
D = rhf.density() # numpy.ndarray, (n_bf, n_bf), AO basis
e = rhf.orbital_energies() # numpy.ndarray, ascending, Hartree
C = rhf.mo_coefficients() # numpy.ndarray, (n_bf, n_mo)
Matrices and tensors come back as numpy.ndarray. Per-atom lists (charges,
ESP values) come back as Python lists; wrap them in np.asarray if you need
arrays. run_drpa, run_linlccd and tune_omega return plain dicts, and
run_drpa_scan returns a list of them.
AO-basis matrices follow libint2's basis-function conventions, which are not PySCF's. Measured on CO/cc-pVDZ: total and orbital energies agree with PySCF to 3e-12 and 8e-9 Ha, and the two AO density matrices differ element by element by up to 1.5. Compare invariant quantities, not raw AO matrices.
Building blocks
ferric.AoIntegrals(mol, basis_set) exposes the integrals and J/K builds the
drivers use, as numpy arrays, so a method can be written in numpy the way it
is on top of PySCF's get_hcore / get_jk / ao2mo. Bohr and Hartree
throughout; AOs are in ferric's shell order (shell_offsets()).
| Block | Returns |
|---|---|
overlap(), kinetic(), nuclear(), ecp(), hcore(), dipole(origin) | (nao, nao); hcore() includes the ECP; dipole is (3, nao, nao) |
jk(D) | (J, K) of the TOTAL density, direct and Schwarz-screened: the RHF Fock matrix is hcore() + J - 0.5 * K |
df_jk(aux) | a DfJk holding the RI-J/RI-K fit, reused for every .jk(D) |
eri() | dense (μν|λσ), chemists' order, refused above max_gb (default 2 GiB) |
eri3(aux), eri2(aux) | raw (P|μν) as (naux, nao, nao) and the metric (P|Q) |
df_mo(aux, C1, C2) | fitted B^P_pq, (naux, k1, k2), with (pq|rs) ≈ Σ_P B^P_pq B^P_rs (the RI-MP2 fit) |
ao2mo(C1, C2, C3, C4) | exact (pq|rs), integral-direct, never holding the nao⁴ AO tensor |
grad_*, hess_*, cphf, xc_functional | nuclear-derivative, response and XC pieces |
ferric.einsum(spec, a, b) | the contraction engine of ferric's correlation codes (grammar) |
Every two-electron method takes operator="coulomb" | "erf" | "erfc" with
omega in Bohr⁻¹. Orbitals come from the result objects
(mo_coefficients(), orbital_energies(), density()) or from a numpy SCF on
the blocks.
ints = ferric.AoIntegrals(water, bs)
J, K = ints.jk(D)
F = ints.hcore() + J - 0.5 * K
E = 0.5 * np.sum(D * (ints.hcore() + F)) + ints.nuclear_repulsion()
print(abs(E - rhf.energy) < 1e-8)
B = ints.df_mo(aux, C[:, :5], C[:, 5:]) # (naux, nocc, nvir)
print(B.shape)
True
(84, 5, 2)
Three methods written this way are in examples/python/ and are tested
against the Rust drivers: rhf_from_blocks.py (DIIS RHF from hcore() and
jk(), equal to run_rhf and PySCF to 1e-8 Ha), rimp2_from_blocks.py
(RI-MP2 from df_mo, equal to run_rimp2 and PySCF DFMP2 to 1e-8 Ha) and
cis_from_blocks.py (singlet CIS from df_mo or ao2mo, equal to
run_tddft(method="tda") and PySCF TDA).
Not exposed: the Rust-side structs (PreparedBasis, Engine, screening
bounds), method intermediates (CC amplitudes, GW self-energies, PDEP
eigenpairs) and periodic (PBC) integrals. Those stay behind the drivers.
Memory, threads and MPI
Most drivers accept memory_budget_gb (GiB). It sets the same per-allocation
limits as the CLI's [memory] budget_gb: an allocation that does not fit is
spilled to disk, recomputed on demand (the DFT grid AO cache) or refused with
an error naming it (for example run_rimp2 and run_ccsd). Unlike the CLI,
Python installs no shared ledger, so each check compares its own allocation
with the whole budget rather than with what other live allocations have left.
Two checks instead subtract the process's current resident memory (RSS) first
and allow 90% of the remainder: the KS-DFT decision to store or recompute the
grid AO cache, and the UKS Newton/TRAH fxc kernel's second grid
cache. Because RSS includes everything already resident, those two see less
than the full budget.
It does not cap total process memory; see
Sharp bits.
mp2 = ferric.run_rimp2(water, bs_dz, aux, memory_budget_gb=8.0)
import ferric pins OpenBLAS to one thread unless OPENBLAS_NUM_THREADS is
already set. ferric parallelizes with rayon instead. run_rhf, run_uhf,
run_rohf, run_dft, run_rimp2, run_ccsd, run_ccsd_t, run_gw and
run_tddft release the GIL, so independent jobs submitted from a
ThreadPoolExecutor run in parallel. The other drivers hold it. For throughput
across many molecules, prefer many single-threaded processes.
Do not run a Python script under mpirun. The bindings expose no rank or
world-size accessor, so every rank runs the whole script. Distributed-memory
runs go through the CLI built from source with MPI; see
Installation.
Full reference
The module registers 61 public functions and 39 classes. That count
excludes _cli_main, the entry point behind the ferric console command. It
also exports two constants: DEFAULT_TEMPERATURE_K (298.15) and
BOLTZMANN_HARTREE_PER_K. The list below was taken from the registration
block of #[pymodule] fn ferric in crates/ferric-python/src/lib.rs, and each
purpose line is condensed from that item's doc comment, or from its code where
it has none. help(ferric.<name>) shows the full docstring and signature.
"CLI" gives the matching method.kind, task or TOML section, or "—" when the
capability is Python-only. How well each one is validated is on
Capabilities and validation.
Molecules and basis sets
| Name | Purpose | CLI |
|---|---|---|
Molecule | Geometry, charge and multiplicity. from_xyz, from_xyz_string, coords, coords_bohr, symbols, atomic_numbers, is_ghost, natoms, nelec, nuclear_repulsion, to_xyz_string. | [molecule] |
BasisSet | A Gaussian basis set, orbital or auxiliary. BasisSet.bundled(name). | [basis] |
SCF and DFT
| Name | Purpose | CLI |
|---|---|---|
run_rhf | Closed-shell RHF with the full SCF knob set, point charges, field and IEF-PCM solvent. | rhf |
run_uhf | Unrestricted HF; α/β counts come from the molecule's charge and multiplicity. | uhf |
run_rohf | Restricted open-shell HF (Guest–Saunders coupling); returns a UhfResult. | rohf |
run_dft | Closed-shell Kohn–Sham DFT (LDA/GGA/hybrid/RSH/meta-GGA by name), optional dispersion (dispersion="d3bj" or "mbd", added to the energy and, with with_gradient=True, to the gradient) and analytic gradient. | ksdft |
run_ksdft | Alias of run_dft. | ksdft |
d3bj_energy | Grimme D3(BJ) dispersion energy for a molecule and functional, in Hartree; s9=1.0 adds the ATM three-body term (default 0, off). | [dft] dispersion |
mbd_rsscs_energy | MBD@rsSCS dispersion energy (Hartree) from per-atom Hirshfeld volume ratios and β (or a functional with a published β); returns MbdRsscsResult with the screened α₀, C6, R_vdW and ω. | — |
tune_omega | IP-based (Baer/Kronik) tuning of an RSH functional's ω (Bohr⁻¹); closed-shell neutral plus doublet cation. continuation=True seeds each evaluation from the nearest already-evaluated ω, which keeps the cation on one branch across an out-of-order golden-section search; branch_tol flags an evaluation whose cation spin-population asymmetry jumps. check_cation_stability (default True) runs the UKS internal-stability analysis on every cation: each eval dict carries cation_lambda_min and cation_stability, and a tuned ω whose cation is an internal saddle raises RuntimeError naming ω and λ_min. Functionals without an orbital-Hessian kernel (VV10 such as ωB97X-V, meta-GGA) are reported not_analysed with a stability_warning, never as stable. | — |
dft_grid_point_count | Number of points in the main KS grid run_dft would build for the molecule with the same grid_* kwargs, without running an SCF (shows what pruning saves). | — |
SymmetricRhfResult | Result of run_rhf(symmetry=True) (point-group-blocked RHF): energy (of the symmetrized geometry), point_group, orbital_irreps, max_symmetrization_shift, irrep_occupations(), orbital_energies(), symmetrized_coords(), frame_transform(). density(), mo_coefficients() and gradient() raise: the AO matrices live in the rotated frame. | |
RhfResult | Result of run_rhf: energy, converged, iterations, density(), orbital_energies(), mo_coefficients(), one_step_energy_drop and stationarity_tol (the converged exit is stationary when the drop is above -stationarity_tol; both None for a non-aufbau or unconverged exit). | |
UhfResult | Result of run_uhf/run_rohf: α and β densities and orbital energies, one_step_energy_drop, stationarity_tol. | |
DftResult | Result of run_dft: total_energy (= e_scf + e_dispersion), e_scf, e_dispersion (None when not requested), dispersion_model ("D3(BJ)", "MBD@rsSCS" or None), volume_ratios (MBD@rsSCS only), converged, exit_reason(), one_step_energy_drop, stationarity_tol, density(), gradient(). |
Constrained DFT
| Name | Purpose | CLI |
|---|---|---|
run_cdft | Constrained UHF, or UKS when functional names a libxc functional other than "HF" (None and "HF", any case, give UHF): minimize the energy subject to fragment population constraints (Wu–Van Voorhis nested λ loop). Raises if the λ loop does not converge. | — |
CdftConstraint | One fragment constraint: atoms (0-based), target (a Becke electron population, not a net charge), kind = "charge" (Nα + Nβ) or "spin" (Nα − Nβ). | — |
cdft_coupling | Wu–Van Voorhis coupling H_ab between two converged single-"charge"-constraint states solved with the same geometry, basis, occupations and Hamiltonian. | — |
CdftResult | energy (without the constraint term), converged, scf_converged, lambdas, populations, targets, density_alpha(), density_beta(), weight_matrix(i). | |
CdftCouplingResult | h_ab (sign is a phase convention), s_ab, e_a, e_b. |
See Constrained DFT for a worked example.
Geometry, vibrations and reaction paths
| Name | Purpose | CLI |
|---|---|---|
run_optimize | RHF geometry optimization (basis by name); solvent / pcm_lebedev_order optimize on the IEF-PCM surface (run_optimize_uhf likewise). | task = "optimize" |
run_frequencies | Harmonic frequencies from the analytic Hessian (RHF, UHF, ROHF with exact J/K, or RI-J / RI-K) or finite differences of the analytic gradient; RHF/UHF/ROHF or their KS variants. Closed-shell KS (LDA/GGA, pure or global hybrid) uses the analytic moving-grid Hessian under the default hessian="auto" (explicitly: hessian="analytic-moving-grid"); other KS references use finite differences. | task = "frequencies" |
run_xtb_energy_gradient | GFN1/GFN2/GFN-FF energy and gradient (Hartree, Hartree/Bohr). Present in every build; raises RuntimeError unless the extension was built with the xtb cargo feature (libxtb on the loader path). | no CLI kind |
run_xtb_optimize | Geometry optimization on a GFN-xTB surface with the run_optimize BFGS driver; max_steps bounds geometry steps, scc_max_iter the SCC at each geometry, and an unconverged SCC raises. Certificate is not graded. | no CLI kind |
run_xtb_frequencies | Harmonic frequencies on a GFN-xTB surface: central differences of the xTB gradient (6N evaluations), shared mass-weighting and translation/rotation projection. | no CLI kind |
run_saddle | First-order saddle-point (transition-state) search by P-RFO; closed-shell. hessian="fd" (default) / "analytic" / "auto"; .hessian_source reports which ran. | — |
run_irc | Intrinsic reaction coordinate in both directions from a saddle; closed-shell. | — |
OptimizeResult | energy, converged, steps, energy_trace, mol(). | |
FrequencyResult | Frequencies in cm⁻¹ (negative = imaginary), normal modes, cartesian_hessian, asymmetry diagnostic, hessian_source. | |
SaddleResult | Outcome of a P-RFO search: geometry (Å), n_imaginary, imaginary_mode, is_transition_state(). | |
IrcResult | Both directions of an IRC, plus the saddle they came from. | |
IrcBranch | One direction of an IRC walk. |
QM/MM
| Name | Purpose | CLI |
|---|---|---|
QmmmSystem | A QM/MM partition with link atoms and boundary-charge schemes (coordinates in Å). | [qmmm] |
MmTopology | Explicit-parameter AMBER-form MM force field; assigns no parameters itself. | — |
run_qmmm | Embedded SCF energy plus QM gradient, MM forces and full gradient. | [qmmm] (energy) |
run_optimize_qmmm | Optimize a QmmmSystem; QM atoms always move, MM atoms per move_mm. | — |
QmmmResult | energy, qm_gradient(), mm_forces() (forces, not gradients), full_gradient(). | |
QmmmOptimizeResult | The relaxed partition and its energy trajectory. |
See QM/MM for a worked example.
MP2 family
| Name | Purpose | CLI |
|---|---|---|
run_rimp2 | RI-MP2 on an RHF reference; UHF + unrestricted RI-MP2 when multiplicity > 1 (reference says which). Exact by default; local=/eps= for the local approximation. | rimp2 |
run_oo_rimp2 | Orbital-optimized RI-MP2 (level-shifted Newton + DIIS + Cayley rotation). Closed shell only; open-shell OO-RI-MP2 is CLI-only. | oo-rimp2 |
run_mp3 | MP3 on an RHF reference, with RI integrals. | mp3 |
run_attenuated_rimp2 | RI-MP2 with the erfc-attenuated operator; ω in Å⁻¹, default 0.420. | att-rimp2 |
run_terfc_rimp2 | RI-MP2 with the exact tempered-erfc operator at one cutoff r0 (Å); needs the terfc tables. | — |
run_scs_mp2 | Spin-component-scaled MP2 (defaults c_OS = 6/5, c_SS = 1/3). | scs-mp2 |
run_scs_mp2_2terfc | Dual-attenuated SCS-MP2(2terfc); needs the terfc tables. | scs-mp2-2terfc |
run_mp2_v | MP2-V: attenuated MP2 plus damped VV10 nonlocal correlation. | mp2-v |
run_double_hybrid | B2PLYP or DSD-PBEP86 double hybrid; the result carries a certificate (Spike). | b2plyp, dsd-pbep86 |
run_laplace_mp2 | Laplace-transform RI-MP2 (default 7 quadrature points). | laplace-mp2 |
run_laplace_sos_mp2 | Laplace-transform SOS-MP2, E = c_os · E_OS; MO, AO or AO-sparse formulations. | laplace-sos-mp2 |
RiMp2Result | Result of run_rimp2 and run_terfc_rimp2: total_energy, rhf_energy (the reference SCF energy, RHF or UHF), mp2_corr, reference, and local (None for the exact method, else the local model dict). | |
OoRiMp2Result | Result of run_oo_rimp2, with converged and grad_norm. | |
Mp3Result | e_hf, e_mp2, e_mp3, e_corr, e_total. | |
AttenuatedMp2Result | Attenuated MP2 total, correlation and spin components. | |
ScsMp2Result | Result of run_scs_mp2/run_scs_mp2_2terfc, with e_os/e_ss. | |
Mp2VResult | MP2-V total, attenuated MP2 part and VV10 part. | |
LaplaceMp2Result | Laplace MP2 total, correlation and spin components. | |
SosMp2Result | Scaled and unscaled OS energy, c_os, n_quad and formulation echoed back. | |
DoubleHybridResult | Result of run_double_hybrid: total_energy, e_ks, e_corr_scaled, e_os, e_ss, c_os, c_ss, certificate. |
run_rimp2, run_oo_rimp2, run_attenuated_rimp2, run_terfc_rimp2,
run_scs_mp2, run_scs_mp2_2terfc, run_mp2_v, run_laplace_mp2,
run_laplace_sos_mp2 and run_rs_mp2_rpa take scf_jk="exact"|"rijk"|"rijcosx"
and jk_auxbasis= to choose how the reference SCF builds J and K, and their
results echo scf_jk and jk_auxbasis. The default is "exact", except for
run_rs_mp2_rpa, whose default is "rijk". run_oo_rimp2 accepts only
"exact" and raises on the fitted modes, because it rebuilds exact J/K at
every orbital step. See
The MP2 family.
Exact and local correlation
run_rimp2, run_drpa and run_linlccd name a method and compute it
exactly by default. The local approximation is asked for with keywords
that mirror the CLI's [local] section, under
the same rules, raised as ValueError before any SCF:
local=isNone(exact;"none"is the same) or"amplitude-threshold"; any other value is an error.eps=is required withlocal="amplitude-threshold": the threshold is part of the model and has no default.eps=0reproduces the exact method.eps=withoutlocal=is an error, not ignored.compute_reference=True(local only) also computes the exact method and reports it:local["e_corr_canonical_ri"](MP2),e_corr_plasmon_canonical(dRPA),local["e_corr_exact"](LinLCCD). Off, the key is present andNone. It is a full exact calculation, so it removes any cost saving.run_rimp2(integral_direct=True, ...)selects the integral-direct local MP2, with its locality mapsaux_radius,virt_radius(Bohr),ao_tail,schwarz_skip,batch_merge,gate_calandvirt_schwarz_kappa(the CLI names and defaults). They are errors withoutintegral_direct=True.kappais an error on the local MP2.
Every result says which model it is: RiMp2Result.local and the dicts'
"local" key are None for the exact method, else a dict with scheme,
eps, keep_fraction and integral_direct plus solver counters. All local
paths are closed shell.
| Name | Purpose | CLI |
|---|---|---|
run_drpa | dRPA@HF by the drCCD Riccati solve. Exact by default (equals the plasmon formula); MemoryError before the SCF when the exact solve cannot fit (use run_pdep_rpa(..., trunc_thresh=0)). diis= (default 8); eps_rtol_factor= (local only). | drpa |
run_drpa_scan | The local dRPA over a list of eps values, sharing one SCF and localization; each dict carries "local". | drpa + [local] eps_sweep |
run_linlccd | Linearized ladder CCD, variant = "hh" (default), "drivers-only", "full". Exact by default. | linlccd |
Coupled cluster
All use RI integrals from the auxbasis argument and a closed-shell RHF
reference.
| Name | Purpose | CLI |
|---|---|---|
run_ccd | CCD correlation energy. | — |
run_ccsd | Spin-adapted closed-shell CCSD. | ccsd |
run_ccsd_t | CCSD plus the spin-adapted (T) correction. | — |
CcResult | correlation_energy and t_correction (None without triples). No reference energy. |
RPA, GW and excited states
| Name | Purpose | CLI |
|---|---|---|
run_pdep_rpa | Direct RPA correlation energy by PDEP (projective dielectric eigenpotentials); accepts point charges, field and solvent. Closed shell only; open-shell U-PDEP-RPA is CLI-only. | pdep-rpa |
run_rs_mp2_rpa | Range-separated SR-MP2 + LR-RPA (formulation = "delta-lr" or "coupled-rings"; ω in Å⁻¹). | rs-mp2-rpa |
run_gw | Closed-shell G0W0 / COHSEX / evGW0 / evGW on an RHF or RKS reference. | gw |
run_u_gw | Open-shell GW variants on a UHF/UKS or ROHF reference. | gw with multiplicity > 1 |
run_bse_tda | BSE-TDA singlet excitation energies on a closed-shell RHF reference. | bse-tda |
run_tdhf_static_polarizability | RPAx@KS static (ω = 0) polarizability on a closed-shell KS reference. | tdhf-static-polarizability |
run_tddft | TDA or Casida excitations on a closed-shell HF (CIS/TDHF) or KS reference, with the f_xc kernel. | tda, tddft |
PdepRpaResult | total_energy, e_rpa, eigensolver_converged, eigenvalues and quadrature grid. | |
RsMp2RpaResult | SR-MP2, LR-MP2 and dRPA pieces; which fields are set depends on formulation. | |
GwResult | eps_qp, eps_mf, sigma_x, sigma_c, z_factor, outer_converged, qp_converged. | |
UGwResult | α and β versions of the GwResult fields. | |
BseResult | Excitation energies and oscillator strengths. | |
TdhfStaticPolarizabilityResult | Polarizability tensor and its isotropic value iso. | |
TddftResult | excitation_energies, oscillator_strengths, method, grid, certificate. |
Properties and charges
Each takes (mol, basis_set, result) with a converged closed-shell
RhfResult or DftResult; esp_at_points also takes the points and
hirshfeld_polarizability an RI basis.
| Name | Purpose | CLI |
|---|---|---|
esp_at_atoms | Electrostatic potential at each nucleus, in atomic units. | [rpa] compute_esp |
esp_at_points | Electrostatic potential at arbitrary points given in Bohr. | [rpa] compute_esp_surface (vdW-surface points only) |
mulliken_charges | Mulliken population charges. | [rpa] compute_mulliken_charges |
lowdin_charges | Löwdin (symmetric-orthogonalization) charges. | [rpa] compute_lowdin_charges |
hirshfeld_charges | Hirshfeld charges. The default proatom="scf" uses free-atom SCF densities in the molecule's basis, solved with the result's own SCF settings, as the CLI does; proatom="slater" uses a single-exponential Slater proatom. | [rpa] compute_hirshfeld_charges |
chelpg_charges | CHELPG charges fitted to the ESP on a grid. | [rpa] compute_chelpg_charges |
resp_charges | Single-stage RESP (restrained ESP-fit) charges. | [rpa] compute_resp_charges |
hirshfeld_polarizability | Per-atom Hirshfeld-partitioned static polarizability tensors (Bohr³) from PDEP-RPA. | — |
orbital_moments | Per-orbital centroids and spatial spreads (Bohr) of the restricted MOs. | — |
density_second_moment | 3×3 second-moment tensor of the electron density (Bohr²). | — |
eval_ao_on_points | AO values at (N, 3) points in Bohr, shape (N, nbasis). | — |
eval_density_on_points | Total electron density (e/Bohr³) at points in Bohr. | — |
eval_mo_on_points | Restricted MO values at points in Bohr, for chosen 0-based MO indices. | — |
write_cube | Gaussian cube file of the density or one restricted MO; all lengths in Bohr. | — |
The [rpa] compute_* keys write into the CLI's NPZ export.
Integrals and orbitals (prototyping)
These take omega/r0 in raw Bohr units, unlike the run_* drivers.
| Name | Purpose | CLI |
|---|---|---|
AoIntegrals | One-electron matrices, ERIs (dense, 3- and 2-index, MO-transformed), J/K, gradient/Hessian/response/XC pieces for one molecule and basis; see Building blocks. | — |
DfJk | From AoIntegrals.df_jk(aux): RI-J/RI-K j(D), k(D), jk(D) over a fit built once. | — |
XcFunctional | From AoIntegrals.xc_functional(name): E_xc, V_xc, f_xc response and XC gradient/Hessian on run_dft's grid. | — |
Cphf, Ucphf | From AoIntegrals.cphf(...) / ucphf(...): orbital-Hessian matvec, CPHF/CPKS solve, response density. | — |
einsum | Two-operand contraction under ferric's einsum spec grammar. | — |
compute_eri3 | Raw 3-centre Coulomb integrals (P|μν), shape (naux, n_bf, n_bf). | — |
compute_eri3_mo | MO-basis 3-centre integrals (P|pq) for any two coefficient matrices, built blockwise under a memory budget. | — |
compute_metric_2c | 2-centre metric (P|w|Q) over the auxiliary basis, Coulomb by default. | — |
shell_info | Shell centres (Bohr), first-function offsets and sizes, for building fitting domains. | — |
boys_localize | Foster–Boys localization of given orbitals; returns a BoysResult. | — |
BoysResult | Result of boys_localize: c_loc(), centers(), converged, iterations. |
Conformer ensembles
| Name | Purpose | CLI |
|---|---|---|
ConformerEnsemble | Conformers of one species sharing atom order, composition, charge and multiplicity. | — |
BoltzmannWeights | Boltzmann populations of an ensemble at one temperature. | — |
EnsembleDiagnostics | Population-structure readout: effective number of conformers, dominance verdict. | — |
WeightedStats | A weighted mean with its spread (mean, std_dev, min, max). | — |
boltzmann_weights | Boltzmann weights from a list of energies (Hartree) at a temperature (default 298.15 K). | — |
weighted_stats | Weighted mean and standard deviation of a scalar property. | — |
weighted_stats_vector | The same, component-wise, for a vector property. | — |
weighted_stats_tensor | The same, element-wise, for a rank-2 tensor property. | — |
Examples
This page indexes every input file in examples/. There are 84 TOML files.
Three Python scripts that build methods from AoIntegrals blocks are in
examples/python/ (Python bindings).
Run a TOML file from the repository root:
OPENBLAS_NUM_THREADS=1 cargo run --release --bin ferric -- examples/water-rhf.toml
What CI checks
CI parses every shipped example and runs the three listed below. The test
all_shipped_examples_parse (in crates/ferric-cli/src/config.rs) loads
every examples/*.toml through the strict parser. It also checks that each
file's [molecule] xyz path exists. It does not execute the calculation or
compare any energy.
Three examples are also executed by integration tests in
crates/ferric-cli/tests/:
| Example | Test | What the test asserts |
|---|---|---|
water-rhf.toml | epistemic_warning.rs, verbose_trace.rs, qmmm_reports_the_pqr_as_its_geometry.rs | The run completes and prints no [warning] grade line. |
water-tdhf-static-alpha.toml | epistemic_warning.rs | The Smoke-grade warning goes to stderr and not to stdout. |
water-qmmm.toml | qmmm_reports_the_pqr_as_its_geometry.rs | The embedded energy -74.9653197421 appears in the output. |
Every other number in the "Reference in header" column below is copied verbatim from that file's comment block, and CI does not re-check it. Where the header cites a Rust test as the reference, that test is the check.
Examples that need extra setup
water-mp2v.toml,water-scs-mp2-2terfc.toml,water-attmp2-terfc.tomland anyrs-mp2-rparun withattenuator = "terf"need the tempered-erfc interpolation tables: pointFERRIC_TERF_TABLE_DIRat them. Without it these runs stop with an error.benzene-dfb3lyp-mpi.tomlis an ordinary input; its header gives the--features mpibuild and thempirunlaunch line.
SCF and DFT
See SCF and DFT.
| File | System / basis | kind / task | Reference in header | Notes |
|---|---|---|---|---|
water-rhf.toml | H2O / STO-3G | rhf / energy | — | water-qmmm.toml's header gives this geometry's energy as −74.9631468000. The file is commented to demonstrate screening. |
benzene-rhf.toml | benzene / cc-pVDZ | rhf | — | Exact 4-index J/K. |
benzene-rhf-dfj.toml | benzene / cc-pVDZ | rhf | — | RI-J only (cc-pvdz-ri). |
benzene-rhf-rijk.toml | benzene / cc-pVDZ | rhf | — | RI-JK (def2-universal-jkfit). |
benzene-rhf-def2.toml | benzene / def2-SVP | rhf | — | |
benzene-rhf-def2-rijk.toml | benzene / def2-SVP | rhf | — | RI-JK. |
decane-rhf.toml | decane / STO-3G | rhf | — | k_builder = "link". |
water-rhf-cosx.toml | H2O / cc-pVDZ | rhf | "E(COSX) − E(direct) is reported in crates/ferric-scf/tests/cosx_scf.rs" | COSX exchange. |
water-rhf-symmetry.toml | H2O / STO-3G | rhf + [scf] symmetry | — | Point-group-blocked RHF (C2v); irrep_nelec shown commented. |
h_uhf.toml | H atom / STO-3G, doublet | uhf | — | |
h2_opt.toml | stretched H2 / STO-3G | rhf / optimize | — | |
water-frequencies.toml | H2O / STO-3G | rhf / frequencies | — | Finite-difference Hessian (hessian = "fd"). Check Hessian asymmetry. |
nh2-rohf-frequencies.toml | NH2 doublet / cc-pVDZ | rohf / frequencies | crates/ferric-scf/tests/validation_rohf_hessian.rs | Analytic ROHF Hessian. |
benzene-dfb3lyp.toml | benzene / def2-SVP | ksdft B3LYP | — | RI-JK is on automatically for ksdft. |
benzene-dfb3lyp-mpi.toml | benzene / cc-pVDZ | ksdft B3LYP | — | Run under mpirun with the MPI build (see its header). |
water-wb97xv.toml | H2O / cc-pVDZ | ksdft wB97X-V | — | |
water-pbe-d3bj.toml | H2O / cc-pVDZ | ksdft PBE + D3(BJ) | — | Prints E(KS-DFT) and E(D3BJ) separately. |
water-pbe-mbd.toml | H2O / cc-pVDZ | ksdft PBE + MBD@rsSCS | — | Prints E(KS-DFT) and E(MBD@rsSCS) separately. |
water-pbe-pruned-grid.toml | H2O / cc-pVDZ | ksdft PBE / energy | "removes ~23% of the grid points" (at 75×110) | Validated in crates/ferric-dft/tests/grid_prune_live_scf.rs. |
h2-lda-opt.toml | H2 / STO-3G | ksdft LDA / optimize | — | |
water-qmmm.toml | H2O + Na⁺ (PQR) / STO-3G | rhf + [qmmm] | "vacuum −74.9629466809, embedded −74.9653197421, i.e. −1.489 kcal/mol from the ion at 4 A. Verified against ferric.run_rhf(point_charges=...) to all 10 digits." | The vacuum number is at the PQR geometry, not the xyz. The embedded number is asserted by a test. |
water-pcm.toml | H2O / STO-3G | rhf + [pcm] | — | IEF-PCM water (solvent = "water", ε = 78.4). |
water-rhf-smeared-charge.toml | H2O / STO-3G | rhf + [external_potential] | — | One Gaussian-smeared charge (width, Bohr) and one point charge. |
o2-uhf-stability-descent.toml | O2 triplet / STO-3G | uhf | "the descent follows the downhill eigenvector to the UHF minimum (-147.63530 Ha)" | [scf] stability_descent = true. |
h2-cell-rhf.toml | H2 / STO-3G, 4 Bohr cubic cell | rhf + [cell], Gamma point | — | Periodic, dense J/K, exxdiv = "ewald". |
h2-cell-kpts-rhf.toml | H2 / STO-3G, 4 Bohr cubic cell | rhf + [cell], 2×2×2 k-mesh | — | Periodic k-point RHF, dense J/K. |
h2-cell-kpts-ksdft.toml | H2 / STO-3G, 4 Bohr cubic cell | ksdft + [cell], 1×1×2 k-mesh | PBE0 | Periodic k-point closed-shell KS-DFT, dense J/K. |
h2-cell-kpts-rhf-stress.toml | H2 / STO-3G, 4 Bohr cubic cell | rhf + [cell], 1×1×2 shifted (centring = "mp") k-mesh, stress = true | — | Analytic k-point stress (Hartree/Bohr³ and GPa, also in the run log's run_end), dense J/K. |
h2-cell-kpts-rhf-opt.toml | stretched H2 / STO-3G, 4 Å cubic cell | rhf + [cell] 1×1×3 k-mesh / optimize | — | k-point atom relaxation at a fixed lattice, analytic k-point force, dense J/K. |
h2-cell-kpts-rks-opt.toml | stretched H2 / STO-3G, 4 Å cubic cell | ksdft + [cell] 1×1×3 k-mesh / optimize | PBE0 | k-point atom relaxation at a fixed lattice, analytic k-point Kohn-Sham force (global hybrid), dense J/K. |
h-cell-kpts-uks.toml | H atom doublet / STO-3G, 4 Bohr cubic cell | uhf + [dft] + [cell], 1×1×2 k-mesh | PBE | Periodic k-point open-shell Kohn-Sham (UKS), dense J/K; rohf gives ROKS/ROHF at a mesh. |
h2-cell-rks-opt.toml | stretched H2 / STO-3G, 4 Å cubic cell | ksdft PBE + [cell] / optimize | — | Gamma-point atom relaxation at a fixed lattice, analytic periodic force, dense J/K. |
MP2 family
See The MP2 family.
| File | System / basis | kind | Reference in header | Notes |
|---|---|---|---|---|
water-rimp2.toml | H2O / cc-pVDZ | rimp2 | — | All-electron. |
water-rimp2-frozen-core.toml | H2O / cc-pVDZ | rimp2 | Expected log line: "[ferric] frozen core: 1 orbital(s) frozen from [mp2] frozen_core = "auto"" | |
water-rimp2-rijk.toml | H2O / cc-pVDZ | rimp2 | — | RI-JK reference SCF ([scf] jk = "rijk", jk_aux = "def2-universal-jkfit"); the MP2 RI aux stays cc-pvdz-ri. |
water-rimp2-local.toml | H2O / 6-31G | rimp2 + [local] | — | Amplitude-threshold local MP2, eps = 1e-4. Sets reference = true, so it also computes the exact RI-MP2 and prints the local error against it (off by default). |
alkane8-rimp2-local-direct.toml | octane / 6-31G | rimp2 + [local] | — | Integral-direct local MP2 (integral_direct = true), every locality map at its default. |
water-drpa.toml | H2O / 6-31G | drpa | — | Exact dRPA (no [local]). |
water-drpa-local.toml | H2O / 6-31G | drpa + [local] | — | Amplitude-threshold dRPA, eps = 1e-4, reference = true (prints the error against the canonical plasmon dRPA). A comment shows the eps_sweep form. |
water-mp3.toml | H2O / cc-pVDZ | mp3 | — | |
water-oo-rimp2.toml | H2O / cc-pVDZ | oo-rimp2 | — | Proven (narrow) grade. |
water-attmp2.toml | H2O / aug-cc-pVDZ | att-rimp2 | — | ω = 0.420 Å⁻¹. |
water-attmp2-terfc.toml | H2O / aug-cc-pVDZ | att-rimp2 | — | att_operator = "terfc", att_r0 = 1.05 Å. Needs the terf tables. |
alkane8-attmp2-terfc-local-direct.toml | C8H18 / 6-31G | att-rimp2 | [local] | Integral-direct amplitude-threshold LMP2 with a decoupled terfc seam (att_omega, r0·ω = 4). Needs the terf tables. |
water-scs-mp2.toml | H2O / cc-pVDZ | scs-mp2 | — | Grimme coefficients (defaults). |
water-scs-mp2-2terfc.toml | H2O / cc-pVDZ | scs-mp2-2terfc | — | Thesis defaults r0 = 0.75/1.05 Å. Needs the terf tables. |
water-laplace-rimp2.toml | H2O / cc-pVDZ | laplace-mp2 | — | |
water-laplace-sos-mp2.toml | H2O / cc-pVDZ | laplace-sos-mp2 | — | |
water-mp2v.toml | H2O / aug-cc-pVDZ | mp2-v | — | The header warns that aDZ is outside the fitted basis. Needs the terf tables. |
water-rs-mp2-rpa.toml | H2O / aug-cc-pVDZ | rs-mp2-rpa | — | Smoke grade. |
Coupled cluster and double hybrids
See Coupled cluster and RPA and GW § double hybrids.
| File | System / basis | kind | Reference in header | Notes |
|---|---|---|---|---|
water-ccsd.toml | H2O / cc-pVDZ | ccsd | "PySCF CCSD run on the same density-fitted integrals gives E_corr = -0.2135061893 Ha" | All electrons, aux cc-pvdz-ri. |
water-linlccd.toml | H2O / 6-31G | linlccd | — | Exact LinLCCD(hh). |
water-linlccd-local.toml | H2O / 6-31G | linlccd + [local] | — | Amplitude-threshold LinLCCD(hh), eps = 1e-4. |
water-ccd.toml | H2O / STO-3G | ccd | — | |
water-ccsd-t.toml | H2O / STO-3G | ccsd(t) | — | Prints the CCSD correlation energy, the (T) correction and the total. |
water-wb97xlv.toml | H2O / 6-31G | wb97x-l-v | — | λ = 0.6, ω = 0.1 Bohr⁻¹ (published values). Proven (narrow) grade. |
water-b2plyp.toml | H2O / cc-pVDZ | b2plyp | — | Spike grade. Aux cc-pvdz-rifit (an alias of cc-pvdz-ri). |
RPA, C6 and properties
See RPA and GW.
| File | System / basis | kind | Reference in header | Notes |
|---|---|---|---|---|
water-pdep-rpa.toml | H2O / cc-pVDZ | pdep-rpa | — | Writes eigenpotential cube files. |
h2o-pdep-rpa-props.toml | H2O / cc-pVDZ | pdep-rpa | — | NPZ export. |
benzene-pdep-rpa.toml | benzene / def2-SVP | pdep-rpa | — | RI-JK SCF. |
benzene-pdep-rpa-export.toml | benzene / cc-pVDZ | pdep-rpa | — | Exports cube files. |
benzene-rijk-pdep-rpa.toml | benzene / cc-pVDZ | pdep-rpa | — | Full rank (trunc_thresh = 0). |
water-c6-pdep.toml | H2O / aug-cc-pVTZ | pdep-rpa, c6_source = "pdep" | "DOSD molecular reference (Meath/Toulouse): C6(H2O–H2O) = 45.3 a.u." | Compare the printed "molecular C6" line against it, not the sum of the NPZ c6_iso entries. Writes to /tmp. |
argon-c6-rpa-pbe.toml | Ar / aug-cc-pVTZ | pdep-rpa @PBE | "C6(Ar-Ar) = 56.4 a.u. vs DOSD 64.3 (-12%); the full He/Ne/Ar sweep gives mean |err| 8.9% at RPA@PBE — vs 39% at RPA@HF" | Writes to /tmp. |
water-tdhf-static-alpha.toml | H2O / cc-pVDZ | tdhf-static-polarizability @PBE | "alpha_iso = 5.20 a.u. … against the DOSD reference 9.64 a.u.: 46% LOW" | Sets [gw] scissor = 0.36 Ha; at scissor = 0 the run is refused (negative α diagonal). Static α only. Executed by a test (for the warning only). |
GW and BSE
| File | System / basis | kind | Reference in header | Notes |
|---|---|---|---|---|
water-g0w0-pbe.toml | H2O / cc-pVDZ | gw G0W0@PBE | "HOMO IP should match … PySCF gw_ac reference (11.1714 eV) to <0.1 eV" (crates/ferric-gw/tests/g0w0_pbe_h2o.rs) | |
oh-ugw.toml | OH doublet / cc-pVDZ | gw U-G0W0@UHF | "alpha-HOMO IP in the ~13-14 eV window, bracketing experiment 13.02 eV" (crates/ferric-gw/tests/oh_u_g0w0.rs) | |
oh-ugw-rohf.toml | OH doublet / cc-pVDZ | gw U-G0W0@ROHF | — | [gw] reference = "rohf". |
water-bse-tda.toml | H2O / cc-pVDZ | bse-tda | "Measured via this exact TOML (2026-07-18): 8.4572 eV", against a PySCF-integral cross-check of 8.46 eV and a sanity window of [5, 12] eV | |
water-augccpvdz-bse-tda.toml | H2O / aug-cc-pVDZ | bse-tda | Literature: "aug-cc-pVDZ CCSDT Delta_Evert = 9.279 eV, f = 0.058; CBS TBE = 7.71+-0.02 eV, f = 0.052+-0.001" | No ferric number is recorded. |
h2co-bse-tda.toml | H2CO / cc-pVDZ | bse-tda | — | Pilot run. The lowest state is dark (f ≈ 0). |
c2h4-bse-tda.toml | C2H4 / cc-pVDZ | bse-tda | — | Pilot run. |
formaldehyde-bse-tda-augdz.toml | H2CO (QUESTDB geometry) / aug-cc-pVDZ | bse-tda | Literature: "lowest singlet 1^1A2 (n->pi*, symmetry-forbidden) at 3.966 eV (QUESTDB TBE…)" | The template for the Thiel-set files below. |
acetaldehyde-bse-tda-augdz.toml, butadiene-bse-tda-augdz.toml, cyclopropene-bse-tda-augdz.toml, ethylene-bse-tda-augdz.toml, furan-bse-tda-augdz.toml, glyoxal-bse-tda-augdz.toml, pyrazine-bse-tda-augdz.toml, pyridine-bse-tda-augdz.toml, pyrimidine-bse-tda-augdz.toml | QUESTDB Thiel-set molecules / aug-cc-pVDZ | bse-tda | — | Same setup as the formaldehyde file. The reference values are in testdata/reference/thiel_set_subset.json. |
TDDFT
| File | System / basis | kind | Reference in header | Notes |
|---|---|---|---|---|
water-tda.toml | H2O / cc-pVDZ | tda (CIS, 5 roots) | — | Default aux cc-pvdz-rifit (an alias of cc-pvdz-ri). |
water-tddft-pbe.toml | H2O / cc-pVDZ | tddft @PBE (5 roots) | — | Includes the f_xc kernel. |
See also Capabilities and validation for the grade of
each kind and its evidence, and Input reference for every key.
Run logs
Every ferric CLI run writes a machine-readable JSON Lines log, without being asked.
The format is ferric's own; it is not QCSchema, and there is no QCSchema
exporter. Each record's schema field (currently 1, on run_start) versions
this format.
$ ferric water-rhf.toml
[ferric] JSON run log: water-rhf.ferric.jsonl
...
Why it is on by default
A result whose run left no artifact can't be checked, and stdout captures get truncated. The run log keeps the record on disk whether or not anyone captured stdout.
Opt out explicitly if you need to:
[output]
json = false # no log
# json = "runs/a.jsonl" # or put it somewhere specific
or from the command line:
ferric --no-json input.toml
ferric --json runs/benzene.jsonl input.toml
The default path is the input file's name with .ferric.jsonl in place of its
extension, written beside the input.
A log never fails a calculation. If the path cannot be opened or the disk fills, ferric warns once on stderr and the run continues.
Format
One JSON object per line, written and flushed as it happens rather than buffered to the end of the run. A job that is OOM-killed, hits a wall-clock limit, or is Ctrl-C'd at iteration 90 of 100 leaves those 90 iterations on disk. Only the final line may be truncated; everything before it parses.
jq -c 'select(.record=="scf_iter") | {iter, energy, dp_rms}' water-rhf.ferric.jsonl
Every record carries record (its type), seq (a gap-free counter, so a
missing record is detectable) and t (seconds since the run started).
run_start
Written before any expensive work, so it survives a job killed in the first
iteration. Carries the ferric version, the git SHA the binary was built from
(null if the build had no checkout; a -dirty suffix means uncommitted
changes were compiled in), a UTC timestamp, the resolved config (including the
memory budget in effect, not the key as written) and the molecule.
scf_iter
One per SCF iteration: iter, energy, de, dp_rms, dp_max, err_max,
and grad_rms where the solver forms one. rung says which ladder rung the
iteration belongs to. method is rhf/uhf/rohf or their KS counterparts
rks/uks/roks.
These are the quantities the solver already computes for its convergence
decision (see scf_converged); the log only reports them.
dp_rms and dp_max are "inf" on the first iteration of each SCF, before
there is a previous density to compare against. JSON has no infinity, so
non-finite values are written as the strings "inf", "-inf" and "NaN"
rather than as null, which would make a NaN indistinguishable from a field
that was not reported.
guess_scf_iter
Identical shape, but for the free-atom SCFs the SAD/MINAO initial guess runs per element. They are real work and can fail, so they are logged, under a different record type so that free oxygen's energy is never mistaken for the molecule's.
ladder_rung
One per convergence-ladder rung: rung, tricks (which accelerators it
enables), iterations, exit, energy, converged and max_iter.
max_iter is the rung's own cap. In the CLI, kind = "rhf" and
kind = "ksdft" run through a convergence ladder, and unless you define your
own [[scf.ladder]] rungs, the default ladder sets its own per-rung iteration
caps:
kind | rung 0 | rungs 1–4 |
|---|---|---|
rhf | 60 | 60 / 60 / 80 / 100 |
ksdft | [scf] max_iter | 60 / 60 / 80 / 100 |
So for rhf, [scf] max_iter does not set the per-rung cap. A run that reports
"iterations = 100" is usually rung 4 using up its own budget, not a single
hundred-iteration SCF. The cap and the count are recorded together so the log
can't be misread that way.
run_end
The terminal record: energy, converged, exit, wall_s, cpu_s and
peak_rss_bytes (the high-water mark, not the RSS at exit).
For a post-SCF method the energy is the SCF reference, not the method's
total. extra.energy_is says which. Per-iteration records for MP2/RPA/CC/GW
are not implemented yet.
Only task = "energy" and periodic task = "optimize" write run_end.
A molecular task = "optimize" and task = "frequencies" currently write
run_start and the SCF iteration records, but no run_end, ladder_rung or
result. A periodic [cell] optimization writes run_end (see below), also
when it stops at max_steps without converging; it writes none when it ends
in an error. Read the final geometry
and energy of an optimization from stdout. A finished task = "frequencies"
writes one frequencies record instead (below). A missing run_end in such a log
doesn't mean the run failed (MEASURED on examples/h2-lda-opt.toml,
2026-09-23).
Precision provenance (precision)
run_end.extra.precision records which mixed-precision kernels the whole
run went through: every kernel, counted from the start of the run to
run_end, whichever code called it (the SCF, the method, and MP2 variants such
as SCS-MP2, SCS-MP2(2terfc), terfc att-rimp2 and the double hybrids, which
carry no record of their own). run_end.extra.scf_precision is the SCF's own
record. A run that writes no run_end from the molecular dispatch (open-shell
uhf/rohf energies and their KS forms, task = "optimize",
task = "frequencies", wb97x-l-v, b2plyp, dsd-pbep86) writes the
whole-run record as a precision record instead. A rimp2 run's
result.components.precision covers the MP2 correlation energy alone (closed
shell and unrestricted). The Python results carry
the same object as RhfResult.precision, UhfResult.precision,
DftResult.precision, RiMp2Result.precision and
RiMp2Result.scf_precision. An all-f64 run, which is every run that does not
set [precision] mode = "mixed", says so explicitly:
{"precision": "f64", "counter_scope": "process", "kernels": []}
Otherwise kernels has one entry per kernel that was allowed to run mixed
for that result, or that ran mixed:
| field | meaning |
|---|---|
kernel | the kernel name (rimp2-energy, xc-vxc, dfj-pack, dfk-occ, cosx-kern) |
requested | mixed when [precision] allowed it, f64 when only a test seam ran it |
mode | mixed when at least one call ran mixed, else f64 (allowed, but nothing ran mixed) |
device | where the mixed calls ran: "cpu", or {"ordinal", "name", "cc"} of the CUDA device; null when calls_mixed is 0 |
calls_mixed | calls that ran mixed: one RI-MP2 energy (per spin block when unrestricted), one XC potential pass, one RI-J or DF-K build, one COSX K build with f32 units |
calls_f64_fallback | {"device_f64", "f32_range", "cpu_f64"}: calls, or parts of calls, that ran f64 instead (mixed kernel unavailable on the device, a value beyond the f32 range, or the whole call on the CPU) |
ladder | for xc-vxc, dfj-pack and dfk-occ: {"builds_mixed", "builds_total", "exit_rebuilt_f64"} of the SCF precision ladder; null for the other kernels |
error_bound | {"kind": "none_computed", "why": ...} for every kernel today. No kernel computes a per-run error bound yet; the why says what exists instead (the measured error maps on the validation page). A computed bound would read {"kind": "a_posteriori", "eh", "method"}; a number is never invented |
precision is mixed when any kernel's mode is mixed. The counts are
process-wide counter differences over the call, which counter_scope: "process" states in every record: two calculations running at the same time
in one Python process can see each other's counts; a sequential run is
exact. The record is observation only: an f64 energy is bit-identical
with or without it.
frequencies
Written once by a finished task = "frequencies" run, which writes no
run_end or result. It carries the numbers the printout rounds, at full
precision:
| field | meaning |
|---|---|
hessian_source | analytic, analytic-moving-grid or finite-difference |
energy | energy at the input geometry, Hartree; with [dft] dispersion the corrected total, KS + dispersion |
correction_energy | the dispersion part of energy (0 without [dft] dispersion) |
frequencies_cm1 | vibrational wavenumbers, cm⁻¹, ascending; negative = imaginary |
trans_rot_frequencies_cm1 | the projected-out translation/rotation modes (≈ 0, a diagnostic) |
asymmetry | largest |H_ij − H_ji| of the raw Hessian, Hartree/Bohr² |
n_gradient_evaluations | gradients the finite-difference Hessian took (0 when analytic) |
cartesian_hessian | the Cartesian Hessian before mass-weighting, row-major 3N × 3N, Hartree/Bohr² |
Non-finite floats are written as the strings "NaN", "inf", "-inf", as
everywhere in the log.
result
The number a correlated run was launched to produce, written once after the
method finishes. Plain SCF runs (rhf, uhf, rohf, ksdft) don't write
one, because for them run_end.energy is the answer.
| field | meaning |
|---|---|
kind | the method that produced it, e.g. "rimp2", "ccsd", "gw" |
total | the headline number a user would quote |
components | the decomposition that makes total checkable — e_corr, e_os/e_ss, the reference energy, and whatever else the method defines |
total and run_end.energy are different numbers for every post-SCF
method. A kind="rimp2" run's run_end.energy is the RHF energy it was built
on; its result.total is the RI-MP2 total. Reading the wrong one gives an
uncorrelated answer without any error.
components exists because a total alone cannot be reconciled against a
reference implementation; the pieces show where a disagreement comes from.
result_unlogged
A post-SCF method that isn't wired up to result yet writes this instead,
naming the kind. The methods that do write result are rimp2,
oo-rimp2, att-rimp2, laplace-mp2, laplace-sos-mp2, scs-mp2,
scs-mp2-2terfc, mp3, ccsd, ccd, ccsd(t), linlccd, drpa, mp2-v and
rs-mp2-rpa. For rimp2, drpa and linlccd the record's components
carry "local": null for the exact method, else {"scheme", "eps", "keep_fraction", "integral_direct"}. A rimp2 record also carries
"timings". A local att-rimp2 record carries both keys under
kind: "att-rimp2", and an exact att-rimp2 record has neither key, so read
them with a default. "timings" is null for the exact method; for a local run, the per-stage
wall-clock seconds {"t_spaces_s", "t_assembly_s", "t_solve_s", "t_reference_s", "direct"}, where direct is null in core and, with
integral_direct = true, every integral-direct assembly counter and timer
(strip_rows_mean/_max, strip_cols_mean/_max, strip_bytes_total,
n_eri3_shell_triples, n_eri3_skipped, n_metric_shell_pairs,
virt_cand_mean/_max, t_maps_s, t_eri3_s, t_eri3_fill_thread_s,
t_eri3_eval_thread_s, t_eri3_scatter_thread_s,
t_eri3_transform_thread_s, t_eri3_setup_s, n_eri3_slabs,
transform_flops, transform_flops_own_support, n_eri3_integrals,
n_eri3_null_blocks, aux_major_task_bytes_max, aux_major_concurrency,
aux_major_in_flight_peak, aux_major_coeff_bytes, n_batches, batch_work_max_s,
batch_work_mean_s, t_metric_s, t_pairs_s, n_pair_groups,
n_distinct_occ_per_group, n_trsm, n_w_reused, own_w_bytes,
own_w_bytes_held, group_scratch_bytes_max, t_loc_s). The
*_thread_s fields are summed over batch workers (thread-seconds), the
eval/scatter split is null unless it was measured, and t_reference_s is
0 when the opt-in reference was off. t_loc_s is counted inside
t_assembly_s, so do not add it to the other stages. Timings are not
reproducible run to run.
Without it, a log with no result record could mean the method does not
record one yet, or that the run died before producing one. With it, a missing
record means the run failed.
Example
A water/STO-3G RHF run, abridged:
{"record":"run_start","schema":1,"seq":0,"t":0.002,"ferric_version":"0.1.0","git_sha":"396e0d61eded-dirty","timestamp":"2026-09-18T16:04:12Z","config":{"method":"rhf","basis":"sto-3g","max_iter":100,"density_conv":1e-7,"memory_budget_bytes":13018454425,"rayon_num_threads":12,"mpi_ranks":1},"molecule":{"formula":"H2O","n_atoms":3,"n_basis":7,"n_electrons":10,"charge":0,"multiplicity":1}}
{"record":"guess_scf_iter","seq":1,"t":0.014,"method":"uhf","iter":1,"energy":-0.4665818503784864,"dp_rms":"inf","dp_max":"inf","de":0.4665818503784864,"err_max":0.0,"grad_rms":null,"rung":0}
{"record":"scf_iter","seq":5,"t":0.017,"method":"rhf","iter":1,"energy":-74.68273456362847,"de":74.68273456362847,"dp_rms":"inf","dp_max":"inf","err_max":1.6088693243641559,"grad_rms":0.5469406324593924,"rung":0}
{"record":"scf_iter","seq":13,"t":0.019,"method":"rhf","iter":9,"energy":-74.9631468000391,"de":5.684341886080802e-14,"dp_rms":1.356320515468342e-10,"dp_max":4.726057323267696e-10,"err_max":3.5214886562329184e-14,"grad_rms":1.2777284834157483e-14,"rung":0}
{"record":"ladder_rung","seq":14,"t":0.019,"rung":0,"tricks":["diis"],"max_iter":60,"iterations":9,"exit":"Converged","energy":-74.9631468000391,"converged":true}
{"record":"run_end","seq":15,"t":0.019,"energy":-74.9631468000391,"converged":true,"exit":"Converged","wall_s":0.0189,"cpu_s":0.05,"peak_rss_bytes":128815104,"extra":{"method":"rhf","task":"energy","scf_iterations":9,"energy_is":"total","precision":{"precision":"f64","counter_scope":"process","kernels":[]}}}
Periodic k-point stress
A periodic [cell] run with kmesh and stress = true adds
extra.stress_hartree_per_bohr3 (the analytic 3×3 stress tensor
σ = (1/Ω) dE/dε of the converged SCF, rows and columns in the Cartesian x, y, z
of the lattice frame, full f64 precision), extra.stress_gpa (the same tensor
in GPa) and extra.stress_unit = "hartree_per_bohr3". run_end.energy is the
total energy per cell. run_start.config.stress records the request; run_end.extra
also carries stress_requested and scf_iterations. If the SCF does not converge
the tensor is absent and run_end has converged = false, exit = "not_converged";
a library error closes the log with exit = "error" and extra.error.
Periodic geometry optimization
A periodic [cell] run with task = "optimize" (Gamma point or kmesh) adds
extra.final_geometry_bohr (the returned Cartesian positions in Bohr, flat
[x0, y0, z0, x1, ...], full f64 precision) and
extra.final_gradient_hartree_per_bohr (the analytic gradient dE/dR per cell
evaluated at exactly that geometry, same layout). run_end.energy is the
energy per cell at that geometry. When the optimization ends in an error (a refusal or a non-converged SCF
step) the run exits with the error and writes no run_end record, so neither
key exists; an optimization that stops at max_steps without converging
still writes both (run_end.converged = false).
Point-group-blocked runs
With [scf] symmetry = true the terminal record carries
extra.symmetry = {point_group, max_symmetrization_shift_bohr, irrep_nelec, geometry}.
irrep_nelec maps each irrep to its electron count in the converged state,
geometry is "symmetrized", and run_end.energy is the energy of that
symmetrized geometry (it differs from the input geometry's by the effect of
max_symmetrization_shift_bohr, at most 1e-4 Bohr). Without the key there is
no symmetry field.
Does it change the answer?
No. SCF energies are bit-identical (f64::to_bits()) with the log on and
off, asserted across RHF, RKS, UHF and ROHF in
crates/ferric-scf/tests/runlog_bit_identity.rs. Every value written is one
the solver had already computed for its own convergence decision, and nothing
is read back.
Does it cost anything?
The per-record cost has not yet been measured on a quiet machine. Run
scripts/bench-runlog-overhead.sh to measure it; the script explains why the
emit path must be timed directly rather than by diffing whole-process wall
times, and what to compare the result against.
Einsum spec grammar
ferric contracts two tensors under a spec string such as ij,jk->ik. The same parser serves the Rust einsum! macro, ferric_tensors::einsum_dyn and the Python function ferric.einsum.
The grammar is stricter than numpy.einsum. A spec that breaks a rule is an error; there is no fallback to another algorithm.
Every example below is a test in crates/ferric-tensors/tests/einsum_spec_examples.rs with a hand-computed expected value. A further test fails if a spec tested there is missing from this page. The parser is crates/ferric-einsum-spec/src/lib.rs.
Entry points
| Entry point | Spec checked | Scale | Scalar spec returns |
|---|---|---|---|
einsum!(spec, &a, &b) (Rust macro) | at compile time; the spec must be a string literal | optional fourth argument, default 1.0 | f64 |
einsum_dyn(spec, a, b, scale) (Rust) | at run time, as TensorError | scale: f64 | 0-dimensional ArrayD<f64> |
ferric.einsum(spec, a, b, scale=1.0) (Python) | at run time, as ValueError | scale keyword | Python float |
The macro takes Tensor or ArrayD<f64> operands. For Tensor operands a debug build also checks that contracted and batch axes carry the same Axis label. A shape mismatch inside a macro call panics; einsum_dyn returns an error instead.
Grammar
spec = left "," right "->" output
left, right, output = zero or more index characters
- Spaces are removed before parsing.
- An index is one
char, andiandIare different indices. The parser splits on the first->and on,, so those characters cannot be indices. - The spec must contain
->and exactly two comma-separated inputs. - No index may repeat within one term, including the output. Traces and diagonals of one operand (
ii,...) are rejected. - Each operand has as many axes as its term has indices;
einsum_dynchecks this.
Index classes
Each index falls in one class by where it appears.
| Appears in | Class | Effect |
|---|---|---|
| both inputs, not the output | contracted | summed; the GEMM inner axis |
| both inputs and the output | batch | element-wise; one GEMM per batch slice |
| one input and the output | free | kept in the output |
| one input only | error | implicit single-operand sums are not supported |
An index in the output that is in neither input is also an error.
Output order
The output lists indices in three groups: batch indices, then free indices of the left operand, then free indices of the right operand. Order within a group is free and sets the output axis order. A group that appears out of sequence is an error.
Examples
Operands used below: A = [[1,2,3],[4,5,6]] (2x3), B = [[1,0],[0,1],[1,1]] (3x2).
| Spec | Operation | Result |
|---|---|---|
ij,jk->ik | matrix product A B | [[4,5],[10,11]] |
ij , jk -> ik | same; spaces ignored | [[4,5],[10,11]] |
ab,bc->ac | same; letters are labels only | [[4,5],[10,11]] |
ij,j->i | A times [1,1,1] | [6,15] |
i,j->ij | outer product of [1,2] and [3,4,5] | [[3,4,5],[6,8,10]] |
ij,ij-> | full contraction A:A | 91 |
ij,ij->ij | Hadamard product A*A | [[1,4,9],[16,25,36]] |
kij,kij->ij | dot over k per (i,j), k=2, shape (2,1,3) | [[17,29,45]] |
ijk,kl->jil | i,j free in operand order j,i; shape (1,2,2) with (2,1) | shape (2,1,1) |
Scale
The scale multiplies the whole result. With scale -0.5, ij,jk->ik gives [[-2,-2.5],[-5,-5.5]]. For a scalar spec, scale 2 gives 182.
let s: f64 = einsum!("ij,ij->", &a, &a, 2.0); // 182.0
let d = einsum_dyn("ij,ij->", a.view(), a.view(), 2.0).unwrap();
assert_eq!(s, d[IxDyn(&[])]);
Errors
Each message is asserted verbatim in malformed_specs_are_errors and operand_mismatches_are_errors_at_run_time. einsum! reports parse errors as compile errors with the same text.
| Spec | Error |
|---|---|
ij,jk | einsum spec must contain '->' |
ij->ij | einsum spec must have exactly two comma-separated inputs |
ij,jk,kl->il | einsum is binary: exactly two inputs |
ii,jk->jk | index 'i' is repeated in the left operand (traces / diagonals of one operand are not supported) |
ij,jk->kk | index 'k' is repeated in the output (traces / diagonals of one operand are not supported) |
ij,jk->k | index 'i' appears in an input but is neither contracted nor in the output (implicit single-operand sums are not supported) |
ij,jk->ikz | output index 'z' is not a free or batch index of either input |
ij,jk->ki | output indices must be listed as (batch indices, then left-operand free indices, then right-operand free indices) to match the runtime axis order |
einsum_dyn adds two checks that are TensorError::Operand (Python ValueError):
- an operand whose number of axes differs from its term;
- a shared index whose extents differ between the operands.
An operand with a zero-extent axis returns an array of zeros with the output shape.
Execution
The contraction is one binary product, not a loop over indices.
- Each operand is permuted so batch, free and contracted axes are contiguous. When the needed order is the identity or the swap of the two axis groups, the operand is passed as a (transposed) view with no copy.
- Axes are flattened to a matrix and multiplied with
ndarray::linalg::general_mat_mul.ferric-tensorsbuilds ndarray with itsblasfeature throughndarray-linalg, so the product is an OpenBLASdgemm. - With batch indices, one GEMM runs per batch slice, in a sequential loop.
- The contracted axis is summed in blocks of
GEMM_K_BLOCK(128). Large products split output rows over rayon bands; this path is bit-identical to the serial product. - The scale is applied (
alphaof the batched GEMM, or a pass over the result when there is no batch index) and the result is reshaped to the output shape.
An outer product (i,j->ij) is a GEMM with inner dimension 1. Contractions run on the CPU unless the crate is built with the gpu feature.
Set OPENBLAS_NUM_THREADS=1 as described in Installation.
API documentation
The crate-level API docs are generated by rustdoc and published alongside this
book.
Browse the API documentation →
That link is only present when the most recent CI run on main succeeded.
cargo doc does not link, but it runs build scripts, and
ferric-integrals' build script compiles a C++ shim that needs libint2.hpp.
The API docs are therefore built by the CI workflow, which already builds and
caches libint2, and handed to the docs workflow as an artifact. If CI was red, the
book still publishes and this link 404s; generate the docs locally in that
case.
Generating locally
cargo doc --workspace --no-deps --open
Drop --no-deps to include dependency documentation as well (much slower, and
much larger).
If that fails with "Only one may be documented at once since they output to
the same path", add --exclude ferric-python. The pyo3 crate's lib is
named ferric so that Python's import ferric works, and that name collides
with the ferric facade crate. Excluding it loses nothing: it is a cdylib,
and its surface is documented in
Python bindings.
Entry points
The most useful starting points, by crate:
| Crate | Start at |
|---|---|
ferric_core | Molecule, BasisSet, Shell |
ferric_scf | solve_rhf, solve_uhf, solve_rohf; constrained DFT in cdft_driver::solve_cdft_uhf and cdft_coupling::coupling_hab (Python: run_cdft, cdft_coupling) |
ferric_mp2 | ri_mp2, oo_ri_mp2 |
ferric_cc | ccsd_closed_shell, ccsd_t_closed_shell |
ferric_rpa | pdep_polarizability_static, RPA correlation drivers |
ferric_gw | run_gw, run_evgw |
ferric_dft | KsXc, functional construction via libxc |
ferric_tensors | the einsum! macro |
On reading the docs
Doc comments record design reasoning at the point of use: convergence hazards, memory-accounting details, parallelizations that would be unsafe, and optimizations that were rejected, with the reason. They are kept next to the code rather than in prose docs, so they stay in sync with it.
References and citing
Citing ferric
If you publish a number computed with ferric, cite three things:
-
The software. The repository carries a
CITATION.cff, which GitHub turns into a "Cite this repository" entry:Goldey, M. ferric: a Rust-native quantum chemistry engine. https://github.com/mgoldey/ferric. Licensed MIT OR Apache-2.0.
Give the version and commit hash you ran (
ferric.build_info()reports both); the only releases so far are pre-releases (0.1.0rc*). -
The libraries every calculation goes through.
- libint2 (all Gaussian integrals): E. F. Valeev, Libint: a library for the evaluation of molecular integrals of many-body operators over Gaussian functions, https://github.com/evaleev/libint. Cite the version you built against (ferric builds on libint 2.7+).
- libxc (every DFT functional): S. Lehtola, C. Steigemann, M. J. T. Oliveira & M. A. L. Marques, SoftwareX 7, 1 (2018), doi:10.1016/j.softx.2017.11.002. Only needed if you ran a DFT functional or a DFT reference.
-
The method papers for what you ran, from the list below. Each method page also ends with a short Cite line naming the entries it relies on.
Methods implemented in ferric
Entries are limited to methods the code implements, and most are the references the source code itself cites.
SCF, convergence and integrals
- Szabo & Ostlund, Modern Quantum Chemistry (Dover, 1996)
- Pulay, Chem. Phys. Lett. 73, 393 (1980): DIIS
- Kudin, Scuseria & Cancès, J. Chem. Phys. 116, 8255 (2002): EDIIS
- Gilbert, Besley & Gill, J. Phys. Chem. A 112, 13164 (2008): maximum overlap method (MOM)
- Ochsenfeld, White & Head-Gordon, J. Chem. Phys. 109, 1663 (1998): LinK exchange
- Maurer, Lambrecht & Ochsenfeld, J. Chem. Phys. 136, 144107 (2012): QQR screening
- Thompson & Ochsenfeld, J. Chem. Phys. 147, 144101 (2017): CSB and CSAM integral screening
- White & Head-Gordon, J. Chem. Phys. 101, 6593 (1994): continuous fast multipole method
- Neese, Wennmohs, Hansen & Becker, Chem. Phys. 356, 98 (2009): COSX seminumerical exchange
- Izsák & Neese, J. Chem. Phys. 135, 144105 (2011): COSX overlap fitting
- Weigend, Phys. Chem. Chem. Phys. 4, 4285 (2002): RI-JK (fully direct RI-HF) and its auxiliary sets
- Dunlap, J. Mol. Struct. (THEOCHEM) 529, 37 (2000): robust density fitting
DFT, grids and dispersion corrections
- Becke, J. Chem. Phys. 88, 2547 (1988): multicentre integration (Becke partitioning)
- Treutler & Ahlrichs, J. Chem. Phys. 102, 346 (1995): radial grids (M4 mapping)
- Lebedev & Laikov, Dokl. Math. 59, 477 (1999): angular grids
- Perdew, Burke & Ernzerhof, Phys. Rev. Lett. 77, 3865 (1996): PBE
- Becke, J. Chem. Phys. 98, 5648 (1993); Stephens, Devlin, Chabalowski & Frisch, J. Phys. Chem. 98, 11623 (1994): B3LYP
- Vydrov & Van Voorhis, J. Chem. Phys. 133, 244103 (2010): VV10 nonlocal correlation
- Mardirossian & Head-Gordon, Phys. Chem. Chem. Phys. 16, 9904 (2014): ωB97X-V
- Sun, Ruzsinszky & Perdew, Phys. Rev. Lett. 115, 036402 (2015): SCAN
- Furness, Kaplan, Ning, Perdew & Sun, J. Phys. Chem. Lett. 11, 8208 (2020): r2SCAN
- Grimme, Antony, Ehrlich & Krieg, J. Chem. Phys. 132, 154104 (2010): DFT-D3
- Grimme, Ehrlich & Goerigk, J. Comput. Chem. 32, 1456 (2011): Becke–Johnson damping for D3
- Tkatchenko & Scheffler, Phys. Rev. Lett. 102, 073005 (2009): TS dispersion and free-atom reference data
- Gould & Bučko, J. Chem. Theory Comput. 12, 3603 (2016): free-atom reference data for Z = 19–54 (all but Pd)
- Jerabek, Schwerdtfeger & Nagle, Phys. Rev. A 98, 012508 (2018): free-atom Pd polarizability (26.14 a.u., closed-shell 4d¹⁰)
- Gobre, PhD thesis, TU Berlin (2016), Table A.1: free-atom Pd C6 (157.5 a.u., the TS value used by libMBD) and the free-atom vdW radii R_vdW (Z = 1–54) used by MBD@rsSCS
- Tkatchenko, DiStasio, Car & Scheffler, Phys. Rev. Lett. 108, 236402 (2012): many-body dispersion (MBD)
- Ambrosetti, Reilly, DiStasio & Tkatchenko, J. Chem. Phys. 140, 18A508 (2014): MBD@rsSCS (range-separated screening; β for PBE, PBE0, HSE06)
Solvation and embedding
- Cancès, Mennucci & Tomasi, J. Chem. Phys. 107, 3032 (1997): IEF-PCM
- Klamt & Schüürmann, J. Chem. Soc., Perkin Trans. 2, 799 (1993): COSMO
- Lin & Truhlar, J. Phys. Chem. A 109, 3991 (2005): redistributed-charge QM/MM boundary schemes
Geometry, frequencies and reaction paths
- Pulay & Fogarasi, J. Chem. Phys. 96, 2856 (1992): redundant internal coordinates
- Banerjee, Adams, Simons & Shepard, J. Phys. Chem. 89, 52 (1985): P-RFO saddle search
- Bofill, J. Comput. Chem. 15, 1 (1994): Hessian update for saddle searches
Charges and properties
- Breneman & Wiberg, J. Comput. Chem. 11, 361 (1990): CHELPG
- Bayly, Cieplak, Cornell & Kollman, J. Phys. Chem. 97, 10269 (1993): RESP
- Foster & Boys, Rev. Mod. Phys. 32, 300 (1960): Boys localization
MP2 family
- Weigend, Häser, Patzelt & Ahlrichs, Chem. Phys. Lett. 294, 143 (1998): RI-MP2 auxiliary basis sets
- Grimme, J. Chem. Phys. 118, 9095 (2003): SCS-MP2
- Jung, Lochan, Dutoi & Head-Gordon, J. Chem. Phys. 121, 9793 (2004): SOS-MP2
- Häser & Almlöf, J. Chem. Phys. 96, 489 (1992): Laplace-transform MP2
- Takatsuka, Ten-no & Hackbusch, J. Chem. Phys. 129, 044112 (2008): minimax Laplace quadrature
- Lochan & Head-Gordon, J. Chem. Phys. 126, 164101 (2007): orbital-optimized (opposite-spin) MP2
- Bozkaya, Turney, Yamaguchi, Schaefer & Sherrill, J. Chem. Phys. 135, 104103 (2011): orbital-optimized MP2 algorithm
- Lee & Head-Gordon, J. Chem. Theory Comput. 14, 5203 (2018): κ-regularized MP2
- Dutoi & Head-Gordon, J. Phys. Chem. A 112, 2110 (2008): the terfc attenuator
- Goldey & Head-Gordon, J. Phys. Chem. Lett. 3, 3592 (2012): attenuated MP2 (erfc, aug-cc-pVDZ)
- Goldey, Dutoi & Head-Gordon, Phys. Chem. Chem. Phys. 15, 15869 (2013): attenuated MP2 in aug-cc-pVTZ (terfc)
- Goldey & Head-Gordon, J. Phys. Chem. B 118, 6519 (2014): SCS-MP2(2terfc), separate attenuation of the two spin components
- Goldey, Belzunces & Head-Gordon, J. Chem. Theory Comput. 11, 4159 (2015): MP2-V
- Wang, Aldossary, Shi, Liu, Li & Head-Gordon, J. Chem. Theory Comput. 19, 7577 (2023): single-threshold local MP2 (the "WSHG23" scheme in the code)
Coupled cluster
- Scuseria, Janssen & Schaefer, J. Chem. Phys. 89, 7382 (1988): CCSD
- Hirata, Podeszwa, Tobita & Bartlett, J. Chem. Phys. 120, 2581 (2004): spin-adapted closed-shell CCSD equations
- Raghavachari, Trucks, Pople & Head-Gordon, Chem. Phys. Lett. 157, 479 (1989): CCSD(T)
- Rendell, Lee & Komornicki, Chem. Phys. Lett. 178, 462 (1991): closed-shell (T) algorithm
- Carter-Fenk, J. Phys. Chem. A 129, 7251 (2025): LinLCCD(hh)
- Ransford & Carter-Fenk, Phys. Chem. Chem. Phys. 28, 14428 (2026): ωB97X-L-V
- Bartlett & Musiał, Rev. Mod. Phys. 79, 291 (2007): coupled-cluster theory (review)
Double hybrids
- Grimme, J. Chem. Phys. 124, 034108 (2006): B2PLYP
- Kozuch & Martin, Phys. Chem. Chem. Phys. 13, 20104 (2011): DSD-PBEP86
RPA, GW and excited states
- Wilson, Gygi & Galli, Phys. Rev. B 78, 113303 (2008): iterative dielectric eigenpotentials (PDEP)
- Eshuis, Yarkony & Furche, J. Chem. Phys. 132, 234114 (2010): RI-RPA and its frequency quadrature
- Kaltak, Klimeš & Kresse, J. Chem. Theory Comput. 10, 2498 (2014): minimax imaginary-frequency grids
- Hedin, Phys. Rev. 139, A796 (1965): the GW approximation
- van Setten et al., J. Chem. Theory Comput. 11, 5665 (2015): GW100 benchmark (the MOLGW reference values)
- Dreuw & Head-Gordon, Chem. Rev. 105, 4009 (2005): TDDFT and CIS (review)
Constrained DFT
- Wu & Van Voorhis, J. Chem. Phys. 125, 164105 (2006): electron-transfer couplings from constrained DFT
Background on MP2's dispersion error
Not implemented in ferric; cited on Electronic response:
- Cybulski & Lytle, J. Chem. Phys. 127, 141102 (2007)
- Heßelmann, J. Chem. Phys. 128, 144112 (2008)
- Pitoňák & Heßelmann, J. Chem. Theory Comput. 6, 168 (2010)
Software dependencies
- libint2: Gaussian integral engine
- libxc: exchange–correlation functionals
- pyo3: Rust/Python interop
- ndarray and ndarray-linalg: arrays and LAPACK bindings
- D3 reference tables are generated from simple-dftd3 (LGPL-3.0-or-later)
License
Dual-licensed under either
- Apache License, Version 2.0 (LICENSE-APACHE)
- MIT License (LICENSE-MIT)
at your option.