Installation

There are two ways in. Most people want the first.

You want toDo thisTime
Run calculationsInstall the prebuilt wheelabout a minute
Change ferric's Rust code, or use MPIBuild from sourceabout 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:

PlatformWheelSource build
Linux x86_64, CPython 3.10–3.13yesyes (the only tested platform)
Linux aarch64nonot tested
macOS (x86_64, arm64)not yet (wanted; no wheel is built)not tested
Windowsnonot 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 ferric command on PATH that runs TOML input files (the same CLI as cargo 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.0rc8 then reports "already satisfied" and keeps the large wheel.
  • pip install -U ferric at 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-links index, 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.sh downloads it (checksum pinned) and needs curl, python3, zstd and patchelf
  • 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 catches debug_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.tgz from the libint-2.7.2-ferric-small-1 release (verify its sha256 against LIBINT_SHA256 in .github/workflows/wheels.yml). Configure it with cmake .. -DCMAKE_INSTALL_PREFIX=$PREFIX -DCMAKE_PREFIX_PATH=$PREFIX -DCMAKE_POSITION_INDEPENDENT_CODE=ON -DCMAKE_BUILD_TYPE=Release, then make and make install, and set LIBINT2_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):

IntegralsSource build with scripts/install-libint.sh (conda-forge 2.13.1)PyPI wheel (ferric's libint2 2.7.2 export)Needed for
4-centre ERI7 / 6 / 36 / 6 / 3SCF, gradients, analytic Hessians
One-electron (overlap, kinetic, nuclear)7 / 6 / 36 / 6 / 6the same
3- and 2-centre ERI (auxiliary shell)7 / 7 / 46 / 6 / –RI-MP2, RPA, GW and their gradients
G12 geminal4 / – / ––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 and dirty: False. To release, push a vX.Y.Z tag; 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 .devN version (0.1.0.dev0) and the commit it was built from, with dirty: True if tracked files had uncommitted changes. Untracked files do not count.
  • A build with no git metadata (an sdist or source tarball) reports commit: "unknown", and ferric.__build__ is None.

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_ov resident on the card: f64 by default ([precision] mode = "f64"), or with [precision] mode = "mixed" B_ov stored 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_ov per spin: f64 by default, or with precision = "mixed" each spin's B_ov stored 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): auto keeps 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). With mode = "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 potential V = A·χᵀ) on the device in f64; naming xc-vxc in mixed_kernels (with precision = "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.