# Installation & the build (`make setup`)

qc-rs is a **Rust** numerical core wrapped in a **Python** package (`qc`). Today you **build it from
source** — there is no `pip install qc` yet — so "installing qc-rs" means: install a few standard tools,
compile the `qc_rs` extension into a Python environment, and check that `import qc` works. This chapter is
**self-contained**: every command you need is here, with an explanation of *why*. It is longer than a bare
recipe on purpose — read the part that matches your machine and skip the rest.

Two situations:

1. **qc-rs is already installed for you** (common on a shared cluster) — see §1 below and just verify.
2. **You are building it yourself** — work through §2.

At the very end, §3 shows the `make setup` / `make install` shortcut that automates the whole
machine-specific build once you understand the pieces.

## 1. Is it already installed?

On a shared machine, an admin may have already built qc-rs into a Python environment. Activate that
environment (ask which one), then run:

```bash
python -c "import qc; print('qc-rs ok:', qc.__file__)"
```

If that prints a path with no error, you are done — go to the [Quickstart](quickstart.md). If it raises
`ModuleNotFoundError: No module named 'qc'` (or `'qc_rs'`), build it yourself below.

## 2. Building it yourself

The build needs, in order: **git** (to get the code), a **Rust** toolchain (to compile the core),
**uv** with a Python (the environment), a **BLAS/LAPACK** library (the dense linear-algebra backend), and
then the **maturin** build itself. Optional pieces — **MPI** and a **CUDA GPU** — come at the end.

:::{tip} A Python note
A **virtual environment** ("venv") is a self-contained Python installation with its own packages, so
different projects never clash. qc-rs uses the **uv** tool to create and manage one. You *activate* a venv
to make its `python` the one your shell uses.
:::

### 2.1 Get the source

Clone the repository over HTTPS (no SSH key needed) or with the GitHub CLI:

```bash
git clone https://github.com/qclo/qc_rs.git      # A) HTTPS
gh   repo  clone qclo/qc_rs                        # B) GitHub CLI (reuses your `gh auth login`)
cd qc_rs
```

### 2.2 Install Rust (via `rustup`)

qc-rs compiles a Rust extension, so you need the Rust toolchain — installed through **`rustup`**, *not* a
distribution's `rustc`:

```bash
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
. "$HOME/.cargo/env"          # put ~/.cargo/bin on PATH for this shell
which rustc                    # should print a path under ~/.cargo/bin
```

The `-y` runs non-interactively and is **required on machines that already have a system
`/usr/bin/rustc`** (otherwise `rustup-init` aborts with *"cannot install while Rust is installed"*).
`rustup` puts `~/.cargo/bin` ahead of `/usr/bin`, so `cargo`/`rustc` resolve to the rustup toolchain, and
the distro `rustc` is ignored. Every new shell must source `~/.cargo/env` (or have it in your shell rc).

### 2.3 Install uv and create the project venv

qc-rs builds into a **shared external uv project** — a directory that holds one library set — pointed to by
the `UV_PROJECT` environment variable, *not* a `./.venv` inside the repo. Set it up once per machine:

```bash
# Install uv (once per machine)
curl -LsSf https://astral.sh/uv/install.sh | sh

# Pick a directory for the shared project and point uv at it
export UV_PROJECT=~/local/myproj
mkdir -p "$UV_PROJECT"

# A base project file: the common scientific-Python stack qc-rs expects
cat << 'EOF' > "$UV_PROJECT/pyproject.toml"
[project]
name = "myproj"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = [ "numpy>=2.4", "scipy>=1.16", "pandas>=2.3",
                 "matplotlib>=3.10", "scikit-learn>=1.8" ]
EOF

uv python install 3.14        # fetch a Python interpreter (once per machine)
uv python pin 3.14            # use it for this project
uv sync                       # create the venv and install the base dependencies
source "${UV_PROJECT}/.venv/bin/activate"    # activate the project venv
```

Because `UV_PROJECT` is set, `uv sync` / `uv add` / `uv python pin` all act on that project without a
`--project` flag.

### 2.4 Add qc-rs's Python dependencies

Add the packages qc-rs itself needs. **Always use `uv add`, never `uv pip install`** (numpy/matplotlib
already come from the base project):

```bash
uv add --dev maturin   --project "$UV_PROJECT"   # builds the qc_rs extension (the key one)
uv add --dev pytest    --project "$UV_PROJECT"   # the test suite
uv add mpi4py          --project "$UV_PROJECT"   # MPI from Python (for the python-mpi-direct feature)
uv add h5py            --project "$UV_PROJECT"   # read HDF5 (.qch5) checkpoints
uv add geometric       --project "$UV_PROJECT"   # geometry-optimization driver (mychk.scf(...).opt())
uv add --dev ipykernel --project "$UV_PROJECT"   # optional: run notebooks in VS Code
uv add --dev patchelf  --project "$UV_PROJECT"   # Linux only: lets maturin bake the library rpath
```

That is the **required** set. The packages below are **recommended** — install them too unless you have a
specific reason not to. Together they make the shared environment complete: visualization, reference
cross-checks, document tooling, and the toolchain to build this manual. (The base scientific stack — numpy,
scipy, pandas, matplotlib, scikit-learn — already came from the base `pyproject.toml` in §2.3, so it needs
no separate `uv add`.)

**Visualization** — plots and in-notebook 3-D molecules/orbitals:

```bash
uv add plotly ipywidgets kaleido nbformat --project "$UV_PROJECT"  # figures/galleries/HTML (plotly), interactive panels (ipywidgets), static-PNG export (kaleido), inline notebook rendering (nbformat); also the qc.view 2-D/general-3-D backend
uv add py3dmol                            --project "$UV_PROJECT"  # in-cell isosurface / molecular-orbital rendering (qc.view 3-D viewer; self-contained HTML)
uv add anywidget                          --project "$UV_PROJECT"  # the interactive modeler (mychk.modeler(): pick / measure / emit an input)
```

**Reference & document tooling** — cross-checks against PySCF, reading papers, exporting results:

```bash
uv add --dev pyscf                 --project "$UV_PROJECT"  # reference values for tests / benchmarks
uv add --dev pyscf-dispersion      --project "$UV_PROJECT"  # DFT-D3/D4 in PySCF, to cross-check qc-rs's dispersion
uv add --dev pymupdf pymupdf4llm   --project "$UV_PROJECT"  # read PDFs / papers (-> Markdown)
uv add --dev python-pptx           --project "$UV_PROJECT"  # generate .pptx slide decks
uv add --dev openpyxl pdfplumber   --project "$UV_PROJECT"  # Excel I/O / PDF-table extraction
uv add --dev pillow                --project "$UV_PROJECT"  # image handling
```

**Documentation toolchain** — to build *this* manual (HTML + PDF) with Jupyter Book v1:

```bash
uv add --dev "jupyter-book<2" sphinx-design sphinx-proof sphinx-exercise sphinxcontrib-mermaid pyppeteer \
  --project "$UV_PROJECT"
# jupyter-book<2 = Jupyter Book v1 (Sphinx-based; pinned <2 to avoid v2/mystmd); sphinx-proof/sphinx-exercise
# = the textbook definition/theorem/exercise environments; sphinxcontrib-mermaid = diagrams;
# pyppeteer = HTML->PDF (CJK-friendly). Details in docs/user/README.md.
```

:::{tip} Add everything first, then build
Every `uv add` **prunes a previously-built `qc_rs`** (see the warning at the end of this chapter). So do
*all* your `uv add` operations first, and build — or rebuild with `make install` — last.
:::

### 2.5 Install a BLAS/LAPACK backend

**BLAS** (Basic Linear Algebra Subprograms) and **LAPACK** are the libraries that do the heavy
matrix arithmetic — matrix multiplies and eigenvalue problems — at the core of every SCF. qc-rs supports
two backends; pick the one for your platform (you meet them again, in depth, in
[Part IV](../30-hpc/threads-and-blas.md)):

- **OpenBLAS** — the open-source default (macOS, and Linux machines that ship it);
- **Intel MKL** — Intel's highly-tuned library (common on HPC clusters, via oneAPI).

Install steps differ by OS; they are folded into the two build flows below.

### 2.6 Build the extension — the two common flows

The build command is **`maturin develop`**, which compiles `qc_rs` and installs it into the active venv.
Two things matter every time: **set `VIRTUAL_ENV`** (or maturin may install into a stray `./.venv`), and
select the right **`--features`** for your backend (next section).

#### Linux (e.g. an HPC cluster) with Intel MKL

Many clusters have no system OpenBLAS and use **Intel oneAPI MKL**. Source the oneAPI environments first
(MKL sets `MKLROOT`; MPI puts `mpicc` on `PATH`, needed only for `python-mpi-direct`):

```bash
source ~/.cargo/env
export UV_PROJECT=~/local/myproj
source "${UV_PROJECT}/.venv/bin/activate"
export VIRTUAL_ENV="${UV_PROJECT}/.venv"

# Intel oneAPI: MKL (sets MKLROOT) and MPI (puts mpicc on PATH) — adjust paths to your install
. /opt/intel/oneapi/mkl/latest/env/vars.sh intel64
. /opt/intel/oneapi/mpi/latest/env/vars.sh          # only for python-mpi-direct

export MKL_THREADING_LAYER=GNU                        # see the MKL notes below

"${UV_PROJECT}/.venv/bin/maturin" develop --no-default-features \
  --features intel-mkl-system,xc-bundled,pcm,python-mpi-direct
"${UV_PROJECT}/.venv/bin/pytest" tests/               # sanity check
```

:::{important} MKL gotchas (why these exact settings)
- `intel-mkl-system` links MKL's **single dynamic library `mkl_rt`** (located via `MKLROOT`), not the
  layered `mkl_*` libs, which break under the linker's default `--as-needed`.
- Keep `MKL_INTERFACE_LAYER` at its default **LP64** (32-bit integers). qc-rs's BLAS/LAPACK bindings pass
  32-bit integers, so **ILP64 silently corrupts them** — never set ILP64.
- `MKL_THREADING_LAYER` defaults to *Intel*, which needs `libiomp5` that the MKL-only `vars.sh` does not
  expose. Set **`GNU`** (uses the system `libgomp`) or `SEQUENTIAL`, or the extension fails to load.
- Without **`patchelf`** (§2.4), maturin cannot bake the library paths (rpath), so you must keep the oneAPI
  lib dirs on `LD_LIBRARY_PATH` at run time — the two `vars.sh` above already do that.
:::

#### macOS with Homebrew OpenBLAS

Install the toolchain with [Homebrew](https://brew.sh) (once per machine):

```bash
xcode-select --install                              # Command Line Tools (clang, ld) — skip if present
brew install gcc openblas libomp cmake pkg-config
brew install open-mpi                                # optional: for mpi4py / python-mpi-direct
```

Each package earns its place: **gcc** provides `gfortran`/`libgfortran` (OpenBLAS links against it);
**openblas** is the BLAS/LAPACK; **libomp** is the OpenMP runtime the extension needs to *import*; **cmake**
builds the bundled libxc; **pkg-config** lets the build locate libraries. Then build with the default
`openblas-system` feature:

```bash
export UV_PROJECT=~/local/myproj
source "${UV_PROJECT}/.venv/bin/activate"
export VIRTUAL_ENV="${UV_PROJECT}/.venv"
export OPENBLAS_PATH="$(brew --prefix openblas)"
export LIBRARY_PATH="$(brew --prefix gcc)/lib/gcc/current"
export RUSTFLAGS="-L $(brew --prefix libomp)/lib -l dylib=omp -L $(brew --prefix gcc)/lib/gcc/current -l dylib=gfortran"
export DYLD_FALLBACK_LIBRARY_PATH="$(brew --prefix libomp)/lib:${OPENBLAS_PATH}/lib:${LIBRARY_PATH}:${DYLD_FALLBACK_LIBRARY_PATH:-}"

"${UV_PROJECT}/.venv/bin/maturin" develop
"${UV_PROJECT}/.venv/bin/pytest" tests/
```

The `RUSTFLAGS`/`DYLD_FALLBACK_LIBRARY_PATH` lines link and locate `libomp` and `libgfortran`; without them
`import qc` fails with *"symbol not found … `__gfortran_…`"* or a missing OpenMP runtime.

### 2.7 Feature flags (`--features`)

`maturin develop --features …` selects optional pieces at build time. The **default build** uses
`openblas-system, python-mpi-direct, xc-bundled, pcm, hdf5`. The ones you will actually toggle:

| Feature | What it gives you |
|---|---|
| `openblas-system` | Homebrew/system OpenBLAS (macOS default) |
| `openblas-bundled` | OpenBLAS built statically from source (no system OpenBLAS needed) |
| `intel-mkl-system` | Intel MKL via `mkl_rt` (Linux/HPC; needs `MKLROOT`) |
| `xc-bundled` / `xc-system` | libxc (DFT functionals) from the bundled source / a system package |
| `pcm` | implicit-solvation (PCM) module |
| `python-mpi-direct` | Rust MPI functions callable from mpi4py-initialized Python |
| `hdf5` | HDF5 (`.qch5`) checkpoint I/O |
| `cuda` | the optional NVIDIA-GPU integral/Fock path (off by default; see §5) |

`--no-default-features` turns everything off so your `--features` list is exact (used in the MKL flow
above). After building, the Python **sentinels** report what was compiled in:

```python
import qc
qc.XC_ENABLED, qc.PCM_ENABLED, qc.MPI_DIRECT_ENABLED, qc.GPU_ENABLED
```

## 3. The easy way: `make setup` + `make install`

Section 2 walked through the build by hand so you understand every piece. In practice you rarely type all
of that again: qc-rs keeps the **machine-specific** settings out of the shared code base and regenerates
them from a **profile**. There are two ways to drive it, and they produce the *same* generated files:

- **§3.1 — let an AI coding assistant do it (recommended on a new machine).**
- **§3.2 — run `make setup` / `make install` yourself.**

### 3.1 The AI-assisted way (recommended)

Getting the build right on a fresh machine — the right BLAS backend, the right `RUSTFLAGS`, the exact
`--features`, the profile — is exactly the kind of environment-specific chore an **AI coding assistant**
handles well. This is not a gimmick: it is the *same* AI-first ("vibe coding") workflow
[qc-rs itself is built with](what-is-qc-rs.md), applied to your own setup. The next chapter, [AI coding assistants](ai-coding-clis.md), shows how to install
**Claude Code** or **Codex**; once you have one, prefer this path.

:::{important} Prerequisites the assistant does **not** install for you
The assistant *configures and builds* qc-rs — it does not install system toolchains. You must have already
done §2.1–§2.5 yourself: **git**, **uv + a Python**, **Rust** (`rustup`), a **BLAS/LAPACK** library, and —
only if you want parallel runs — **MPI**. Assume those five are in place before you start; the assistant
takes over from there.
:::

With the prerequisites installed, open the assistant **in the repository directory** and ask it, in plain
language — for example:

> Build qc-rs with `make setup` and `make install`, and save the build configuration to a profile for this
> machine.

It will inspect your environment (which OS, which Python/venv, MKL vs OpenBLAS, whether `nvcc` and MPI are
present), write a matching **profile** under `mytools/setup/profiles/`, run `make setup` to generate the
machine-specific files, run `make install` to compile the extension, and — this is the real payoff — **read
any build errors and fix the flags itself**, iterating until `import qc` works in your venv. You describe
the goal; it handles the machine-specific details. The rest of this section explains what it (or you) are
doing, so you can check its work or reproduce it by hand.

### 3.2 Doing it yourself

If you would rather run it directly (or you do not have an assistant set up yet), the same profile machinery
is one command:

```bash
make setup
```

What `make setup` does, in detail:

- It reads a **profile** — a small JSON file under `mytools/setup/profiles/` (e.g. `local.json`,
  `yanai-linux-mkl.json`, `yanai-mac-openblas.json`) that records *your* Python interpreter, BLAS backend,
  MPI setup, the `RUSTFLAGS`/oneAPI environment, and the exact Cargo `--features` for your machine — i.e.
  everything you chose by hand in §2.6–§2.7.
- From it, it **generates git-ignored, machine-specific files**: your editor config (`.vscode/settings.json`,
  `tasks.json`, `launch.json`, `my.env`), a shell env file (`mytools/setup/local/env.sh`), and an
  **install helper** (`mytools/setup/local/install-qc.sh`) that runs the right `maturin develop` for you.
- It **auto-detects and remembers** your profile (in `mytools/setup/local/state.json`), so later
  `make setup` needs no arguments.

(install-profile)=

### What a profile looks like

A profile is a small JSON file — it is worth understanding, because it *is* everything you did by hand in
§2.6–§2.7, captured in one place. Here is the shape of a Linux + MKL profile, annotated:

```json
{
  "os": "linux",                     // "linux" or "mac"
  "backend": "mkl",                  // "mkl" or "openblas" — picks the BLAS features
  "python": "/home/you/local/myproj/.venv/bin/python",   // the venv interpreter (→ VS Code)
  "vscode": {
    "activate_env": true,            // auto-activate the venv in VS Code terminals
    "rust_analyzer": {               // how the Rust language server should compile the core
      "no_default_features": true,
      "features": ["intel-mkl-system", "xc-bundled", "pcm"],
      "extra_env": { "MKLROOT": ".../mkl/2025.2", "MKL_THREADING_LAYER": "GNU" }
    }
  },
  "my_env": {                        // runtime env vars -> .vscode/my.env
    "MKL_THREADING_LAYER": "GNU",
    "MKL_INTERFACE_LAYER": "LP64",
    "LD_LIBRARY_PATH": ".../mpi/2021.16/lib"
  },
  "build": {
    "task_label": "maturin develop (MKL, myproj)",   // the VS Code build-task name
    "shell": "/bin/bash",
    "pre": [                         // sourced before the build (also becomes env.sh)
      "source ~/.cargo/env",
      "export UV_PROJECT=~/local/myproj",
      "source \"${UV_PROJECT}/.venv/bin/activate\"",
      "export VIRTUAL_ENV=\"${UV_PROJECT}/.venv\"",
      ". .../mkl/2025.2/env/vars.sh intel64",
      ". .../mpi/latest/env/vars.sh"
    ],
    "rustflags": "-C link-arg=-Wl,-rpath,.../mkl/lib:.../mpi/lib",   // bake the library rpath
    "maturin_args": "--no-default-features --features intel-mkl-system,xc-bundled,pcm,python-mpi-direct,hdf5"
  }
}
```

Field by field:

- **`os`, `backend`** — your platform and BLAS choice (`mkl` / `openblas`); together they set the default
  `--features`.
- **`python`** — the absolute path to your venv's interpreter. VS Code uses it as the interpreter, and the
  build derives `VIRTUAL_ENV` from it.
- **`vscode.activate_env`** — auto-activate the venv in VS Code's integrated terminals.
- **`vscode.rust_analyzer`** — how the Rust language server compiles the core so its analysis matches your
  build (`no_default_features`, the `features` list, and any `extra_env` such as `MKLROOT`). This affects
  only the Rust-side editor IntelliSense, not the Python build.
- **`my_env`** — runtime environment variables written verbatim to `.vscode/my.env` and used when you run or
  debug from the editor: here the MKL threading/interface layers and the MPI `LD_LIBRARY_PATH`.
- **`build.pre`** — the shell lines sourced *before* the build (activate the venv; source `~/.cargo/env` and
  the oneAPI `vars.sh`). These same lines make `mytools/setup/local/env.sh` a one-shot dev shell —
  `source mytools/setup/local/env.sh` and Rust + uv + venv + MKL are all ready.
- **`build.rustflags`** — extra linker flags, typically the `-rpath` that bakes the library search paths into
  the extension so it imports with no `LD_LIBRARY_PATH`.
- **`build.maturin_args`** — the exact `maturin develop` feature string for this machine.
- **`build.task_label`** — the name of the generated VS Code build task (you will see it under *Tasks: Run
  Build Task*).

On a new machine, `make setup` **auto-detects** the per-node absolute paths and writes a git-ignored
`profiles/local.json` that overrides the reference profile — so in practice you run `make setup` and only
edit `local.json` if a detected path is wrong.

Variants:

```bash
make setup --list                 # list the available profiles
make setup PROFILE=<name>         # pick a specific profile
make setup FORCE=1                # overwrite existing generated files (no .bak backup)
```

Then build qc-rs into your venv with the generated helper:

```bash
make install
```

`make install` runs `install-qc.sh` — a `maturin develop` with your machine's features and the library
rpath baked in, so `import qc` works with no extra environment variables. Run it **after** `make setup`.

:::{note} Everything `make setup` writes is git-ignored
It is *your* machine config and is never committed. A brand-new machine may need its own profile — copy the
closest file under `mytools/setup/profiles/` and edit the interpreter/BLAS/MPI fields.
:::

## 4. Verify

```bash
python -c "import qc; print('qc-rs ok')"
```

Then run the [Quickstart](quickstart.md): if you get `-76.026772`, everything works end to end.

## The one gotcha: `uv add` prunes qc-rs

:::{warning}
`uv add`, `uv remove`, and `uv sync` reconcile the venv to the project's lockfile, which **uninstalls** the
maturin-built `qc_rs` extension (it is not declared in `pyproject.toml`). If `import qc` suddenly fails
after you add a package — e.g. `ModuleNotFoundError: No module named 'qc_rs'` or
`ImportError: libmkl_rt.so.2: cannot open shared object file` — just rebuild:

```bash
make install         # or re-run your maturin develop command
```

Do the `uv` operation first and the rebuild last. To avoid the prune, manage extras with `uv pip install` /
`uv pip uninstall` (they don't touch the lockfile) or pass `uv sync --inexact`.
:::

## 5. Optional: MPI and GPU

- **MPI** (running across many machines). Install an MPI implementation (`brew install open-mpi`, or your
  cluster's module) and `mpi4py` (§2.4), and build with the **`python-mpi-direct`** feature (it is in the
  default set). Actually *running* over a fast interconnect (InfiniBand) has its own subtleties — that is a
  whole topic, taught from zero in [Part IV → MPI & interconnects](../30-hpc/mpi-and-interconnects.md).
- **GPU** (NVIDIA CUDA). The default build is **CUDA-free and runs anywhere**. To opt in you need an NVIDIA
  GPU (compute capability ≥ 7.0), the CUDA Toolkit (`nvcc`, `cudart`, cuBLAS), and CMake ≥ 3.19, then add
  the **`cuda`** feature to your `maturin develop`. Details are in [Part IV → GPU computing](../30-hpc/gpu-cuda.md).

## Troubleshooting

A few common first-build errors:

- **`symbol not found … __gfortran_…` / OpenMP missing (macOS)** — the `RUSTFLAGS` /
  `DYLD_FALLBACK_LIBRARY_PATH` lines in §2.6 are missing or wrong (they link `libgfortran`/`libomp`).
- **`libmkl_rt.so.2: cannot open shared object file` (Linux)** — you did not source the MKL `vars.sh` in
  this shell, or `uv add` pruned the extension (rebuild with `make install`).
- **Wrong numbers / silent corruption at large sizes (MKL)** — you set `MKL_INTERFACE_LAYER=ILP64`; use the
  default **LP64**.
- **`xc-system` cannot find `libxc.pc`** — use the bundled build instead: `--features xc-bundled`.
- **`ModuleNotFoundError: qc_rs` right after a `uv` command** — the prune above; run `make install`.

More is in [Troubleshooting & FAQ](../40-reference/troubleshooting.md). With qc-rs installed, head to the
[Quickstart](quickstart.md), or set up your editor next in [VSCode](editor-vscode.md).
