ferric
A Rust-native quantum chemistry engine, wrapping libint2 for electron integrals, with pyo3 Python bindings.
ferric is organized around one object: electronic response — how the
density reacts to a perturbation. That object shows up as the polarizability
\( \alpha \), the dielectric function \( \varepsilon \), and the
susceptibility \( \chi \), and the methods here are three faces of getting it
right where standard methods get it wrong.
- Attenuated MP2 — MP2 builds dispersion from an uncoupled polarizability that over-polarizes, giving too-large \( C_6 \) and overestimated π-stacking. Attenuating the correlation operator tames that response error with a single tunable parameter.
- PDEP-RPA / GW — the dielectric matrix is the density–density response. PDEP keeps only its dominant low-rank eigenmodes, so RPA correlation and the GW screened interaction need no explicit sum over empty states.
- Constrained DFT — a constraint couples to the density and reads its response (\( \partial N / \partial \lambda \) is a susceptibility), building charge-localized diabatic states and their electron-transfer couplings.
The motivating claim is that response is local in real space and low-rank in its eigenspectrum, so organizing around it should make the computation cheaper.
Implemented ≠ validated
Working code is not a checked number. This documentation describes what exists; it is not a claim that every number is trustworthy.
For how strongly each capability's numbers are checked against ground truth — and where they are known to fail — see What is validated. Capability maturity varies a great deal between methods, and they are graded individually rather than presented as a flat list of equals.
Where to start
| If you want to | Go to |
|---|---|
| Understand the design | Electronic response |
| Run a calculation | Quick start |
| Build it | Installation |
| Call it from Python | Python bindings |
| Read the crate docs | API documentation |
| Know what to trust | What is validated |
Source
github.com/mgoldey/ferric — dual-licensed MIT / Apache-2.0.
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)). That
is a perfectly good organizing principle. It is not the one used here.
The object of interest appears under several names depending on which perturbation you apply:
| Perturbation | Response object |
|---|---|
| Uniform electric field | Polarizability \( \alpha \) |
| Density fluctuation | Susceptibility \( \chi \) |
| Screened Coulomb interaction | Dielectric function \( \varepsilon \) |
| Constraint potential | \( \partial N / \partial \lambda \) |
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 motivating 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 matrix 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, then organizing the computation around response should make it cheaper: attenuate the operator, keep the dominant dielectric modes.
What is actually demonstrated
This is where honesty matters more than the pitch.
The low-rank half is demonstrated. PDEP's compression of the dielectric matrix works and is used in production paths — see RPA and GW. Keeping only the dominant eigenmodes removes the explicit sum over empty states that conventional RPA and GW require.
The real-space locality half remains a design premise, not a measured result. Several attempts to exploit it are implemented and measured negative:
- The AO-sparse Laplace SOS-MP2 variant's truncation radius tracks the molecular diameter instead of saturating — so it is not a reduced-scaling path.
- Local MP2 (amplitude-threshold) is implemented with localized virtuals and per-pair domain-local RI fits, but the J build is still dense-from-RI, so no scaling claim is made.
- RI-Laplace MP2 is dense; it serves as a correctness reference for the AO formulation, not as an O(N) path.
Those are reported as negative results rather than quietly omitted, because a locality claim that has not survived measurement is not a feature.
Why this framing is useful anyway
Even where the scaling payoff has not materialized, the response framing buys something concrete: it makes the error in one method diagnosable through another.
MP2's dispersion error is the clearest case. MP2 builds dispersion from an uncoupled polarizability, which over-polarizes — giving \( C_6 \) coefficients that are too large and overestimated π-stacking. That is not a mysterious failure of a wavefunction ansatz; it is a specific, identifiable defect in a response function, and it suggests a specific fix: attenuate the correlation operator so the over-polarizing long-range part is damped.
That is attenuated MP2, and it works for a reason the response picture predicts.
Where the methods come from
The three method families in ferric are not an arbitrary selection. Each one
attacks the response function from a different direction.
Attenuated MP2 — fixing a response error
MP2 correlation is built from an uncoupled polarizability. Uncoupled means the density fluctuation does not feel the field it creates: there is no self-consistency in the response. The result over-polarizes, which shows up as:
- \( C_6 \) dispersion coefficients that are too large
- overestimated π-stacking energies
- basis-set superposition error that partly cancels the overestimate, disguising the problem in small basis sets
Attenuating the correlation operator — replacing \( 1/r \) with
\( \mathrm{erfc}(\omega r)/r \) or a terfc form — damps the long-range part
where the uncoupled approximation is worst, with a single tunable parameter.
This is Goldey & Head-Gordon (JPCL 2012); the dual-attenuated SCS variant is Goldey, Dutoi & Head-Gordon (PCCP 2013). See The MP2 family.
PDEP-RPA and GW — compressing the response
The dielectric matrix is the density–density response function. Conventional RPA and GW evaluate it through an explicit sum over empty orbital states, which is expensive and converges slowly with basis size.
PDEP — projective dielectric eigenpotentials — builds a low-rank basis from the dominant eigenmodes of the dielectric matrix instead. Because the spectrum decays quickly, a modest number of modes captures the physics, and the sum over empty states disappears.
This is the part of the locality-and-low-rank claim that is actually demonstrated in this codebase. See RPA and GW.
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, and it yields charge-localized diabatic states whose electron-transfer couplings \( H_{ab} \) follow from non-orthogonal determinant overlaps.
See Constrained DFT.
What this buys
Three methods, 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 the measured negatives); that it pays off in diagnosis is already clear.
Installation
ferric links against libint2, a C++ integral library that must be built
from source. That build takes roughly 30 minutes and is the main cost of getting
started.
Prerequisites
- Rust 1.75+ — install via rustup
- libint2 2.7+ — from the mpqc4 tarball
- OpenBLAS and LAPACK
- Eigen3 headers
- Python 3.10+ and maturin — optional, for the Python bindings
What the mpqc4 export does and does not carry
These capabilities are fixed when the tarball is generated, so no cmake flag
changes them. compiler.config inside the tarball records the exact settings.
| Capability | mpqc4 export | Needed for |
|---|---|---|
| 1st derivatives | yes | analytical gradients, geometry optimization |
| RI / 3- and 2-center ERI | yes | RI-MP2, RPA, GW |
| 2nd derivatives | no | analytical Hessians / frequencies |
| G12 geminal | no | F12 / geminal integrals |
Building against this tarball is correct for everything ferric currently
validates. The G12-dependent tests detect its absence at run time and skip
with an explicit message rather than failing.
Getting either missing capability requires re-generating libint2 from the
upstream source repo with the corresponding --enable-* flags — a substantially
longer build.
Building from source
# System dependencies (Ubuntu 22.04+)
sudo apt-get install -y build-essential cmake g++ gfortran wget \
libeigen3-dev libopenblas-dev liblapack-dev pkg-config \
python3-dev python3-pip python3-venv
# Build and install libint2 (~30 min)
wget https://github.com/evaleev/libint/releases/download/v2.7.2/libint-2.7.2-mpqc4.tgz
tar xzf libint-2.7.2-mpqc4.tgz
cd libint-2.7.2-mpqc4
mkdir build && cd build
cmake .. -DCMAKE_INSTALL_PREFIX=$HOME/.local -DCMAKE_POSITION_INDEPENDENT_CODE=ON
make -j$(nproc)
make install
cd ../..
# Build ferric
cargo build --release
# Run tests
OPENBLAS_NUM_THREADS=1 cargo test --workspace
Debug vs release
The difference is large enough to matter in practice:
- 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 permit. - Release (
cargo build --release) — slow to compile, fast to run. Use it for anything you will actually wait on: real molecules, benchmarks, and any RPA/GW/CC job.
Python bindings
# Set up the venv and install the extension in editable/develop mode
python3 -m venv .venv && source .venv/bin/activate
pip install maturin
maturin develop --release
# Verify
python -c "import ferric; print(ferric.__file__)"
See Python bindings.
Optional: MPI
The mpi feature additionally needs an MPI implementation (OpenMPI or MPICH)
and libclang (libclang-dev, for mpi-sys's bindgen step).
Threading
Set OPENBLAS_NUM_THREADS=1. ferric uses rayon for outer parallelism and pins
BLAS to a single thread inside rayon workers; letting OpenBLAS thread on top of
that oversubscribes the machine and produces unstable timings.
Quick start
Both interfaces cover most methods: a TOML-driven CLI and Python bindings.
Build first — see Installation.
ferricneeds libint2 built and on the linker path.
CLI
Calculations are described by a TOML file. The examples/ directory has one per
method.
# RHF on water with STO-3G
cargo run --release -- examples/water-rhf.toml
# RI-MP2 on water with cc-pVDZ / cc-pVDZ-RI
cargo run --release -- examples/water-rimp2.toml
# Attenuated RI-MP2 (short-range correlation only, r0 = 1.05 Å)
cargo run --release -- examples/water-attmp2.toml
# SCS-MP2 (Grimme spin-component scaling)
cargo run --release -- examples/water-scs-mp2.toml
# SCS-MP2(2terfc) (dual-attenuated, Goldey/Head-Gordon 2013)
cargo run --release -- examples/water-scs-mp2-2terfc.toml
# CCSD (H2/STO-3G)
cargo run --release -- examples/water-ccsd.toml
# LinLCCD(hh) — linearized hole-hole ladder CCD (closed-shell only)
cargo run --release -- examples/water-linlccd.toml
# wB97X-L-V — a double hybrid built on LinLCCD(hh) instead of MP2
cargo run --release -- examples/water-wb97xlv.toml
CLI coverage is not complete. Only method.kind = "ccsd" is wired for
coupled cluster; CCD and CCSD(T) are library/Python-only. Use
ferric.run_ccd / ferric.run_ccsd_t from Python until a CLI arm is added.
Python
import ferric
mol = ferric.Molecule.from_xyz("testdata/molecules/water.xyz")
bs = ferric.BasisSet.bundled("cc-pvdz")
aux = ferric.BasisSet.bundled("cc-pvdz-ri")
# Standard RI-MP2
mp2 = ferric.run_rimp2(mol, bs, aux)
print(f"RI-MP2 total: {mp2.total_energy:.10f} Ha")
# Attenuated RI-MP2 (omega in Å⁻¹)
att = ferric.run_attenuated_rimp2(mol, bs, aux, omega=0.420)
print(f"Att-MP2 total: {att.total_energy:.10f} Ha "
f"(E_OS={att.e_os:.6f}, E_SS={att.e_ss:.6f})")
# SCS-MP2 (Grimme defaults)
scs = ferric.run_scs_mp2(mol, bs, aux)
# SCS-MP2(2terfc) — thesis defaults r0_1=0.75Å, r0_2=1.05Å, c_OS=1.27, c_SS=4.05
terfc = ferric.run_scs_mp2_2terfc(mol, bs, aux)
# Coupled cluster — RI-CCSD(T)
cc = ferric.run_ccsd_t(mol, bs, aux)
print(f"CCSD(T) total: {cc.correlation_energy + cc.t_correction:.10f} Ha")
See Python bindings for the full surface and threading notes.
Threading
Set OPENBLAS_NUM_THREADS=1 when running tests or benchmarks. ferric uses
rayon for outer parallelism and pins BLAS to one thread inside rayon workers;
letting OpenBLAS thread on top of that oversubscribes the box and can produce
unstable timings.
OPENBLAS_NUM_THREADS=1 cargo test --workspace
For throughput across many independent jobs, prefer many single-threaded processes over one multi-threaded job.
Python bindings
The pyo3 bindings expose most of the library. Build them with
maturin develop --release — see Installation.
Basics
import ferric
mol = ferric.Molecule.from_xyz("testdata/molecules/water.xyz")
bs = ferric.BasisSet.bundled("cc-pvdz")
aux = ferric.BasisSet.bundled("cc-pvdz-ri")
Bundled orbital bases include STO-3G, 6-31G, cc-pVDZ and def2-SVP; bundled auxiliary bases include cc-pVDZ-RI, the def2-*-RIFIT family, and def2-universal-jkfit. Both BSE-JSON and Gaussian-94 basis files can be parsed from disk.
Ground state
rhf = ferric.run_rhf(mol, bs)
print(f"RHF: {rhf.energy:.10f} Ha, converged={rhf.converged}")
uhf = ferric.run_uhf(mol, bs, multiplicity=3)
rohf = ferric.run_rohf(mol, bs, multiplicity=3)
dft = ferric.run_dft(mol, bs, functional="b3lyp")
Always check .converged. These functions return a result whether or not the
SCF converged — a non-converged result is not an error, it is a result with
converged = False. Treating it as success is a common way to get a plausible,
wrong number.
Correlation
mp2 = ferric.run_rimp2(mol, bs, aux)
att = ferric.run_attenuated_rimp2(mol, bs, aux, omega=0.420) # Å⁻¹
scs = ferric.run_scs_mp2(mol, bs, aux)
terfc = ferric.run_scs_mp2_2terfc(mol, bs, aux)
ccd = ferric.run_ccd(mol, bs, aux)
ccsd = ferric.run_ccsd(mol, bs, aux)
ccsd_t = ferric.run_ccsd_t(mol, bs, aux)
run_ccd and run_ccsd_t are Python-only — they are not yet CLI-wired.
Response and excited states
gw = ferric.run_gw(mol, bs, aux) # G0W0 quasiparticle energies
tddft = ferric.run_tddft(mol, bs, aux) # TDA or Casida
run_tddft warns on stderr when the reference is not pure Hartree–Fock: the
(ia|f_xc|jb) XC-kernel response is unimplemented, so with a DFT reference the
excitation energies omit a physical term and are approximate. Only
c_hf = 1.0 (CIS/TDHF) is exact.
Memory budgets
Most drivers accept memory_budget_gb. The budget is enforced, not advisory:
a job whose predicted peak exceeds it is refused with a breakdown naming the
dominant term, rather than being OOM-killed partway through.
mp2 = ferric.run_rimp2(mol, bs, aux, memory_budget_gb=8.0)
Threading and concurrency
The compute drivers release the GIL, so independent jobs submitted from a
ThreadPoolExecutor genuinely run in parallel rather than serializing at the
FFI boundary.
Set OPENBLAS_NUM_THREADS=1. ferric uses rayon for outer parallelism and pins
BLAS to one thread inside rayon workers; for throughput across many jobs, prefer
many single-threaded processes over one wide job.
Property export
ESP at nuclei, electric fields, static and atom-partitioned polarizabilities, Hirshfeld and Löwdin charges, and density matrices are available, with NPZ export of ML-ready features (MO coefficients, orbital energies, PDEP eigenvectors, ESP, polarizability tensors, charges) for downstream model conditioning.
For the full signature list, see the API documentation.
Methods overview
What exists, grouped by family. Maturity varies a great deal between these — see What is validated before trusting any particular number.
| Family | Methods | Page |
|---|---|---|
| SCF | RHF, UHF, ROHF, KS-DFT, gradients | SCF and DFT |
| MP2 | RI, attenuated, SCS, OO, Laplace, MP3, LMP2 | The MP2 family |
| Coupled cluster | CCD, CCSD, (T), LinLCCD | Coupled cluster |
| Response | PDEP-RPA, G0W0, COHSEX, evGW, TDDFT | RPA and GW |
| Electron transfer | cDFT, \( H_{ab} \) couplings | Constrained DFT |
Infrastructure
Shared machinery underneath all of the above:
- QQR screening (Maurer, Lambrecht & Ochsenfeld 2012) and LinK exchange (Ochsenfeld, White & Head-Gordon 1998) for the Fock build
- Spherical and Cartesian basis support, with BSE-JSON and Gaussian-94 parsers
einsum!— a tensor-contraction macro routing contractions through BLAS3 GEMMs, used throughout the CC and MP3 code- Memory budgets — enforced allocation ceilings that refuse an oversized job with a named breakdown rather than letting it be OOM-killed
- Python bindings (pyo3) and a TOML-driven CLI
Properties
ESP at nuclei, electric field, static and atom-partitioned polarizabilities, Hirshfeld and Löwdin charges, density matrices, and NPZ export of ML-ready features for downstream generative-model conditioning.
A note on negative results
Several reduced-scaling approaches in this codebase are implemented and measured negative — they are documented as such rather than omitted:
- AO-sparse Laplace SOS-MP2 — truncation radius tracks the molecular diameter instead of saturating
- Local MP2 — the J build is still dense-from-RI, so no scaling claim is made
- TDHF/RPAx \( C_6 \) — stays ~60% low regardless of gap
Knowing which ideas did not work is part of the documentation.
SCF and DFT
Ground-state self-consistent field methods, plus analytical nuclear gradients.
Hartree–Fock
- RHF (closed-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
- UHF / ROHF (open-shell) with per-spin DIIS, virtual-space level shifting, augmented-Hessian Newton, and Maximum-Overlap-Method (MOM) orbital tracking for near-degenerate cases
The convergence machinery is not decoration. Heavy atoms and near-degenerate frontier orbitals genuinely break plain DIIS; the virtual-block level shift and MOM exist because specific systems failed without them.
Kohn–Sham DFT
Closed- and open-shell (RKS / UKS / ROKS) via libxc:
- LDA, GGA, hybrid, and range-separated-hybrid functionals — LDA, PBE, B3LYP, ωB97X-V
- Becke–Lebedev grids, with pruning
- VV10 nonlocal correlation
Gradients
Analytical nuclear gradients for RHF, UHF, ROHF and KS-DFT — including grid response — validated against finite differences.
Hessians are not implemented. The mpqc4 libint2 export does not carry second-derivative integrals, so the CPKS machinery is stubbed with explicit TODOs rather than silently absent. See Installation.
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 are real. For systems like alkane chains, different initial guesses converge to genuinely different SCF solutions — not a convergence failure but a different 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.
Screening
- Schwarz bounds on every 4-centre path, built so they can never underestimate (a zero-valued table entry once cost 1.5e-4 Ha; it is now floored)
- LinK (Ochsenfeld, White & Head-Gordon 1998) — exchange via significant-pair and density-pair lists; the lists were corrected in #50 and 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 before the #50 list fix)
- COSX shell-pair screening uses a primitive-level Hölder bound that provably never underestimates; an earlier overlap-based bound did, and silently corrupted K
Choosing how exchange is built
Four ways to build K, and the choice is a real one — measured on this code, not a rule of thumb. Butane, one thread; the QZ column is def2-QZVP (528 functions), the TZ column def2-TZVP (184).
[scf] setting | what it is | exact? | K at TZ | K at QZ | scope |
|---|---|---|---|---|---|
| (default) | Schwarz-screened direct 4-centre J+K | yes | — | 400 s (J+K) | all SCF types |
k_builder = "link" | LinK — pair-list-screened direct K | yes | re-measuring (#50) | re-measuring (#50) | RHF, UHF, ROHF |
df_j_aux / df_k_aux | density-fitted J and K (RI-JK) | ~1e-5 Ha | 0.05 s | 0.43 s | all SCF types |
k_builder = "cosx" | seminumerical (COSX) K on a grid | grid error, see below | 358 s* | 137 s | RHF/UHF/ROHF, Coulomb only, no gradients |
* full SCF at TZ was 358 s for COSX against 98 s direct — COSX is slower at TZ.
Start with density fitting. RI-JK is two to three orders of magnitude
faster than anything else here whenever its three-index tensor fits in memory
(n_aux × n_bf² × 8 bytes — 1 GB for butane/QZVP, 7 GB for octane/QZVP), and
it spills to disk when it does not. Its error with a JK-fitting auxiliary basis
is a few µHa. If your system fits, this is the answer and the rest of this
section is about when it does not.
Need exact exchange? Direct or LinK; both are exact to the screening
threshold as K builders. LinK's pair lists were fixed in #50 (three
pair-list defects; butane/def2-SVP link == direct to 9e-12 Ha). Every LinK
timing taken before that fix was against a kernel that skipped quartets, so
none is repeated here; its cost against the corrected kernel is being
re-measured. k_builder is honoured by UHF and ROHF as well as RHF (it was
silently ignored for open-shell runs before 2026-09-08): 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. It is skipped with a warning, never silently, whenever
density-fitted J/K is active, the functional uses no exact exchange, or the
functional is range-separated (exchange then comes from the SR/LR fitters).
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 reaches analytic exchange only at quadruple-zeta: on butane/def2-QZVP a COSX K build is 137 s against 400 s for the default direct J+K build (parity; J and K share that sweep), while at TZ the full COSX SCF is 3.7× slower than direct (358 s vs 98 s). Below QZ it is the wrong tool. Ratios against LinK are withheld until LinK is re-measured with the #50 lists.
Its integral work is sub-quadratic in system size — a density-driven pair
screen (on the product of the integral bound and the local half-transformed
density) gives an A-build tail exponent of N^1.5 on C12–C20 alkanes at
def2-SVP, with a K error below 2e-6 Ha at the default threshold. The half-transforms D·X and X·Gᵀ are still dense
GEMMs, which grow faster and are a third of the build by C20; until they are
made sparse (the standard next step), expect the full build to scale roughly
N^2 past a dozen heavy atoms even though the integrals do not.
COSX's error is a grid error, and it is not µHa-small: 5e-6 Ha on water/cc-pVDZ and 1.2e-4 Ha on butane/def2-TZVP at the default grid. Reaction energies cancel most of it (0.02 kcal/mol on an isodesmic alkane reaction at the same grid); absolute energies do not. Three knobs, all optional:
cosx_grid = { radial = 50, angular = 110 }is the default and the coarsest grid that meets a 0.1 kcal/mol reaction-energy bar. Coarser grids fail it. Finer grids reduce the error roughly tenfold per step and cost proportionally.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 is strongly molecule-dependent — large on water, nil to negative on ethane — so do not expect the factor quoted in the literature.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 together
with df_k_aux, is refused or warned about rather than silently ignored.
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, not just
intended.
This matters more than it might seem: a tree-fold reduction would be equally deterministic but would produce different bits, since floating-point addition is not associative. The ascending order is load-bearing.
The MP2 family
The largest method family here, and the one most directly tied to the response framing.
Why attenuation
MP2 builds dispersion from an uncoupled polarizability — the density fluctuation does not feel the field it creates. That over-polarizes, producing:
- \( C_6 \) coefficients that are too large
- overestimated π-stacking
- an error partly masked by BSSE in small basis sets, which is why the problem is easy to miss
Attenuating the correlation operator damps the long-range part where the uncoupled approximation is worst. One parameter, and the error it targets is identifiable rather than empirical.
Variants
RI-MP2 — density-fitted via 3-center/2-center integrals. Canonical MP2 is also implemented, for cross-validation rather than production.
Attenuated RI-MP2 — \( \mathrm{erfc}(\omega r)/r \) and terfc operators
(Goldey & Head-Gordon, JPCL 2012).
SCS-MP2 — Grimme spin-component scaling (JCP 2003), and SCS-MP2(2terfc), dual-attenuated (Goldey, Dutoi & Head-Gordon, PCCP 2013).
OO-RI-MP2 — orbital-optimized, with level-shifted Newton, orbital DIIS, Cayley rotations and backtracking.
MP3 — spin-orbital third-order Møller–Plesset via the einsum! framework.
MP2-V — attenuated MP2 combined with VV10 nonlocal correlation.
Laplace formulations
RI-Laplace MP2 — AO-Laplace via pseudo-density matrices. The implementation is dense; it is the correctness reference for that formulation, not a reduced-scaling path. No O(N) has been measured.
Laplace SOS-MP2 — opposite-spin-only with a Laplace-factorized denominator
(c_os scaling, minimax quadrature). The MO and AO formulations agree to
machine precision.
The AO-sparse variant is measured negative: its truncation radius tracks the molecular diameter instead of saturating, so it does not deliver reduced scaling. That is reported rather than omitted.
Local MP2
Amplitude-threshold LMP2 — WSHG23 single-threshold, with localized virtuals and per-pair domain-local RI fits.
Counters only — no scaling claim is made. The J build is still dense-from-RI, so while the amplitude machinery is in place and anchored, the end-to-end cost is not reduced. The assembly step, not the solve, is the measured wall.
Size-extensivity
RI-MP2's total energy is size-extensive to 2e-12 Ha for a well-separated dimer versus twice the monomer — pinned by a test, not asserted. That is five orders inside the test's tolerance, so it passes on physics rather than on a loose bound.
Robust fitting
When the RI metric differs from the physical kernel — as it does for attenuated operators — robust (Dunlap) density fitting is required, not optional. Domain-local \( V^{-1} \) plus robust fitting gives size-extensive µHa-level MP2 error; the non-robust form collapses in a way driven by the metric, not by the domain size.
Coupled cluster
RI-based coupled cluster, validated against exact-integral and PySCF references.
What exists
- RI-CCD — doubles only
- RI-CCSD — singles and doubles
- (T) — the perturbative triples correction
H2O/cc-pVDZ CCSD(T) matches PySCF to roughly 1e-6 Ha.
CLI coverage is partial
Only method.kind = "ccsd" is wired into the CLI — see
examples/water-ccsd.toml. CCD and CCSD(T) are library/Python-only:
ccd = ferric.run_ccd(mol, bs, aux)
ccsd_t = ferric.run_ccsd_t(mol, bs, aux)
LinLCCD
Linearized hole-hole ladder CCD, closed-shell only. Its main use here is as the correlation component of wB97X-L-V — a double hybrid that converges its own KS reference and then adds a short-range LinLCCD(hh) correction, rather than the MP2 correction a conventional double hybrid uses.
The [dft] lambda and omega keys override the published 0.6 / 0.1
Bohr⁻¹ values; omitting them gives the published parameters.
Implementation
All contractions route through einsum!, a macro that maps tensor
contractions onto BLAS3 GEMMs. That matters for performance: the alternative —
many small explicit loops — leaves most of the machine's throughput unused.
The permutation copies that feed those GEMMs are parallelized. This is less trivial than it sounds: for a strided permutation the copy can dominate the contraction it feeds — measured at 47% at nv=40 and 70% at nv=80, since it is memory-bandwidth-bound and gets relatively worse with size.
Those copies are bit-identical regardless of thread count. A permutation is pure data movement — every output element written exactly once — so unlike a reduction there is no summation order to perturb. That property is pinned by a test that has been verified to fail when deliberately broken.
Memory
The amplitude tensors dominate, and they 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 rather than being OOM-killed midway.
The DLPNO family additionally reads its own budget, which it previously ignored.
RPA and GW
The part of the response claim that is actually demonstrated.
PDEP
The dielectric matrix is the density–density response function. Conventional RPA and GW evaluate it through an explicit sum over empty orbital states — expensive, and slow to converge with basis size.
PDEP — projective dielectric eigenpotentials — instead builds a low-rank basis from the dominant eigenmodes of the dielectric matrix. The spectrum decays quickly, so a modest number of modes captures the physics and the empty-state sum disappears.
This low-rank compression works and is used in production paths. It is the demonstrated half of the design premise; the real-space locality half is not (see Electronic response).
RPA
- PDEP-RPA — RPA correlation via a low-rank W basis in Gaussians
- U-PDEP-RPA — open-shell, over a spin-summed dielectric
- Attenuated RPA — short-range correlation via erfc
The solver defaults to Lanczos; a dense path is used for small problems. Note that the eigensolve is serial by design — that is a deliberate choice, not an oversight, and RPA here is already faster than the PySCF reference.
GW
- G0W0, COHSEX, evGW0, evGW
- U-GW — unrestricted
G0W0@HF matches MOLGW to roughly 5 meV.
The quasiparticle solve runs a Newton root-find on the self-energy, which is fragile near \( \Sigma_c \) poles. That fragility has a consequence worth recording: the frequency-quadrature loop inside the self-energy is a sequential floating-point accumulation, so it cannot be parallelized without changing summation order — and reordering would perturb quasiparticle energies in a thread-count-dependent way. The loop is deliberately left serial.
TDDFT
Linear response in both the Tamm–Dancoff approximation (TDA/CIS) and the full Casida equations, closed-shell references.
Important limitation. The \( (ia|f_{xc}|jb) \) XC-kernel response is not implemented. With a pure Hartree–Fock reference (\( c_{HF} = 1 \)) that term is identically zero and the result is exactly CIS/TDHF. With any DFT reference it is not zero, and the excitation energies omit it — they are approximate.
The code warns on stderr when \( c_{HF} \neq 1 \) rather than returning silently incomplete numbers.
Double hybrids
B2PLYP and DSD-PBEP86, plus wB97X-L-V — see Coupled cluster.
Dispersion and polarizability
Static and atom-partitioned polarizabilities, Casimir–Polder \( C_6 \) coefficients, and many-body dispersion.
TDHF/RPAx is a measured negative for dispersion: the static α it produces is reasonable, but the \( C_6 \) stays roughly 60% low regardless of gap. It is a polarizability tool, not a dispersion one.
Dynamic dRPA@PBE α, by contrast, gives \( C_6 \) roughly 3× better than the static Tkatchenko–Scheffler (TS) model.
Constrained DFT
Charge- and spin-constrained DFT, and the electron-transfer couplings that follow from it.
The response connection
A cDFT constraint couples a Lagrange multiplier \( \lambda \) to a fragment-weighted density operator. The derivative
\[ \frac{\partial N}{\partial \lambda} \]
— how much charge moves per unit constraint potential — is a susceptibility. So cDFT 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
The nesting is what makes cDFT more expensive than a plain SCF — each outer step is a full converged inner solve.
Electron-transfer coupling
Once you have two charge-localized diabatic states, the coupling \( H_{ab} \) between them follows from a non-orthogonal determinant overlap, computed via Löwdin biorthogonalization.
That gives the matrix element governing electron-transfer rates in Marcus theory, from states that are constructed rather than guessed.
A caveat
The cdft_lambda_tol convergence tolerance interacts with the coupling
calculation in a way worth checking: a loosely converged \( \lambda \)
produces diabatic states that are not quite the ones you asked for, and
\( H_{ab} \) inherits that error. Tighten it before trusting a coupling.
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 -> all 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.
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. A different-but-deterministic order — a tree-fold, say — would
not be acceptable, because floating-point addition is not associative.
Memory. MemoryPlan expresses what a path will allocate and when, so an
oversized job is refused before allocating, with a breakdown naming the dominant
term. Guards are tested in both directions: a starved budget must be refused,
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.
What is validated
Implemented ≠ validated. Working code is not a checked number.
This is the most important page in this documentation, and the one to read before trusting any result.
The authority is the wiki
The project wiki's VALIDATION.md is the authority on what each capability's
numbers are checked against, and where they are known to fail. It grades each
capability — proven / smoke / stub — rather than presenting them as a flat
list of equals.
This documentation describes what exists. It is not a claim that everything here is equally trustworthy, and the feature list on these pages should not be read as a validation claim.
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 — TDDFT's XC-kernel response is exactly this case, which is why it now warns
- 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. All of them have occurred and been fixed in this codebase. The remedy is not optimism — it is grading each capability separately and saying which ones are checked against ground truth.
Some anchors
Where numbers are checked, they are checked against external references rather than self-consistency:
| Capability | Anchor |
|---|---|
| CCSD(T) | H2O/cc-pVDZ matches PySCF to ~1e-6 Ha |
| G0W0@HF | matches MOLGW to ~5 meV |
| Gradients | validated against finite differences |
| RI-MP2 extensivity | 2e-12 Ha on a separated dimer |
| COSX exchange | water/cc-pVDZ dense-grid limit 3.3e-7 vs direct K; SCF dE 4.9e-6 Ha (water/cc-pVDZ), 1.7e-4 Ha (butane/def2-SVP), 1.2e-4 Ha (butane/def2-TZVP) at (50,110)+fit; butane/def2-QZVP K 137 s vs 400 s default direct J+K (one thread; LinK ratios withdrawn pending re-measurement after #50); density-driven screen t=1e-7 changes K by <= 2e-6 (water..octane); alkane_4..20/def2-SVP one-thread tail (C12-C20): A-build ~N^1.5, full K ~N^2.2 (dense half-transform GEMMs), slower than analytic exchange at SVP; Coulomb-only, no gradients. Open-shell (UHF/ROHF) wiring anchored 2026-09-08: CH3 doublet/cc-pVDZ SCF dE 1.96e-5 Ha (UHF) / 1.97e-5 Ha (ROHF) vs direct at (50,110)+fit, same iteration count; LinK over the same open-shell path is exact (UHF dE 0 bitwise, ROHF 1.4e-14 Ha) |
These are the figures stated in the repository itself. The wiki's
VALIDATION.md carries the full per-capability grading, including benchmark
sweeps (GW100 and others) whose numbers are not reproduced here — quoting a
benchmark MAE from memory rather than from the record is exactly the kind of
unchecked claim this page exists to discourage.
Known negatives
Reported rather than omitted:
- AO-sparse Laplace SOS-MP2 — truncation radius tracks molecular diameter instead of saturating; not a reduced-scaling path
- Local MP2 — J build still dense-from-RI; no scaling claim made
- TDHF/RPAx \( C_6 \) — ~60% low regardless of gap
- TDDFT with a DFT reference — omits the \( (ia|f_{xc}|jb) \) kernel response; warns at run time
- Hessians / frequencies — not implemented; the mpqc4 libint2 export lacks second-derivative integrals
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 exploits 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 deliberate defect is injected and the test confirmed to fail before the guard is trusted. This has caught guards that passed while proving nothing.
API documentation
The crate-level API docs are generated by rustdoc and published alongside this
book.
Browse the API documentation →
Generated by rustdoc and published alongside this book.
That link is only present when the most recent CI run on main succeeded.
cargo doc does not link, but it does run build scripts, and
ferric-integrals' build script compiles a C++ shim needing libint2.hpp — so
the API docs are 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
deliberately named ferric — that is what makes Python's import ferric work
— which collides with the ferric facade crate. Excluding it costs 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 |
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
The doc comments in this codebase carry more than signatures. Where a design decision was hard-won — a convergence hazard, a memory-accounting subtlety, a parallelization that would be unsafe — the reasoning is recorded at the point of use, including cases where an apparently obvious optimization was rejected and why.
Those notes are often the most useful part of the documentation for anyone modifying the code, and they are deliberately kept next to the code rather than in prose docs that drift.
References
Dependencies
- libint2 — Obara–Saika integral engine
- pyo3 — Rust/Python interop
- ndarray — N-dimensional arrays for Rust
- ndarray-linalg — LAPACK bindings for ndarray
- libxc — exchange–correlation functionals
Methods
General
- Szabo & Ostlund, Modern Quantum Chemistry (1996)
- Pulay, Chem. Phys. Lett. 73, 393 (1980) — DIIS convergence acceleration
MP2 family
- Weigend, Phys. Chem. Chem. Phys. 4, 4285 (2002) — RI-MP2 auxiliary basis sets
- Grimme, J. Chem. Phys. 118, 9095 (2003) — SCS-MP2
- Bozkaya & Sherrill, J. Chem. Phys. 135, 104103 (2011) — orbital-optimized MP2
- Goldey & Head-Gordon, J. Phys. Chem. Lett. 3, 3592 (2012) — attenuated MP2
- Goldey, Dutoi & Head-Gordon, Phys. Chem. Chem. Phys. 15, 15869 (2013) — SCS-MP2(2terfc)
Coupled cluster
- Scuseria, Janssen & Schaefer, J. Chem. Phys. 89, 7382 (1988) — CCSD
- Raghavachari et al., Chem. Phys. Lett. 157, 479 (1989) — CCSD(T) triples
- Bartlett & Musiał, Rev. Mod. Phys. 79, 291 (2007) — coupled-cluster theory
Screening and scaling
- Ochsenfeld, White & Head-Gordon, J. Chem. Phys. 109, 1663 (1998) — LinK exchange
- Maurer, Lambrecht & Ochsenfeld, J. Chem. Phys. 136, 144107 (2012) — QQR screening
Constrained DFT and nonlocal correlation
- Wu & Van Voorhis, J. Chem. Phys. 125, 164105 (2006) — cDFT electron-transfer coupling \( H_{ab} \)
- Vydrov & Van Voorhis, J. Chem. Phys. 133, 244103 (2010) — VV10 nonlocal correlation
License
Dual-licensed under either
- Apache License, Version 2.0 (LICENSE-APACHE)
- MIT License (LICENSE-MIT)
at your option.