Open Foundation Integrated Structure and Affinity Prediction: Surpassing AlphaFold3 with Boltz-2 and Chai-1
The release of AlphaFold3 in May 2024 once again revolutionized the field of structural biology. However, for actual laboratory or drug discovery teams to run AlphaFold3 locally, they need to overcome two hurdles: a terabyte-scale genomic database and an academic-use-only license. In November 2024, MIT/Genentech released Boltz-1 as fully open source (MIT license), and in 2025, Boltz-2 integrated binding affinity prediction, becoming a practical tool for high-throughput drug screening, operating 1000 times faster than Free Energy Perturbation (FEP+). This article demonstrates how to build a hardcore drug discovery pipeline using this open-source tool combination.
📚 Recommended Prerequisite Material (Strongly Recommended)
This article represents the pinnacle of the AI × Bio hardcore advanced learning series. We strongly recommend that you review the following DryBench materials before proceeding:
- DryBench ai-native #3: Transformers and Embeddings
- DryBench ai-native #12: PyTorch Basics
- DryBench ai-native #14: Claude Code and Cursor
- DryBench ai-native #15: Bio-AI Integration
Without reviewing the prerequisite materials, you will find it difficult to follow the practical code demonstrated in this article, as it assumes familiarity with transformer architectures for 3D coordinate processing, large model loading and GPU management in PyTorch, pipeline automation using Claude Code, and a general understanding of the Bio-AI landscape.
We Learned This in Our DryBench Course
In DryBench ai-native #3, we learned that transformers can be extended to handle not only sequences but also arbitrary structured data such as 3D coordinates and graphs. In #12, we learned how PyTorch loads the parameters of large models into GPU memory and utilizes optimizations like mixed precision and flash attention. In #14, we learned that Claude Code is a practical tool for streamlining repetitive pipeline scripting. And in #15, we learned where structure prediction fits within the broader context of Bio-AI integration.
Now, let's examine why simply having "AlphaFold3" isn't enough from the perspective of an actual drug discovery team. Structure prediction alone doesn't tell us how strongly a ligand binds (affinity). Traditionally, this answer is obtained through FEP (Free Energy Perturbation) calculations, which take days to weeks per ligand. Boltz-2 integrates these two aspects (structure + affinity) into a single neural network forward pass, yielding results in milliseconds. This is the point where drug screening changes drastically, and this article provides a practical implementation.
Hardcore Problem Definition
Practical Requirements for Drug Screening
For a target protein (e.g., a specific kinase), we need to screen over 1000 candidate small molecule compounds to identify effective hits. Traditional approaches:
- Docking (AutoDock Vina, Glide): Takes seconds to minutes per ligand. However, while the poses are roughly correct, the affinity accuracy is low (correlation r ~ 0.4~0.5).
- FEP+ (Schrödinger): Takes days per ligand. High accuracy (r ~ 0.8+). However, it is commercial and the license is very expensive.
- AlphaFold3: Provides excellent complex structures but does not directly output affinity. Requires separate scoring.
- Boltz-2: Integrates structure + affinity (approximation of log10 IC50) output. Near-FEP+ accuracy. Takes milliseconds to seconds per ligand.
The goals of the pipeline covered in this article are:
- Target protein sequence + 1000 candidate ligand SMILES → Complete screening within 24 hours.
- Automatically rank the top 20 affinity candidates and generate automated PyMOL visualizations.
- Handle multiple conformations: Sample multiple poses for each ligand and perform ensemble scoring.
- Enforce reproducibility: All runs are defined by a single YAML configuration file for tag tracking.
Why Boltz-2 and Chai-1, and Not AlphaFold3?
- License: AlphaFold3 requires academic non-commercial approval, and commercial use requires separate negotiation. Boltz-1/2 and Chai-1 are open source (Boltz is MIT [1], Chai is research-free + commercial separately [2]).
- Infrastructure Burden: AlphaFold3 requires local synchronization of terabyte-scale databases such as UniRef, MGnify, and PDB. Boltz-2 can use MMseqs2 remote API instead [1].
- Affinity Integration: AF3 only predicts structure. Boltz-2 integrates structure + affinity prediction [1].
- Speed: Boltz-2 is 1000x faster than FEP+ (based on published results [3]).
- Commercial Freedom: The MIT license has no restrictions on commercial use. Chai-1 provides a commercial paid API.
Tool Stack and Infrastructure Requirements
| Tool | Role | License |
|---|---|---|
Boltz (v2, pip install boltz) | Integrated structure + affinity prediction | MIT |
Chai-1 (chai_lab) | Alternative/ensemble partner | Research-free, commercial separately |
| MMseqs2 remote API (BioLM free tier) | MSA construction (no local DB required) | GPL v3 (MMseqs2 itself), API is BioLM policy |
| RDKit | SMILES parsing, ligand processing | BSD-3-Clause |
| PyMOL (open-source version) | 3D structure visualization | LGPL |
| Biopython | PDB file post-processing | Biopython License |
| PyTorch, CUDA | Neural network backend | BSD |
Infrastructure Requirements:
- Minimum Practical: 24GB VRAM data center GPU or high-end consumer GPU (RTX 4090 24GB) x 1. Boltz-2 consumes approximately 15GB VRAM in fp16, and larger complexes may require 20GB+.
- CPU Fallback: Very slow (single prediction takes over 30 minutes), practically unusable.
- RAM: 32GB or more.
- Disk: Boltz weights approximately 2-5GB, Chai-1 weights approximately 5GB, MSA cache several GB.
- Network: MMseqs2 remote API calls require several MB of communication per target.
Estimated Cost for Learners: If you don't have a local GPU, refer to the hourly rate of cloud on-demand GPU instances (e.g., 24GB VRAM). Screening 1000 ligands takes approximately 2-4 hours of GPU time (based on Boltz-2 paper benchmarks [3]).
Practical Pipeline Implementation
Overall Flow:
Step 1. Target Protein MSA Remote Construction
Instead of installing MMseqs2 locally and downloading UniRef, use the remote API. Boltz supports multiple remote MSA servers [4].
from pathlib import Pathfrom dataclasses import dataclass
import requests
@dataclassclass MSAResult: """Multiple Sequence Alignment Result.""" query_id: str a3m_content: str # a3m format MSA depth: int # MSA depth (number of sequences)
def build_msa_remote( query_sequence: str, query_id: str = "target", api_base: str = "https://api.colabfold.com", max_depth: int = 5000,) -> MSAResult: """Build remote MSA using ColabFold MSA server (MMseqs2 backend).
The Boltz official documentation recommends ColabFold or BioLM API [4]. """ # Actual API call (example: ColabFold MMseqs2 API) payload = {"query": f">{query_id}\n{query_sequence}", "mode": "env"} resp = requests.post(f"{api_base}/ticket/msa", data=payload, timeout=600) resp.raise_for_status() ticket = resp.json()["id"]
# Poll and wait for completion import time while True: status = requests.get(f"{api_base}/ticket/{ticket}", timeout=30).json() if status["status"] == "COMPLETE": break elif status["status"] == "ERROR": raise RuntimeError(f"MSA construction failed: {status}") time.sleep(5)
# Download a3m result a3m_resp = requests.get(f"{api_base}/result/download/{ticket}", timeout=60) a3m_resp.raise_for_status() a3m_content = a3m_resp.text depth = a3m_content.count("\n>")
return MSAResult(query_id=query_id, a3m_content=a3m_content, depth=depth)
def save_a3m(msa: MSAResult, output_dir: Path) -> Path: """Save a3m file.""" output_dir.mkdir(parents=True, exist_ok=True) path = output_dir / f"{msa.query_id}.a3m" path.write_text(msa.a3m_content) return pathStep 2. Ligand Library Canonicalization
Since SMILES can represent the same molecule in multiple ways, canonicalization is necessary. RDKit is the standard.
from rdkit import Chemfrom rdkit.Chem import AllChem, Descriptors, Lipinskiimport pandas as pd
def canonicalize_smiles(smiles: str) -> str | None: """Return SMILES canonical form. Return None if parsing fails.""" mol = Chem.MolFromSmiles(smiles) if mol is None: return None return Chem.MolToSmiles(mol, canonical=True)
def lipinski_filter(smiles: str) -> dict: """Check if Lipinski's Rule of Five is satisfied + physicochemical properties.""" mol = Chem.MolFromSmiles(smiles) if mol is None: return {"valid": False} props = { "MW": Descriptors.MolWt(mol), "LogP": Descriptors.MolLogP(mol), "HBA": Lipinski.NumHAcceptors(mol), "HBD": Lipinski.NumHDonors(mol), "RotB": Lipinski.NumRotatableBonds(mol), } violations = sum([ props["MW"] > 500, props["LogP"] > 5, props["HBA"] > 10, props["HBD"] > 5, ]) props["valid"] = True props["lipinski_violations"] = violations props["lipinski_pass"] = violations <= 1 return props
def prepare_ligand_library(smiles_list: list[str]) -> pd.DataFrame: """Canonicalize ligand library + physicochemical filter.""" records = [] for i, raw in enumerate(smiles_list): canon = canonicalize_smiles(raw) if canon is None: continue props = lipinski_filter(canon) records.append({ "ligand_id": f"L{i:04d}", "smiles_raw": raw, "smiles_canonical": canon, **props, }) df = pd.DataFrame(records) return df[df["lipinski_pass"]].reset_index(drop=True)Step 3. Boltz-2 YAML Config Generation
Boltz specifies protein + ligand + MSA in a single YAML file [4].
import yaml
def build_boltz_config( protein_sequence: str, protein_msa_path: Path, ligand_smiles: str, ligand_id: str, output_dir: Path,) -> Path: """Generate Boltz YAML config. One per protein-ligand pair.""" config = { "version": 1, "sequences": [ { "protein": { "id": "A", "sequence": protein_sequence, "msa": str(protein_msa_path), } }, { "ligand": { "id": "L", "smiles": ligand_smiles, } }, ], "constraints": [], "properties": [ {"affinity": {"binder": "L"}} # Enable binding affinity prediction (Boltz-2 new) ], } output_dir.mkdir(parents=True, exist_ok=True) yaml_path = output_dir / f"{ligand_id}.yaml" with open(yaml_path, "w") as f: yaml.safe_dump(config, f, sort_keys=False) return yaml_pathStep 4. Boltz-2 Batch Inference
Iterate over multiple ligands and perform prediction. Session management considering GPU memory.
import subprocessimport json
@dataclassclass BoltzResult: """Boltz-2 prediction result.""" ligand_id: str pdb_path: Path # Predicted 3D complex structure plddt_mean: float # Structure confidence (0~100) iptm: float # Interface pTM (0~1) affinity_log_ic50: float # Predicted log10(IC50 mol/L) (lower is better) inference_time_sec: float
def run_boltz_prediction( yaml_path: Path, output_dir: Path, device: str = "cuda", use_msa_server: bool = False,) -> BoltzResult: """Run Boltz-2 CLI (Python API is also possible in practice).""" import time start = time.time()
cmd = [ "boltz", "predict", str(yaml_path), "--out_dir", str(output_dir), "--devices", "1", "--accelerator", "gpu" if device == "cuda" else "cpu", ] if use_msa_server: cmd.append("--use_msa_server")
result = subprocess.run(cmd, capture_output=True, text=True, check=True) elapsed = time.time() - start
# Boltz generates output_dir/{name}/{name}_model_0.pdb + confidence.json name = yaml_path.stem pdb_path = output_dir / name / f"{name}_model_0.pdb" conf_path = output_dir / name / f"confidence_{name}_model_0.json"
with open(conf_path) as f: conf = json.load(f)
# Affinity is in a separate JSON (Boltz-2 new field) affinity_path = output_dir / name / f"affinity_{name}.json" affinity_data = json.loads(affinity_path.read_text()) if affinity_path.exists() else {}
return BoltzResult( ligand_id=name, pdb_path=pdb_path, plddt_mean=float(conf.get("complex_plddt", 0.0)), iptm=float(conf.get("iptm", 0.0)), affinity_log_ic50=float(affinity_data.get("affinity_pred_value", 0.0)), inference_time_sec=elapsed, )Step 5. Batch Screening Orchestration
Iterate over the entire ligand library + handle failures + display progress.
from tqdm import tqdm
def batch_screen( protein_sequence: str, protein_msa: MSAResult, ligand_library: pd.DataFrame, work_dir: Path, device: str = "cuda",) -> pd.DataFrame: """Screen the entire ligand library.""" msa_path = save_a3m(protein_msa, work_dir / "msa")
results = [] failures = [] for _, row in tqdm(ligand_library.iterrows(), total=len(ligand_library), desc="Screening"): try: yaml_path = build_boltz_config( protein_sequence=protein_sequence, protein_msa_path=msa_path, ligand_smiles=row["smiles_canonical"], ligand_id=row["ligand_id"], output_dir=work_dir / "configs", ) boltz_result = run_boltz_prediction( yaml_path=yaml_path, output_dir=work_dir / "predictions", device=device, use_msa_server=False, ) results.append({ "ligand_id": boltz_result.ligand_id, "smiles": row["smiles_canonical"], "affinity_log_ic50": boltz_result.affinity_log_ic50, "plddt": boltz_result.plddt_mean, "iptm": boltz_result.iptm, "pdb_path": str(boltz_result.pdb_path), "inference_sec": boltz_result.inference_time_sec, "MW": row.get("MW"), "LogP": row.get("LogP"), }) except Exception as e: failures.append({"ligand_id": row["ligand_id"], "error": str(e)})
df_results = pd.DataFrame(results).sort_values("affinity_log_ic50") df_failures = pd.DataFrame(failures) df_failures.to_csv(work_dir / "failures.csv", index=False) return df_resultsStep 6. Top-K PyMOL Visualization
Automatically render the binding pose of the top candidates.
def render_pymol( pdb_path: Path, output_png: Path, ray_trace: bool = True,) -> None: """Headless PyMOL rendering.""" script = f"""load {pdb_path}, complexhide everythingshow cartoon, complex and polymershow sticks, complex and organiccolor grey70, complex and polymercolor yellow, complex and organicbg_color whitezoom complex and organic, 5{"ray 1200, 900" if ray_trace else ""}png {output_png}, dpi=150quit""" script_path = output_png.parent / "render.pml" script_path.write_text(script) subprocess.run(["pymol", "-cq", str(script_path)], check=True)
def render_top_k(results_df: pd.DataFrame, output_dir: Path, k: int = 20) -> None: output_dir.mkdir(parents=True, exist_ok=True) for _, row in results_df.head(k).iterrows(): render_pymol( pdb_path=Path(row["pdb_path"]), output_png=output_dir / f"{row['ligand_id']}.png", )Unified Pipeline
def full_screening_pipeline( protein_sequence: str, protein_id: str, smiles_library: list[str], work_dir: Path, device: str = "cuda", top_k: int = 20,) -> pd.DataFrame: """FASTA + SMILES library -> Ranked DataFrame.""" print(f"[1/6] Remote MSA construction request (query length={len(protein_sequence)})") msa = build_msa_remote(protein_sequence, query_id=protein_id) print(f" MSA depth={msa.depth}")
print(f"[2/6] Ligand library canonicalization (raw={len(smiles_library)})") library = prepare_ligand_library(smiles_library) print(f" Lipinski pass={len(library)}")
print(f"[3/6] Batch screening start") results = batch_screen(protein_sequence, msa, library, work_dir, device) results.to_csv(work_dir / "screening_results.csv", index=False)
print(f"[4/6] Top-{top_k} PyMOL rendering") render_top_k(results, work_dir / "top_k_renders", k=top_k)
print(f"[5/6] Complete: work_dir={work_dir}") print(f"[6/6] Top 5 ligands summary:") print(results.head(5)[["ligand_id", "affinity_log_ic50", "plddt", "iptm"]].to_string(index=False)) return results
## Chai-1 Ensemble Verification (Optional Enhancement)
Chai-1 is a SOTA open foundation model similar to Boltz, so re-evaluating top candidates with Chai-1 can reduce false positives [2].
```pythonfrom chai_lab.chai1 import run_inference
def chai_reverification( protein_sequence: str, ligand_smiles: str, output_dir: Path, device: str = "cuda",) -> dict: """Re-predict the same pair using Chai-1. Ensemble scoring with Boltz results.""" fasta_str = f">protein|name=A\n{protein_sequence}\n>ligand|name=L\n{ligand_smiles}\n" fasta_path = output_dir / "chai_input.fasta" fasta_path.write_text(fasta_str) output_dir.mkdir(parents=True, exist_ok=True) result = run_inference( fasta_file=fasta_path, output_dir=output_dir, num_trunk_recycles=3, num_diffn_timesteps=200, seed=42, device=device, use_esm_embeddings=True, ) return { "pdb_path": result[0], "iptm": float(result[0].iptm), # Example field, refer to the actual API }
def ensemble_ranking( boltz_score: float, chai_score: float, weight: float = 0.5,) -> float: """Weighted average of scores from the two models (lower is better).""" return weight * boltz_score + (1 - weight) * chai_scorePerformance, Cost, and Known Failure Cases
Performance Reference (Citation of Public Benchmarks)
| Approach | Benchmark | Structure RMSD (Å) | Affinity Pearson r | Time/Pair | Source |
|---|---|---|---|---|---|
| Docking (AutoDock Vina) | PDBbind core | 3.5~5.0 | 0.4~0.5 | Seconds | Legacy |
| Glide XP | PDBbind core | 2.5~3.5 | 0.55~0.65 | Minutes | Schrödinger |
| FEP+ (Schrödinger) | Selected subset | — | 0.75~0.85 | Days | Commercial |
| AlphaFold3 (Structure only) | Recent PDB | 1.5~2.5 | Separate scoring required | Seconds~Minutes | Google DeepMind 2024 |
| Chai-1 | Benchmark subset | ~2.0 | ~0.65 | Seconds~Minutes | Chai Discovery 2024 [2] |
| Boltz-1 | Recent PDB | 2.0~3.0 | ~0.6 | Seconds | MIT/Genentech 2024 [1] |
| Boltz-2 | PDBbind, etc. | ~2.0 | ~0.80 (FEP+ level) | Milliseconds~Seconds | Wohlwend et al. 2025 [3] |
Estimated Cost for Learners
- API cost: 0 (when using a local GPU). Utilizing the free tier of the MMseqs2 remote API.
- GPU time: Approximately 2-4 hours for one target + screening 1000 ligands (based on a 24GB VRAM GPU).
- Time-based pricing may apply when using cloud on-demand services.
- Disk space: Boltz weights 2-5GB, resulting PDB + rendering approximately 500MB.
Three Known Failure Cases (Community/Paper Collection)
-
OOM (Out of Memory) for large complexes (>2000 residues) Symptom: Even with 24GB VRAM, large multi-subunit complexes exceed memory limits. Cause: Boltz-2's attention mechanism has O(N²) memory complexity with respect to the number of residues. Mitigation: (a) Enable the
--use_flash_attentionoption, (b) Force fp16, (c) For large complexes, predict subunit by subunit and then combine the results in post-processing, (d) 48GB+ VRAM GPU required. Source: Boltz GitHub Issues — "OOM for large complexes" thread [5]. -
Significant accuracy drop due to insufficient MSA depth Symptom: For new orphan sequences or metagenomic sequences, MMseqs2 remote may return MSA depths < 32, leading to a significant drop in Boltz-2 pLDDT and affinity reliability. Cause: Foundation models still rely on co-evolution signals. Mitigation: (a) Add a warning flag to the results if MSA depth < 100, (b) Try multiple servers with the
--msa_serveroption, (c) Compare with a baseline using single-sequence mode (no MSA) to assess reliability. Source: Boltz official documentation "MSA quality" section [6]. -
Incorrect binding pose due to ignoring ligand stereochemistry Symptom: Even if stereochemistry is specified in the SMILES string, the prediction arbitrarily selects among the different stereoisomers. Cause: Loss of chirality tag during the SMILES canonicalization step or bias in the foundation model's training data. Mitigation: (a) Use
Chem.MolFromSmiles(canonical=False)to preserve the original stereochemistry, (b) Generate 3D conformers with RDKitAllChem.EmbedMoleculeand try inputting as SDF, (c) Post-validate the chirality of the predicted pose. Source: RDKit Discussions + Boltz Issues (multiple threads related to stereochemistry) [7].
Extension Ideas
- Fragment-based drug discovery: Compose a ligand library primarily of fragments (< 300 Da), screen for hit fragments with Boltz-2, and then design linkers.
- Target class benchmark: Repeat screening of the same ligand library against an entire kinase family (e.g., MAPK) to create a selectivity map.
- Active learning loop: Perform experimental assays on the top predictions, feed the results back, and fine-tune the model (domain-specific).
- Protein-Protein complex + ligand: Boltz supports multiple chains. Expand to scenarios involving antibody-antigen + small molecule combinations.
- MCP tool exposure: Have the MCP Agent from Part 14 call this pipeline as a tool, enabling autonomous drug candidate research.
Next Part
- Part 08
docking-hybrid-diffusion: Refine the poses of the top-k candidates from this part using a DiffDock-Glide hybrid. - Part 13
protein-design-multimodal: Design new target proteins using ESM3, and then screen for binding candidates using Boltz-2. - Part 14
bio-mcp-agent: Wrap this pipeline as an MCP tool to create an autonomous drug research agent. - Part 15
bio-mcp-server-suite: Custom MCP server for accessing PDBbind and ChEMBL data.
References
- Wohlwend J, Corso G, Passaro S, et al. Boltz-1: An open-source foundation model for structure prediction. MIT/Genentech 2024. GitHub:
https://github.com/jwohlwend/boltz - Chai Discovery. "Chai-1: Decoding the molecular interactions of life." bioRxiv 2024.
https://www.biorxiv.org/content/10.1101/2024.10.10.615955v2/ GitHub:https://github.com/chaidiscovery/chai-lab - Passaro S, Corso G, Wohlwend J, et al. "Boltz-2: Towards Accurate and Efficient Binding Affinity Prediction." bioRxiv 2025.
https://www.biorxiv.org/content/10.1101/2025.06.14.659707v1 - Boltz official documentation (prediction usage):
https://github.com/jwohlwend/boltz/blob/main/docs/prediction.md - Boltz GitHub Issues (OOM, MSA, stereochemistry):
https://github.com/jwohlwend/boltz/issues - Boltz official documentation MSA section:
https://github.com/jwohlwend/boltz/blob/main/docs/msa.md - RDKit Discussions (stereochemistry canonicalization):
https://github.com/rdkit/rdkit/discussions - Abramson J, Adler J, Dunger J, et al. "Accurate structure prediction of biomolecular interactions with AlphaFold 3." Nature 2024.
https://www.nature.com/articles/s41586-024-07487-w - Schrödinger FEP+ (commercial):
https://www.schrodinger.com/products/fep - PDBbind:
http://www.pdbbind.org.cn/ - MMseqs2:
https://github.com/soedinglab/MMseqs2 - ColabFold MSA server (open tier):
https://github.com/sokrypton/ColabFold - BioLM API:
https://biolm.ai/models/ - RDKit:
https://www.rdkit.org/ - PyMOL open-source:
https://github.com/schrodinger/pymol-open-source - AutoDock Vina:
https://vina.scripps.edu/ - Recent PDB benchmark set:
https://www.rcsb.org/