All posts

October 5, 2026 · Temple Compute

Docking into Cryptic Pockets with Lacuna and AutoDock Vina

Docking starts with a box. You pick a pocket on the receptor, draw a search volume around it, and let AutoDock Vina place ligands inside. That works when the pocket is already there. A cryptic pocket is not. It is closed or shallow in the unbound (apo) structure and only opens when something binds.

If you dock into the apo structure, you dock into the pockets the apo structure shows you. A cryptic site never gets a box, so it never gets a ligand. This post walks through an open-source workflow that finds cryptic pockets first and docks second: Lacuna + AutoDock Vina Docking.

Why apo-structure docking misses cryptic sites

Most pocket finders read the surface as it is. fpocket, for example, only proposes a pocket where the surface is already concave. That is exactly the assumption a cryptic site violates. Run it on one crystal structure and the site is invisible. Run it on a pre-built ensemble and it can only see what the ensemble happens to contain.

KRAS is the standard example. The switch-II pocket that covalent G12C inhibitors such as sotorasib bind is absent from apo KRAS structures. It forms on inhibitor binding. A docking run boxed on the apo surface has no reason to look there.

So the question is not how to dock better into a known pocket. It is how to find the pocket before you draw the box. For more on the detection side, see cryptic pocket detection.

The pipeline

The workflow has six stages. Each one is a single command or script.

1. Lacuna finds and ranks pockets

Lacuna (lacuna-pockets on PyPI) takes the apo structure and generates a conformational ensemble from it. The workflow asks for 20 conformers with the default normal-mode backend. Lacuna detects pockets in every conformer, then clusters them across the ensemble. A site that appears in only a few frames still gets reported.

Detection uses --detector surface-fusion. This pools the original geometric detector with a learned surface detector. On held-out CryptoBench data, pooling both takes coverage from 68.5% to 86.4% and top-five recovery from 57.1% to 73.9% over the geometric detector alone.

A fitted model ranks the surviving clusters. Each pocket in pocket_report.json carries a rank, druggability, persistence (the fraction of conformers it appears in), crypticity (how much it opens relative to the input), and its contact residues. Conformer 0 is always the unperturbed input, so a pocket that never leaves the crystal structure is still reported.

On the bundled example, PDB 4LDJ (GDP-bound apo KRAS(G12C)), the sites ranked above the switch-II pocket are the GDP pocket, a real pocket with a ligand already bound. Lacuna still surfaces the switch-II pocket twice in the top ten. One hit is the compact switch-II groove. The other is the wider pocket with 11 of 21 contact residues and nine-fold opening across the ensemble. KRAS is not in the benchmark Lacuna's ranker was fitted on.

2. Export docking inputs

lacuna dock-prep turns the top 5 ranked pockets into AutoDock Vina box configs (pocket_N_vina.conf), Boltz-2 YAML constraints, and pocket PDBs. It is a separate task from discovery, so you can change --top or --format without rerunning ensemble generation.

3. Pick the pocket

The prep stage reads one box config through --box-config. The default is pocket_0_vina.conf, Lacuna's rank-1 site. On 4LDJ that is the GDP pocket. To dock the switch-II cryptic pocket instead, swap in pocket_3_vina.conf. Lacuna supplies the box center and size. The receptor that Vina docks into is the input structure.

4. Meeko and OpenBabel prep

prep.py keeps only the ATOM, TER, and END records of the receptor. This drops the RCSB header lines that Vina's parser rejects. It also removes the bound GDP and waters, so a new ligand is not scored against an occupied pocket. OpenBabel then writes a rigid receptor.pdbqt with hydrogens at pH 7.4.

For a SMILES library, RDKit adds hydrogens, embeds each molecule in 3D with ETKDGv3, and optimizes it with MMFF. Meeko writes one PDBQT per ligand. An SDF library skips the RDKit step. The receptor and box.json go in one folder, the ligand PDBQTs in another.

5. Vina docks each ligand

dock_one.py runs once per ligand. It loads the receptor, computes Vina maps over the box, docks with exhaustiveness 8, and writes 9 poses. It records the best affinity in status.json. Any exception is caught and recorded as status: failed, so one bad ligand does not stop the batch.

6. Ranked summary

summary.py reads every slot, skips failed ligands, and parses the REMARK VINA RESULT lines. It writes scores.csv, one row per ligand ranked by best affinity, and poses.csv, one row per pose with RMSD bounds. A separate report stage renders the pocket ranking as an HTML page with a Mol* view.

On the three-compound demo set docked into the rank-1 GDP pocket, imatinib ranks best, then caffeine, then aspirin. None of the three were chosen for this pocket. Treat it as a mechanics check, not a hit.

How Horus splits the stages

The two halves of this pipeline need different software. Lacuna is a plain pip package, so discover and dock_prep run in a uv-managed venv. OpenBabel, Meeko, RDKit, and Vina come from conda-forge, so prep and dock run in conda environments. The two stacks never share a solver.

The dock stage is a horus_map task. Horus runs one clone per ligand, concurrently, and gives each clone a copy of the shared receptor and box. Docking time scales with concurrency, not with the ligand count. A 100-ligand library is 100 independent Vina runs.

The default run is CPU-only end to end. Hardware routing is per stage. Give dock a cluster target: and every clone inherits it. Give discover its own target: to scale ensemble generation. Lacuna's --backend boltz (diffusion sampling) needs a GPU. The workflow does not wire it up by default. Adding it means a GPU-capable target: on discover only. The docking stages stay where they are, and the commands do not change.

FAQ

Can AutoDock Vina dock into a cryptic site?

Yes, once the site has a box. Vina needs a center and size. Lacuna exports both for each ranked pocket as a pocket_N_vina.conf, and the prep stage reads one of them directly.

Is this ensemble docking?

Not in the dock-into-every-conformer sense. The ensemble is used to find and rank pockets. Vina then docks into the input structure, boxed on the chosen pocket. It also exports Boltz-2 constraints for the top 5 pockets.

How do I dock into a pocket other than the top-ranked one?

Change --box-config in the prep stage. On 4LDJ, pocket_3_vina.conf is the switch-II cryptic pocket. Nothing else in the workflow changes.

Do I need a GPU for cryptic site docking?

No. The default backend, both detectors, and Vina all run on a CPU. A GPU only enters if you switch Lacuna to the Boltz backend.

Run it

The workflow is open source and runs with uv run horus run workflow.yaml. Read the library page for inputs, outputs, and parameters. Use Lacuna Cryptic Pocket Discovery if you only need the pocket report. Use AutoDock Vina Docking if you already know where the pocket is.

To run it with each stage on the hardware you pick, open Horus.