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.