Probe-based spherical interaction-energy fields for pocket-conditioned molecule generation. PocketField ranks growth sectors from a 3D chemical-probe field around a protein binding pocket, then enumerates fragments and linkers from mapped anchor dummy atoms and scores the resulting RDKit candidates.
It is a deterministic, non-learned pipeline — an MVP of a CMB-like workflow. Fragment and linker choices are ranked by explicit coefficient-vector matching plus field/clash/solvation scoring.
receptor PDB ─┐
├─► build ──► field.npz
ligand PDB ───┘ metadata.json
growth_plan.json
│
▼
grow ──► candidates.sdf
candidates.json
sector_coefficients.json
fragment_features.json
dummy_sector_assignments.json
│
┌──────────────────┴───────────────────┐
▼ ▼
scripts/score_docked.py scripts/solvation.py
PocketField fit + OBC-GBSA OBC-GBSA / explicit TIP3P
→ ranked.sdf (one combined score) → solvation energies only
pocketfield build ──► pocketfield grow ──► docked/candidate poses
│ │ │
▼ ▼ ▼
field.npz candidates.sdf two independent downstream tools:
metadata.json candidates.json • score_docked.py (fit + solvation)
growth_plan.json sector/fragment vectors • solvation.py (solvation only)
The two scripts are alternatives, not stages: score_docked.py folds OBC-GBSA solvation into a single PocketField ranking, while solvation.py reports solvation alone (and is the one that offers explicit TIP3P water).
The field is not a raw conformal ESP map. Each value is an interaction-energy well computed at the original 3D sample point before spherical indexing, which avoids treating the 2D spherical projection as the physical object.
python3 -m pip install -e .The core field builder needs only NumPy. Candidate growth requires RDKit; solvation scoring additionally requires the OpenFF Toolkit (and OpenMM for explicit water).
# Recommended: a conda env with RDKit + OpenFF + OpenMM
conda create -n pocketfield python=3.12
conda activate pocketfield
pip install -e .
pip install rdkit openff-toolkit openmmOptional retrosynthesis validation needs rdchiral (see Filtering).
pocketfield build \
--protein receptor.pdb \
--center 12.4,3.1,-8.0 \
--out-dir out/pocket_001/fieldOr use ligand atoms to define the pocket center:
pocketfield build \
--protein receptor.pdb \
--ligand ligand.pdb \
--out-dir out/pocket_001/fieldOutputs field.npz, metadata.json, and growth_plan.json into out/pocket_001/field/.
python -m pocketfield.cli grow \
--field out/pocket_001/field/field.npz \
--plan out/pocket_001/field/growth_plan.json \
--metadata out/pocket_001/field/metadata.json \
--out-dir out/pocket_001/growThe default anchor is C([*:1])([*:2])([*:3])[*:4]. Dummy atoms mark growth sites. The generator attaches built-in probe-compatible fragments, embeds and optimizes 3D conformers with RDKit, aligns them toward ranked sectors, penalizes pocket clashes, and writes candidates.sdf, candidates.json, and the sector/fragment feature vectors.
pocketfield design \
--protein receptor.pdb \
--ligand ligand.pdb \
--out-dir out/pocket_001 \
--fragment-library examples/fragments.csvWrites out/pocket_001/field/ and out/pocket_001/grow/.
python -m pocketfield.cli inspect \
--grow-dir out/pocket_001/grow \
--top 10 \
--json-out out/pocket_001/grow/inspection.jsonRead-only (except --json-out); audits sector chemistry, dummy assignments, fragment/linker matches, desolvation penalties, and candidate score summaries.
python scripts/score_docked.py \
--field out/pocket_001/field/field.npz \
--metadata out/pocket_001/field/metadata.json \
--sdf docked_poses.sdf \
--output ranked.sdf \
--solvation obc \
--solvation-weight 0.15Computes a PocketField fit score per molecule plus an OBC-GBSA solvation free energy, then combines them into one ranking. See Scoring.
python scripts/solvation.py --sdf out/pocket_001/grow/candidates.sdfReports OBC-GBSA and (optionally) explicit TIP3P solvation energies for each molecule.
# build
--shells 2.5,3.5,4.5,6.0
--samples 2048
--sectors 64
--pocket-radius 8.0
--probes hydrophobic,hbond_donor,hbond_acceptor,positive,negative
# grow / design
--anchor-smiles 'c1cc([*:1])ccc1[*:2]'
--anchor-structure examples/anchor_dummy.sdf
--reference-ligand path/to/reference.sdf
--sectors-per-dummy 4
--dummy-angle-cutoff 80
--bridge-anchor-dummies
--linker-library examples/linkers.csv
--fragment-library examples/fragments.csv
--growth-depth 3
--request-limit 5000
--max-heavy-atoms 45
--max-clash-score 0.5
--max-field-score 0.5
--sector-match-weight 0.25
--cross-attention-weight 0.25
--linker-distance-tolerance 2.0
--linker-conformers 20| Module | Role |
|---|---|
pocketfield/cli.py |
Command-line entry point — build, grow, design, fragments, inspect |
pocketfield/atoms.py |
PDB parsing, atom typing, pocket-atom selection by radius |
pocketfield/sphere.py |
Fibonacci sphere directions, shell sampling, numeric arg parsing |
pocketfield/probes.py |
Chemical probe definitions (hydrophobic, donor, acceptor, ±charge) |
pocketfield/field.py |
Spherical interaction-energy field (energy + Cartesian gradient) |
pocketfield/sectors.py |
Sector assignment and single/pair sector ranking |
pocketfield/plan.py |
Growth plan (ranked single sectors and adjacent pairs) |
pocketfield/library.py |
Fragment/linker library definitions, loading, and validation |
pocketfield/features.py |
15-dim sector/fragment/linker vectors, match ranking, reference-occupancy screening |
pocketfield/geometry.py |
Anchor placement, exit vectors, dummy-sector assignment, reference occupancy |
pocketfield/growth.py |
Growth-request generation (sector cover, bridge linker span prefilter) |
pocketfield/assembly.py |
Bonds fragments/linkers onto the anchor through mapped dummies, 3D alignment |
pocketfield/rdkit_grow.py |
pocketfield grow orchestrator — embedding, scoring, candidate assembly |
pocketfield/scoring.py |
Candidate field/clash scoring against the pocket field |
pocketfield/attention.py |
Multi-head cross-attention fragment re-ranking (no learned params) |
pocketfield/solvation.py |
OBC-GBSA implicit-solvent free energy (pure NumPy) |
pocketfield/linker_export.py |
Linker SMILES normalization ([*:1]/[*:2]) and probe annotation |
pocketfield/merge_utils.py |
Fragment assembly + optional rdchiral retrosynthesis validation |
pocketfield/io.py |
JSON write helper |
| Script | Role |
|---|---|
scripts/solvation.py |
Solvation free energy CLI — OBC-GBSA + explicit TIP3P water |
scripts/score_docked.py |
Score docked poses — PocketField fit + solvation, combined ranking |
scripts/demo_attention.py |
Demonstrates the production cross-attention re-ranking |
scripts/export_linkers.py |
Exports linker SMILES from an external database to a PocketField CSV |
scripts/prepare_arm_fragments.py |
Converts docked ARM fragments (plain SMILES) into a [*:1]-capped PocketField fragment library |
For each sample point on several spherical shells, PocketField evaluates the interaction energy of chemical probes (hydrophobic, H-bond donor/acceptor, positive, negative — plus aromatic if requested). Energies are computed in 3D first, then indexed by sphere direction and shell radius.
field.npz contains:
directions:(samples, 3)unit vectors.points:(shells, samples, 3)Cartesian sample points.energies:(probes, shells, samples)interaction energies (kcal/mol-like).gradients:(probes, shells, samples, 3)energy gradients.sector_ids:(samples,)sector assignment per direction.sector_centers:(sectors, 3)unit sector-center directions.
The sphere is partitioned into approximately equal sectors. Each sector gets a best probe, best shell, field score, direction vector, and gradient-derived growth vector. Sectors are not the same as growth sites: when an anchor is spatially known, sectors are clustered around each anchor dummy exit vector.
The anchor is the fixed starting scaffold. It must use mapped RDKit dummy atoms ([*:1], [*:2], [*:3], …). Bare * atoms are rejected because they cannot be mapped to spatial dummy positions.
Valid:
c1cc([*:1])ccc1[*:2]
[*:1]c1ccccc1.[*:2]CC
Invalid:
*c1ccccc1.*CC
--anchor-structure (SDF/MOL/PDB) is optional but important when the anchor is already placed in the pocket. Dummy isotope labels 1, 2, 3, … map to [*:1], [*:2], [*:3], …. PocketField clusters and ranks sectors around each dummy exit vector instead of assigning global top sectors blindly.
Fragments are one-dummy substituents ([*:1]C, [*:1]O, [*:1]C(=O)O). Linkers are two-dummy bridges ([*:1]CC[*:2], [*:1]C(=O)N[*:2]) used in bridge mode to connect two anchor dummy sites:
anchor [*:1] -> linker [*:1]
anchor [*:2] -> linker [*:2]
Fragment and linker libraries may be .csv, .json, or .jsonl. Only smiles is required; optional name and probes columns are honored. Minimal CSV:
smiles
[*:1]CCO
[*:1]c1ccccc1Linker CSV:
smiles
[*:1]CC[*:2]
[*:1]C(=O)N[*:2]Use bridge mode when disconnected anchor components should be connected through an enumerated linker:
pocketfield design \
--protein receptor.pdb \
--ligand ligand.pdb \
--anchor-smiles '[*:1]c1ccccc1.[*:2]CC' \
--bridge-anchor-dummies \
--linker-library examples/linkers.csvIn bridge mode PocketField uses true two-dummy linkers, bonds anchor site [*:1] to the linker [*:1] neighbor and [*:2] to the linker [*:2] neighbor, then removes all dummy atoms. Candidate records use mode: bridge and produce connected SMILES.
PocketField computes a 15-dim coefficient vector for each sector and a matching feature vector for each fragment/linker:
[0] hydrophobic [1] hbond_donor [2] hbond_acceptor
[3] positive [4] negative [5] aromatic
[6] steric_openness [7] small_size [8] medium_size
[9] large_size [10] rigidity [11] polarity
[12] depth [13] water_displacement [14] buried_polar_support
Sector coefficients come from probe-well strength, best-shell depth, steric openness, polarity/rigidity preference, and burial-dependent water-displacement and buried-polar support. Fragment/linker features come from atom types, donor/acceptor counts, formal charge, aromaticity, heavy-atom count, bond-rigidity proxy, and probe tags.
The match score is:
sector_match_score =
-dot(sector_vector, candidate_vector)
+ mismatch_penalty
+ unsupported_polar_desolvation_penalty
Lower is better. The desolvation penalty is applied outside the dot product so a desolvation risk penalizes unsupported buried polar atoms rather than rewarding polar fragments.
When --cross-attention-weight > 0, fragment rankings are refined by multi-head cross-attention over nearby sector feature vectors — a fragment bound in one sub-pocket is influenced by what sits in neighbouring sub-pockets. There are no learned parameters.
| Mechanism | Role |
|---|---|
| Co-assignment exclusion | Sectors bound by the same fragment (shared dummy) are self, not neighbours |
| Sparse attention | Only top-k spatially closest active sectors matter |
| Multi-head attention | Pharm (complementarity), shape (similarity), polar (similarity) heads |
| Charge modulation | Charged/polar fragments are more sensitive to neighbour environment |
| Confidence gating | Neighbours agree → amplified; disagree → dampened |
| Self-check | Fragment similarity to its own sector reinforces the base score |
| Mean-centering | Approximately zero-sum re-ranking per sector; above-mean → bonus, below-mean → penalty |
The base score uses similarity (the fragment should be what the pocket wants), while neighbour attention uses complementarity (a neighbour that wants X will itself be X, so my fragment should complement X).
PocketField scores are normalized to [0, 1] (lower is better):
per_atom: norm = (E_raw - E_min) / 50 # clip [0,1], 50 kcal/mol cap
field_score = mean(norm over heavy atoms)
clash_score = min(1.0, Σ overlap² / (n_heavy × 3.0 Ų))
pf_score = 0.35 × field_score + 0.30 × clash_score + 0.15 × (n_heavy / 70)
grow ranks candidates by pf_score alone. The sector-match score is used only to select which fragment fills each sector, not in the final candidate ranking.
pocketfield/solvation.py implements Onufriev-Bashford-Case generalized-Born with pairwise-descreened Born radii and a Shrake-Rupley nonpolar surface term:
ΔG_el = -½ (1/ε_in - 1/ε_out) × 332 × ΣᵢΣⱼ qᵢqⱼ / f_GB
f_GB = √(r² + RᵢRⱼ exp(-r² / 4RᵢRⱼ))
Born radii Rᵢ via HCT pairwise descreening (no bond exclusion)
ΔG_np = 0.005 × SASA (Shrake-Rupley)
ΔG_solv = ΔG_el + ΔG_np
A more negative ΔG_solv means more soluble, hence harder to desolvate into the pocket.
score_docked.py folds the solvation penalty into the PocketField score:
solv_penalty = (ΔG_max - ΔG) / (ΔG_max - ΔG_min) # [0,1], most soluble → 1
combined = pf_score + w × solv_penalty
Default w = 0.15 — PocketField dominates and solvation serves as a tiebreaker.
| Path | Description |
|---|---|
examples/tiny_receptor.pdb, examples/tiny_ligand.pdb |
Minimal smoke-test inputs |
examples/anchor_dummy.sdf |
Example 3D anchor with positioned dummy atoms |
examples/fragments.csv, examples/linkers.csv |
Built-in fragment/linker libraries |
examples/fragments_smiles_only.csv, examples/linkers_smiles_only.csv |
Minimal SMILES-only libraries |
tests/6duk_* (the EGFR reference protein, precomputed field, and grow outputs) are gitignored — they are large, per-experiment reference data supplied separately.
out/pocket_001/
├── field/ # `build` output (also written by `design`)
│ ├── field.npz # directions, shells, points, energies, gradients, sectors
│ ├── metadata.json # center, pocket atoms, probes, shell/sector settings
│ └── growth_plan.json # ranked single sectors + adjacent sector pairs
└── grow/ # `grow` output (also written by `design`)
├── candidates.sdf # ranked 3D molecules with score properties
├── candidates.json # score table, fragments, sector ids, SMILES
├── sector_coefficients.json # per-sector 15-dim coefficient vectors
├── fragment_features.json # per-fragment 15-dim feature vectors
├── linker_features.json # per-linker 15-dim feature vectors (bridge mode only)
├── dummy_sector_assignments.json # dummy map -> nearby sectors
├── sector_stats.json # per-sector score mean/std
├── reference_screening.json # fragments vs reference-occupied sectors (only with --reference-ligand)
└── reference_sector_coefficients.json # 15-dim vectors for reference-occupied sectors (only with --reference-ligand)
Important filters:
--max-heavy-atoms 45
--max-clash-score 0.5
--max-field-score 0.5Optional retrosynthesis validation:
pocketfield design \
... \
--retro-rules path/to/uspto.templates.classified.json.gzThis uses RetrosynthesisValidator from the bundled pocketfield/merge_utils.py (override with --retro-script). PocketField marks newly formed anchor-fragment or anchor-linker bonds with isotope pair 88/99, calls the validator, then strips isotopes before writing outputs.
Scores are normalised to [0, 1], so to disable the field/clash filters set them to 1.0 (--max-clash-score 1.0 --max-field-score 1.0); for real pockets, keep them strict.
PocketField builds the field, sector plan, and first-pass RDKit candidates with field/clash/vector scoring plus heuristic desolvation. It is not yet a production de novo design engine. Current limitations:
- No docking refinement.
- No conformer ensemble scoring.
- Desolvation inside
growis a heuristic coefficient/penalty; explicit thermodynamics is only in the externalscripts/solvation stage. - No torsion-aware linker fitting (bridge mode uses a coarse span prefilter).
- Retrosynthesis validation is optional and does not replace a full synthetic-accessibility model.