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.