Install any skill in seconds. Free to start, no credit card required.
Get Started Free →PyTorch-based ML platform for drug discovery: graph molecular representation learning, property prediction (ADMET, activity), retrosynthesis, drug-target interaction (DTI), and pretraining on large molecular datasets. Provides GNN layers (GraphConv, GAT, MPNN), pretrained models, and benchmark datasets.
.claude/skills/jaechang-hits-torchdrug/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-06 | ✗→✓ | ▲ Improved | 124% | 0% |
| case-07 | ✗→✓ | ▲ Improved | 118% | 0% |
| case-16 | ✗→✓ | ▲ Improved | 322% | 0% |
| case-02 | ✗→✓ | ▲ Improved | 142% | 0% |
| case-11 | ✗→✓ | ▲ Improved | 246% | 0% |
TorchDrug is a comprehensive machine learning framework for drug discovery built on PyTorch. It provides graph-based molecular representations (atoms as nodes, bonds as edges), a library of graph neural network (GNN) architectures, benchmark datasets, and pretrained models for tasks including molecular property prediction, drug-target interaction, retrosynthesis, and generative molecular design. TorchDrug integrates with PyTorch Lightning and standard ML tooling, making it accessible to both computational chemists and ML practitioners.
torchdrug, torch, torch-geometric, rdkitbashpip install torch torchvision --extra-index-url https://download.pytorch.org/whl/cu118 pip install torch-geometric pip install torchdrug pip install rdkit
pythonimport torch from torchdrug import data, datasets, models, tasks, core # Load a benchmark dataset and train a GNN for property prediction dataset = datasets.BBBP("~/data/bbbp", node_feature="default", edge_feature="default") print(f"Dataset: {len(dataset)} molecules, task: BBBP (blood-brain barrier penetration)") # Define model: GIN encoder model = models.GIN( input_dim=dataset.node_feature_dim, hidden_dims=[256, 256], short_cut=True, batch_norm=True, concat_hidden=True, ) # Define training task task = tasks.PropertyPrediction( model, task=dataset.tasks, criterion="bce", metric=("auprc", "auroc"), ) # Train with the Solver optimizer = torch.optim.Adam(task.parameters(), lr=1e-3) solver = core.Engine(task, dataset, None, None, optimizer, gpus=[0]) solver.train(num_epoch=50) print("Training complete")
TorchDrug represents molecules as typed graphs. data.Molecule is the core data structure.
pythonfrom torchdrug import data from rdkit import Chem # Create a molecule from SMILES smiles = "CC(=O)Oc1ccccc1C(=O)O" # aspirin mol = data.Molecule.from_smiles(smiles, node_feature="default", edge_feature="default") print(f"Atoms: {mol.num_node}") print(f"Bonds: {mol.num_edge}") print(f"Node feature dim: {mol.node_feature.shape}") # (N_atoms, feature_dim) print(f"Edge feature dim: {mol.edge_feature.shape}") # (N_bonds*2, feature_dim)
python# Convert a MoleculeNet / custom SMILES list to a dataset from torchdrug import data as td_data import pandas as pd df = pd.read_csv("compounds.csv") # columns: smiles, label molecules = [td_data.Molecule.from_smiles(s) for s in df["smiles"] if s] print(f"Loaded {len(molecules)} valid molecules") # Check feature dimensions print(f"Default atom feature dim: {molecules[0].node_feature.shape[1]}")
TorchDrug provides GIN, RGCN, GraphSAGE, GAT, MPNN, AttentiveFP, and more.
pythonfrom torchdrug import models, datasets dataset = datasets.ESOL("~/data/esol", node_feature="default", edge_feature="default") feature_dim = dataset.node_feature_dim # Graph Isomorphism Network (GIN) — good default for property prediction gin = models.GIN( input_dim=feature_dim, hidden_dims=[256, 256, 256], short_cut=True, batch_norm=True, concat_hidden=True, # concatenate layer representations ) print(f"GIN output_dim: {gin.output_dim}")
pythonfrom torchdrug import models # Message Passing Neural Network (MPNN) — captures edge features mpnn = models.MPNN( input_dim=feature_dim, hidden_dim=256, edge_input_dim=16, # edge feature dimension num_layer=4, num_gru_layer=1, ) # Graph Attention Network (GAT) — attention-weighted neighbors gat = models.GAT( input_dim=feature_dim, hidden_dims=[256, 256], edge_input_dim=16, num_head=8, batch_norm=True, ) print(f"MPNN output_dim: {mpnn.output_dim}, GAT output_dim: {gat.output_dim}")
Wrap a GNN encoder with a prediction head for classification or regression.
pythonimport torch from torchdrug import datasets, models, tasks, core # Regression example: ESOL aqueous solubility dataset = datasets.ESOL("~/data/esol", node_feature="default", edge_feature="default") train, val, test = dataset.split() print(f"Train: {len(train)}, Val: {len(val)}, Test: {len(test)}") model = models.GIN( input_dim=dataset.node_feature_dim, hidden_dims=[300, 300], short_cut=True, batch_norm=True, concat_hidden=True, ) task = tasks.PropertyPrediction( model, task=dataset.tasks, # list of property names criterion="mse", # "mse" for regression, "bce" for classification metric=("mae", "rmse"), num_mlp_layer=2, ) optimizer = torch.optim.Adam(task.parameters(), lr=1e-3, weight_decay=1e-5) solver = core.Engine(task, train, val, test, optimizer, batch_size=32, log_interval=50) solver.train(num_epoch=100) # Evaluate on test set metrics = solver.evaluate("test") print(f"Test RMSE: {metrics['rmse']:.4f}") print(f"Test MAE: {metrics['mae']:.4f}")
Predict binding affinity between molecules and protein sequences.
pythonfrom torchdrug import datasets, models, tasks, core import torch # Load a DTI dataset (e.g., Davis kinase binding affinities) dataset = datasets.Davis("~/data/davis", mol_node_feature="default", mol_edge_feature="default") train, val, test = dataset.split() # Molecule encoder mol_model = models.GIN( input_dim=dataset.mol_node_feature_dim, hidden_dims=[256, 256], short_cut=True, batch_norm=True, concat_hidden=True, ) # Protein encoder (CNN on sequence) prot_model = models.ProteinCNN( input_dim=21, # amino acid vocabulary size hidden_dims=[128, 128, 128], kernel_size=3, ) task = tasks.InteractionPrediction( mol_model, prot_model, task=dataset.tasks, criterion="mse", metric=("rmse", "pearsonr"), ) optimizer = torch.optim.Adam(task.parameters(), lr=1e-3) solver = core.Engine(task, train, val, test, optimizer, batch_size=64, log_interval=100) solver.train(num_epoch=50) metrics = solver.evaluate("test") print(f"DTI Test RMSE: {metrics['rmse']:.4f}") print(f"DTI Pearson r: {metrics['pearsonr']:.4f}")
Predict one-step retrosynthetic disconnections to find plausible building blocks.
pythonfrom torchdrug import datasets, models, tasks, core import torch # USPTO-50k retrosynthesis benchmark dataset = datasets.USPTO50k("~/data/uspto50k", as_synthon=False, atom_feature="default", bond_feature="default") train, val, test = dataset.split() # Reaction-predicting GNN model = models.RGCN( input_dim=dataset.node_feature_dim, hidden_dims=[256, 256, 256], num_relation=dataset.num_bond_type, batch_norm=True, ) task = tasks.CenterIdentification( model, feature=("graph", "atom", "bond"), ) optimizer = torch.optim.Adam(task.parameters(), lr=1e-4) solver = core.Engine(task, train, val, test, optimizer, batch_size=64, log_interval=100) solver.train(num_epoch=50) metrics = solver.evaluate("test") print(f"Retrosynthesis top-1 accuracy: {metrics.get('accuracy', 'N/A')}")
Use TorchDrug's pretrained GNN representations as features for downstream tasks.
pythonfrom torchdrug import models # Load a GNN pretrained on ChEMBL with context-prediction self-supervised learning pretrained_gin = models.GIN( input_dim=39, hidden_dims=[300, 300, 300, 300, 300], short_cut=False, batch_norm=True, concat_hidden=False, ) # Load pretrained weights (download from TorchDrug model zoo) import torch ckpt = torch.load("gin_supervised_contextpred.pth", map_location="cpu") pretrained_gin.load_state_dict(ckpt) pretrained_gin.eval() print(f"Pretrained GIN loaded, output_dim={pretrained_gin.output_dim}") print("Use as encoder in PropertyPrediction task for transfer learning")
Molecules are represented as attributed graphs: atoms are nodes with features (atomic number, degree, charge, aromaticity) and bonds are edges with features (bond type, ring membership). All TorchDrug models operate on these graph representations rather than SMILES strings or fingerprints.
pythonfrom torchdrug import data mol = data.Molecule.from_smiles("c1ccccc1") # benzene print(f"Atoms: {mol.num_node}, Bonds: {mol.num_edge // 2}") print(f"Atom features (first atom): {mol.node_feature[0]}")
TorchDrug uses a core.Engine (also called Solver) to handle the training loop, logging, checkpointing, and multi-GPU setup. Pass the task, train/val/test splits, and optimizer to the Engine rather than writing a manual training loop.
python# Engine handles: batch iteration, loss backward, logging, checkpointing solver = core.Engine( task, train_set, valid_set, test_set, optimizer, batch_size=32, log_interval=100, gpus=[0, 1], # multi-GPU support ) solver.train(num_epoch=100) solver.save("checkpoint.pth")
Goal: Train a GIN model to predict blood-brain barrier penetration from SMILES, then predict on new compounds.
pythonimport torch import pandas as pd from torchdrug import data, datasets, models, tasks, core # 1. Load dataset dataset = datasets.BBBP("~/data/bbbp", node_feature="default", edge_feature="default") train, val, test = dataset.split() print(f"BBBP: {len(train)} train, {len(val)} val, {len(test)} test molecules") # 2. Build model model = models.GIN( input_dim=dataset.node_feature_dim, hidden_dims=[256, 256], short_cut=True, batch_norm=True, concat_hidden=True, ) task = tasks.PropertyPrediction( model, task=dataset.tasks, criterion="bce", metric=("auroc", "auprc"), ) # 3. Train optimizer = torch.optim.Adam(task.parameters(), lr=1e-3) solver = core.Engine(task, train, val, test, optimizer, batch_size=32, log_interval=50) solver.train(num_epoch=100) metrics = solver.evaluate("test") print(f"Test AUROC: {metrics['auroc']:.4f}") # 4. Predict on new SMILES new_smiles = ["CC(=O)Oc1ccccc1C(=O)O", "c1ccc(cc1)N"] task.eval() with torch.no_grad(): for smi in new_smiles: mol = data.Molecule.from_smiles(smi, node_feature="default", edge_feature="default") batch = data.Batch.from_data_list([mol]) pred = task.predict(batch) print(f" {smi}: BBB penetration probability = {pred.sigmoid().item():.3f}")
Goal: Simultaneously predict 12 toxicity endpoints using a shared GNN encoder.
pythonimport torch from torchdrug import datasets, models, tasks, core # Tox21: 12 toxicity assays, multi-label classification dataset = datasets.Tox21("~/data/tox21", node_feature="default", edge_feature="default") train, val, test = dataset.split() print(f"Tox21 tasks ({len(dataset.tasks)}): {dataset.tasks}") model = models.GIN( input_dim=dataset.node_feature_dim, hidden_dims=[300, 300, 300], short_cut=True, batch_norm=True, concat_hidden=True, ) # Multi-task: one output head per toxicity assay task = tasks.PropertyPrediction( model, task=dataset.tasks, criterion="bce", metric=("auroc",), num_mlp_layer=2, ) optimizer = torch.optim.Adam(task.parameters(), lr=1e-3) solver = core.Engine(task, train, val, test, optimizer, batch_size=64) solver.train(num_epoch=100) metrics = solver.evaluate("test") for name, val_score in metrics.items(): print(f" {name}: {val_score:.4f}")
| Parameter | Module | Default | Range / Options | Effect | |-----------|--------|---------|-----------------|--------| | hidden_dims | GIN/MPNN/GAT | [256, 256] | list of int | Width and depth of GNN layers | | short_cut | GIN | False | True, False | Add residual connection between layers | | batch_norm | GIN/MPNN | False | True, False | Apply batch normalization after each layer | | concat_hidden | GIN | False | True, False | Concatenate all layer outputs as final representation | | num_mlp_layer | PropertyPrediction | 1 | 1–4 | Depth of MLP prediction head after GNN | | criterion | PropertyPrediction | "mse" | "mse", "bce", "ce" | Loss function: regression, binary/multi-label classification | | batch_size | Engine | 32 | 8–512 | Training batch size |
concat_hidden=True for GIN on small datasets: Concatenating all layer outputs provides a richer molecular representation and often improves performance when training data is limited (<10,000 molecules).batch_norm=True for training stability: Batch normalization reduces sensitivity to learning rate and initialization, especially with deep GNNs (3+ layers).dataset.split(test_scaffold_ratio=0.1) for more realistic evaluation.PropertyPrediction task handles NaN labels automatically, but verify that missing rates are not too high for rare assays.When to use: Visualize a molecular library in embedding space or use GNN features in scikit-learn models.
pythonimport torch import numpy as np from torchdrug import data, models model = models.GIN(input_dim=39, hidden_dims=[300, 300], concat_hidden=True) model.eval() smiles_list = ["CC(=O)O", "c1ccccc1", "CCN", "CC(=O)Oc1ccccc1C(=O)O"] embeddings = [] with torch.no_grad(): for smi in smiles_list: mol = data.Molecule.from_smiles(smi, node_feature="default") batch = data.Batch.from_data_list([mol]) graph_feat = model(batch, batch.node_feature.float())["graph_feature"] embeddings.append(graph_feat.squeeze(0).numpy()) emb_matrix = np.stack(embeddings) print(f"Embedding matrix: {emb_matrix.shape}") # (N_mols, embed_dim)
When to use: Training on proprietary assay data rather than benchmark datasets.
pythonfrom torchdrug import data import torch class CustomDataset(data.MoleculeDataset): def __init__(self, csv_path, smiles_col="smiles", label_col="activity"): import pandas as pd df = pd.read_csv(csv_path).dropna(subset=[smiles_col]) smiles_list = df[smiles_col].tolist() targets = df[label_col].tolist() self.load_smiles(smiles_list, {"activity": targets}, node_feature="default", edge_feature="default") self.tasks = ["activity"] dataset = CustomDataset("assay_data.csv", smiles_col="smiles", label_col="pIC50") print(f"Custom dataset: {len(dataset)} molecules")
| Problem | Cause | Solution | |---------|-------|----------| | ImportError: torchdrug | Package not installed | pip install torchdrug after installing PyTorch | | CUDA error: device-side assert | Label dtype mismatch | Ensure regression labels are float, classification labels are long | | Poor test metrics with small dataset | Overfitting | Use pretrained weights, add dropout, or reduce model depth | | KeyError: task name in dataset.tasks | Task name mismatch | Print dataset.tasks to see exact task names; pass the same list to PropertyPrediction | | RuntimeError: Expected all tensors on same device | Mixed CPU/GPU tensors | Use solver = core.Engine(..., gpus=[0]) to ensure consistent device placement | | Slow training | CPU-only mode | Install CUDA-compatible PyTorch; set gpus=[0] in Engine | | Missing assay values cause NaN loss | Dataset has missing labels | Set criterion="bce" — TorchDrug masks NaN labels during loss computation |
rdkit — molecular fingerprints and cheminformatics preprocessing before TorchDrugdiffdock — structure-based docking complementary to TorchDrug's ligand-based prediction| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-01 | fail→fail | 12,508 | 21,762 | +74% | 1 | 1 | 0% | 2,854 | 7,968 | +179% | 0 | 0 | — |
case-06 | fail→pass | 21,061 | 14,868 | -29% | 1 | 1 | 0% | 3,620 | 8,110 | +124% | 0 | 0 | — |
case-07 | fail→pass | 16,579 | 8,575 | -48% | 1 | 1 | 0% | 3,255 | 7,102 | +118% | 0 | 0 | — |
case-16 | fail→pass | 6,699 | 3,715 | -45% | 1 | 1 | 0% | 1,425 | 6,011 | +322% | 0 | 0 | — |
case-02 | fail→pass | 15,357 | 11,411 | -26% | 1 | 1 | 0% | 3,152 | 7,624 | +142% | 0 | 0 | — |
case-03 | pass→pass | 10,620 | 9,618 | -9% | 1 | 1 | 0% | 2,157 | 7,335 | +240% | 0 | 0 | — |
case-04 | pass→pass | 13,916 | 9,444 | -32% | 1 | 1 | 0% | 2,509 | 7,033 | +180% | 0 | 0 | — |
case-05 | pass→pass | 14,059 | 28,235 | +101% | 1 | 1 | 0% | 2,564 | 7,725 | +201% | 0 | 0 | — |
case-08 | pass→pass | 11,931 | 5,216 | -56% | 1 | 1 | 0% | 2,175 | 6,364 | +193% | 0 | 0 | — |
case-09 | pass→pass | 13,684 | 5,689 | -58% | 1 | 1 | 0% | 2,575 | 6,519 | +153% | 0 | 0 | — |
case-10 | fail→fail | 13,129 | 14,396 | +10% | 1 | 1 | 0% | 2,418 | 8,161 | +238% | 0 | 0 | — |
case-11 | fail→pass | 8,417 | 3,710 | -56% | 1 | 1 | 0% | 1,752 | 6,069 | +246% | 0 | 0 | — |
case-17 | fail→pass | 12,633 | 8,158 | -35% | 1 | 1 | 0% | 2,577 | 7,073 | +174% | 0 | 0 | — |
case-12 | fail→pass | 14,344 | 14,459 | +1% | 1 | 1 | 0% | 2,915 | 7,531 | +158% | 0 | 0 | — |
case-13 | fail→pass | 21,405 | 7,683 | -64% | 1 | 1 | 0% | 2,459 | 6,814 | +177% | 0 | 0 | — |
case-14 | pass→fail | 8,469 | 7,679 | -9% | 1 | 1 | 0% | 1,703 | 6,816 | +300% | 0 | 0 | — |
case-15 | pass→pass | 9,899 | 7,395 | -25% | 1 | 1 | 0% | 1,975 | 6,938 | +251% | 0 | 0 | — |
case-18 | fail→pass | 11,659 | 6,051 | -48% | 1 | 1 | 0% | 2,302 | 6,544 | +184% | 0 | 0 | — |
case-19 | pass→pass | 10,053 | 3,373 | -66% | 1 | 1 | 0% | 2,162 | 5,941 | +175% | 0 | 0 | — |
case-20 | fail→fail | 8,803 | 5,468 | -38% | 1 | 1 | 0% | 1,854 | 6,453 | +248% | 0 | 0 | — |
case-21 | pass→pass | 11,245 | 7,019 | -38% | 1 | 1 | 0% | 2,074 | 6,619 | +219% | 0 | 0 | — |
case-22 | fail→pass | 15,087 | 9,325 | -38% | 1 | 1 | 0% | 2,854 | 7,165 | +151% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 22 cases were attempted. The headline lift of +41 percentage points is the difference between those two pass rates over the 22 comparable cases. 1 case got worse with the skill loaded, and it is included in that figure.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
Other measured skills in the registry, with their headline benchmark lift.