Metadata-Version: 2.4
Name: rustina
Version: 0.9.0
Classifier: Development Status :: 5 - Production/Stable
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: POSIX :: Linux
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Rust
Classifier: Topic :: Scientific/Engineering :: Bio-Informatics
Requires-Dist: rdkit>=2024.3 ; extra == 'prep'
Requires-Dist: molscrub>=0.2.2 ; extra == 'prep'
Requires-Dist: meeko>=0.7.1,<0.8 ; python_full_version < '3.10' and extra == 'prep'
Requires-Dist: meeko>=0.7.1 ; python_full_version >= '3.10' and extra == 'prep'
Requires-Dist: scipy>=1.12.0 ; extra == 'prep'
Requires-Dist: gemmi>=0.6.0 ; extra == 'prep'
Requires-Dist: joblib ; extra == 'prep'
Requires-Dist: pandas ; extra == 'prep'
Provides-Extra: prep
License-File: LICENSE
Summary: CPU/GPU molecular docking with native GNINA-compatible CNN rescoring
Author: Adrien H. Cerdan
Requires-Python: >=3.9
Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
Project-URL: Homepage, https://github.com/AdrienCerdan/rustina
Project-URL: Issues, https://github.com/AdrienCerdan/rustina/issues
Project-URL: Repository, https://github.com/AdrienCerdan/rustina

# Rustina

Linux x86-64 molecular docking in Rust, with a command-line interface and Python
bindings. Rustina combines a QuickVina2-style Monte Carlo search on Vinardo grids
with native inference of GNINA's fast CNN, fused with the empirical energy to
pick the final pose. It needs no GPU, no PyTorch and no network access at run time.

| Component | Default | Alternatives |
| --- | --- | --- |
| Search | `qvina2-active`: staged QuickVina2 runs (16), no moves on torsions the scoring function cannot see | `qvina2-race`, `qvina2`, `vina` (CPU), `gpu` (wgpu batched Monte Carlo) |
| Scoring | `vinardo` | `vina`, `ad4` (charge-aware AutoDock 4.2) |
| Pose ranking | GNINA `fast` CNN on the 8 best candidates, fused with the energy: logit(p) − 0.25 (E − median E) / max(IQR E, 1 kcal/mol) | `--skip-cnn` (energy only) |
| Receptor waters | kept as supplied | `--waters strip`, `--waters toggle` |
| Affinity | CNN pK of the returned pose | optional ensemble of GNINA heads (`--cnn-affinity-models`) |

## Benchmarks

Pose prediction with the default setup: crystal-ligand box + 8 Å per axis (minimum
18 Å), receptors from `rustina.prepare_receptor`, ligands from the supplied start
conformer via `rustina.prepare_ligand(skip_gen3d=True)`, seed 1. Success is a top-1
pose within 2 Å RMSD of the crystal ligand, evaluated with PoseBusters 0.6.5
(`redock`, all crystal instances); "valid" also requires its physical checks.
Failures, including preparation failures, count as misses. Published rows use the
same inputs and the same RMSD criterion.

| Set | Rustina | Rustina, valid | GNINA 1.3 | Glide SP | GOLD | AutoDock Vina | DiffDock |
| --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: |
| Astex Diverse (85) | **76.5%** | 74.1% | 81.2%¹ | 63.5%¹ | 67.1%² | 57.6%² | 71.8%² |
| PoseBusters (428) | **61.7%** | 60.3% | — | — | 51.2%² | 52.3%² | 37.9%² |
| PoseX self-docking (718) | **58.1%** | 56.5% | 64.6%¹ | 48.3%¹ | — | 40.0%¹ | — |
| PoseX cross-docking (1,312 pairs) | **47.6%** | 46.9% | 53.1%¹ | 37.5%¹ | — | 28.1%¹ | — |

¹ [PoseX](https://arxiv.org/abs/2505.01700) (ICLR 2026); cross-docking is the
per-target average. ² [PoseBusters](https://doi.org/10.1039/D3SC04185A) paper data,
no post-processing.

Median docking time is 2.1 s per complex on 4 CPU threads (Astex, Ryzen 5 5600X). GNINA's lead
on the PoseX sets comes from its slower three-model CNN ensemble and from that
benchmark's tighter box convention (crystal ligand + 4 Å per side, no minimum);
Rustina's search already returns a near-native pose among its nine outputs for
79% of the PoseX self-docking cases (567/718). With a GPU, `--search-mode gpu` matches the
CPU default's accuracy about 11× faster: median 0.17 s per complex on Astex (77.6%) and
0.18 s on PoseBusters (61.7%, as on CPU).

Virtual screening on [LIT-PCBA](https://drugdesign.unistra.fr/LIT-PCBA/), all 15
targets (1,096 actives, 14,993 sampled inactives): mean AUROC 0.597 with the CNN affinity
and EF1% 3.4 with CNN affinity × pose probability (Vinardo: 0.548 / 1.3). Published
results on the full sets are GNINA 0.609 / 4.6 and Glide SP 0.532 / 3.4
([DrugCLIP](https://arxiv.org/abs/2310.06367), Table 3). LIT-PCBA remains hard for every
structure-based score; heavy-atom count alone gives an AUROC of 0.567.

Details, protocols and per-target results are in [docs/benchmarks.md](docs/benchmarks.md).

## Build

```bash
cargo build --release --locked
python -m pip install maturin
maturin build --release --locked --out target/wheels
python -m pip install target/wheels/*.whl
```

The Python package supports Python 3.9+. Docking and scoring need no Python
dependencies. Receptor and ligand preparation use `pip install 'rustina[prep]'`
(RDKit, Meeko, molscrub, gemmi).

## Command line

Inputs are receptor and ligand PDBQT files with polar hydrogens and partial charges.

```bash
target/release/rustina dock -r data/receptor.pdbqt -l data/ligand.pdbqt \
  --cx 0 --cy -5 --cz 0 --sx 12 --sy 16 --sz 12 --seed 42 \
  --num-modes 9 -o target/docked.pdbqt

target/release/rustina score -r data/receptor.pdbqt -l data/ligand.pdbqt \
  --scoring vinardo --cnn
```

`--batch DIRECTORY` docks a directory of ligands against one receptor prepared once
and writes a `summary.csv`. `--diagnostics-json FILE` records resolved settings and
every written pose's scores. `dock --help` and `score --help` list all options.

- **Reproducibility.** A seed makes CPU output identical across thread counts. GPU
  reproducibility also depends on the adapter and driver.
- **Search budget.** CPU budgets are fixed by the mode; `qvina2-active` and
  `qvina2-race` need at least 16 runs. `--steps` and `--bfgs-iter` apply to GPU search.
- **Waters.** `--waters fixed` (default) keeps receptor waters, `strip` removes them,
  and `toggle` docks with and without them and keeps the result whose top pose has the
  higher CNN probability (single-ligand docking only, twice the cost). On PoseX
  cross-docking with ordered crystal waters the toggle gained 2.4 percentage points.
- **Inputs.** Coordinates and charges must be finite, atom serials unique and branch
  bonds valid; multi-model PDBQT uses its first model. Ligands with pseudo-water `W`
  atoms are rejected.

## Python

```python
import rustina

receptor = rustina.prepare_receptor("protein.pdb", center=(0, -5, 0), size=(20, 20, 20))
ligand = rustina.prepare_ligand("CC(=O)Nc1ccc(O)cc1")[0]

poses = rustina.dock(
    receptor["pdbqt"], ligand["pdbqt"],
    cx=0, cy=-5, cz=0, sx=20, sy=20, sz=20, seed=42,
)
print(poses[0]["affinity"], poses[0]["cnn_score"], poses[0]["cnn_affinity"])

results = rustina.screen(receptor["pdbqt"], [ligand["pdbqt"]], cx=0, cy=-5, cz=0, sx=20, sy=20, sz=20)
```

- `dock` returns ranked poses with the empirical `affinity` (kcal/mol), the CNN pose
  probability `cnn_score` and CNN affinity `cnn_affinity` (pK). `screen` shares the
  receptor grids across a batch and returns the top pose per ligand in input order;
  a failed ligand carries an `error` string. Paths and PDBQT text are both accepted.
- `prepare_receptor(protein, center, size)` crops whole residues around the box,
  removes waters (`keep_waters=True` keeps them) and crystallisation additives, and
  runs Meeko. Cofactors and ions Meeko cannot template, and observed atoms Meeko
  deletes from incomplete residues, are kept as element-typed atoms instead of
  failing or silently changing the pocket; tied histidine states, unbuildable
  residues and untemplated modified residues (e.g. selenomethionine) are retried. The returned `report` records every such change.
- `prepare_ligand(smiles_or_file)` enumerates protonation (pH 7.4) and tautomer states
  with molscrub and embeds them deterministically; `skip_gen3d=True` keeps supplied
  3D coordinates and protonation.
- `score` and `score_cnn` rescore an existing pose.

## Optional affinity ensemble

Averaging the affinity heads of GNINA's default2018-architecture models gives small,
consistent ranking gains over the fast model (congeneric series and LIT-PCBA; see the
benchmarks). The models are not bundled; convert them from a GNINA checkout:

```bash
python scripts/convert_gnina_models.py /path/to/gnina/gninasrc/lib/models --affinity-pack models/
target/release/rustina dock ... --cnn-affinity-models models/
```

Python `dock()` and `screen()` accept `cnn_affinity_models="models/"`. Results add
`cnn_affinity_ensemble` and `cnn_affinity_ensemble_std`; pose ranking is unchanged.
The cost is one fast-CNN evaluation per model and pose. See [docs/models.md](docs/models.md).

## Seed poses from deep-learning docking

Poses from a deep-learning model can join Rustina's ranking. Each seed is refined by
Rustina's direct Vinardo minimisation, CNN-scored and ranked with the search's candidates
by the CNN-energy fusion plus a fixed bonus for seeds (`--seed-bonus`, default 2 logit
units); the search still runs, so a wrong seed can lose to a better Rustina pose.

**Uni-Mol Docking V2, built in.** `--unimol-model FILE` adds the poses that
[Uni-Mol Docking V2](https://arxiv.org/abs/2405.11769) predicts for the ligand from 10
conformers. The network runs natively in Rust, on the CPU or on the GPU
(`--unimol-device gpu`, the default for `--search-mode gpu`); no PyTorch or RDKit is needed.
The weights (MIT) are not bundled: download upstream's `unimol_docking_v2_240517.pt` and
convert it once with NumPy.

```bash
python scripts/convert_unimol_model.py unimol_docking_v2_240517.pt unimol_docking_v2.bin
target/release/rustina dock -r receptor.pdbqt -l ligand.pdbqt ... \
  --unimol-model unimol_docking_v2.bin [--unimol-reference crystal_ligand.sdf]
```

Uni-Mol's pocket is the docking box or, with `--unimol-reference`, upstream's box around a
reference ligand (its extent + 10 Å), which suits the model better. Python:
`dock(..., unimol_model=..., unimol_reference=...)`; `screen()` takes the same arguments and
encodes the pocket once. Top pose within 2 Å:

| Set | Rustina | + Uni-Mol, docking-box pocket | + Uni-Mol, reference pocket |
| --- | ---: | ---: | ---: |
| Astex (85) | 76.5% | **95.3%** | 91.8% |
| Family-disjoint cohort, 2021–2025 (132) | 48.5% | 58.3% | **64.4%** |
| PoseX cross-docking (1,312 pairs) | 45.9% | 63.9% | **68.7%** |
| PoseX self-docking (717) | 58.4% | 72.0% | **78.7%** |
| PoseBusters (428; held out) | 61.7% | 75.5% | **81.3%** |

Seeds are cleared of the receptor clashes PoseBusters flags; 72.9% and 78.3% of PoseBusters
top poses are within 2 Å and pass all its checks (Rustina alone: 60.3%). Seeding adds 1.1 s
per complex with the network on the GPU (Astex median 3.2 s against 2.2 s; 1.4 s with
`--search-mode gpu`) and 4.7 s on the CPU; upstream's PyTorch pipeline took 9–10 s.
The seeding settings were chosen before any PoseBusters run, the clash relief after it.
With the reference pocket the results match those of upstream's own PyTorch pipeline used
as a seed. Uni-Mol was trained on PDBbind, which contains the Astex complexes. Details:
[docs/benchmarks.md](docs/benchmarks.md#seed-poses-from-uni-mol-docking-v2).

**Other programs.** `--seed-poses seeds.pdbqt`; Python `dock(..., seed_poses=[...])`
accepts PDBQT texts or paths, and poses refined from a seed carry `seed_index`.
`rustina.seed_pose_from_sdf(pose_sdf, ligand_pdbqt)` converts any program's SDF pose of the
ligand into a seed; `scripts/unimol_seed_poses.py` writes one from upstream's PyTorch
Uni-Mol.

## Models and licenses

The bundled `fast` CNN (GNINA 1.3.2 `all_default_to_default_1_3_1`, Apache-2.0), the
optional Uni-Mol Docking V2 weights (MIT, not bundled) and their provenance are described
in [docs/models.md](docs/models.md). Attribution is in
[THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md).

## Development

`screening.rs` shares receptor preparation, docking, refinement and pose ranking
between the CLI and Python bindings. Search lives in `optimize.rs`, `qvina.rs` and
`gpu/`; empirical potentials in `scoring.rs` and `ad4.rs`; CNN inference in `cnn.rs`.
[AGENTS.md](AGENTS.md) describes working in the repository, and
[docs/validation.md](docs/validation.md) the protocol for evaluating changes.
[docs/benchmark.md](docs/benchmark.md) documents the search and ranking diagnostics harness.

```bash
cargo fmt --all --check
cargo clippy --all-targets --locked -- -D warnings
cargo test --all-targets --locked
```

Install the release wheel, then run `python -m unittest discover -s /absolute/path/to/rustina/python/tests -v`
from outside the checkout. GPU tests exercise the available adapter; an absent adapter
prints a skip message but counts as passing, so hardware validation must confirm that
they ran on a real GPU.

Tagged releases publish one Linux x86-64 Python wheel and a CLI archive, built in
manylinux 2.28 and tested as installed packages before publishing.

Graphify is optional navigation tooling: `scripts/setup_graphify.sh` installs it and
`scripts/graphify update . --force` rebuilds the code graph under `scratch/graphify`.

