Home▸Articles▸Deep Learning

Antibody Drug Design with ML

Monoclonal antibodies dominate the biologics pipeline, with over 100 approved therapeutics and hundreds in clinical trials. ML transforms antibody discovery across every stage: language model-guided CDR design for affinity maturation, developability scoring to prevent manufacturing liabilities (aggregation, viscosity, immunogenicity), and structure-based paratope modelling for epitope-specific campaigns.

mysimulator teamUpdated January 2026≈ 4 min read▶ Open the simulation

IgG antibodies use six complementarity-determining region (CDR) loops — three on the heavy chain (H1, H2, H3) and three on the light chain (L1, L2, L3) — to bind antigen. CDR-H3 is the most variable and typically contributes most to binding specificity and affinity. Antibody language models pre-trained on millions of antibody sequences (OAS database) capture natural CDR sequence distributions and can score the fitness of candidate sequences without experimental measurement.

Antibody Sequence Feature Encoding

import numpy as np
from typing import Optional


IMGT_CDR_POSITIONS = {
    "CDRH1": (26, 35, "heavy"),
    "CDRH2": (51, 57, "heavy"),
    "CDRH3": (93, 102, "heavy"),
    "CDRL1": (27, 32, "light"),
    "CDRL2": (50, 52, "light"),
    "CDRL3": (89, 97, "light"),
}

AA_VOCAB = list("ACDEFGHIKLMNPQRSTVWY")
AA_TO_IDX = {aa: i for i, aa in enumerate(AA_VOCAB)}


def encode_cdr_sequences(
    heavy_seq: str,
    light_seq: str,
    scheme: str = "IMGT",
    include_framework: bool = False,
) -> dict[str, np.ndarray]:
    """
    One-hot encode CDR sequences from a numbered antibody sequence pair.

    Args:
        heavy_seq: Full variable domain sequence (heavy chain), IMGT-numbered
        light_seq: Full variable domain sequence (light chain), IMGT-numbered
        scheme: Numbering scheme ('IMGT', 'Chothia', 'Kabat')
        include_framework: If True, also encode framework regions FW1–FW4

    Returns:
        Dict mapping CDR/FW loop name → one-hot encoded matrix (L × 20).
    """
    encodings = {}
    for loop_name, (start, end, chain) in IMGT_CDR_POSITIONS.items():
        seq = heavy_seq if chain == "heavy" else light_seq
        segment = seq[start:end + 1] if len(seq) > end else seq[start:]
        onehot = np.zeros((len(segment), 20), dtype=np.float32)
        for i, aa in enumerate(segment):
            j = AA_TO_IDX.get(aa, -1)
            if j >= 0:
                onehot[i, j] = 1.0
        encodings[loop_name] = onehot
    return encodings


def compute_cdr_physicochemical(cdr_sequence: str) -> dict[str, float]:
    """
    Compute physicochemical properties of a CDR sequence for developability scoring.

    Properties known to correlate with developability liabilities:
    - Net charge at pH 7.4
    - Hydrophobicity (GRAVY index)
    - Aromatic content (W/Y/F fraction)
    - CDR length (absolute and vs. germline mean)
    """
    pKa_residues = {"D": -1, "E": -1, "K": 1, "R": 1, "H": 0.1}
    hydrophobicity = {"A":1.8,"C":2.5,"D":-3.5,"E":-3.5,"F":2.8,"G":-0.4,"H":-3.2,
                      "I":4.5,"K":-3.9,"L":3.8,"M":1.9,"N":-3.5,"P":-1.6,"Q":-3.5,
                      "R":-4.5,"S":-0.8,"T":-0.7,"V":4.2,"W":-0.9,"Y":-1.3}
    aromatics = {"W", "Y", "F"}
    seq = cdr_sequence.upper()
    net_charge = sum(pKa_residues.get(aa, 0) for aa in seq)
    gravy = sum(hydrophobicity.get(aa, 0) for aa in seq) / max(len(seq), 1)
    frac_aromatic = sum(1 for aa in seq if aa in aromatics) / max(len(seq), 1)
    return {
        "net_charge": round(net_charge, 2),
        "gravy": round(gravy, 3),
        "frac_aromatic": round(frac_aromatic, 3),
        "length": len(seq),
    }

Developability Prediction

import numpy as np
from sklearn.ensemble import RandomForestClassifier
from sklearn.model_selection import cross_val_score


DEVELOPABILITY_LIABILITIES = {
    "aggregation": "Hydrophobic patches in CDR-H3, CDRH1 + CDRL1 proximity",
    "high_viscosity": "High net positive charge at pH 7.4 (net_charge_VH+VL > +5)",
    "immunogenicity": "Non-germline residues in frameworks, PTM-prone motifs",
    "thermal_instability": "Unpaired cysteines, buried charged residues",
    "polyreactivity": "High CDR-H3 hydrophobicity and length > 20 residues",
}


def predict_developability_risk(
    heavy_seq: str,
    light_seq: str,
) -> dict[str, str]:
    """
    Rule-based developability liability screen using biophysical heuristics.

    Based on Jain et al. (2017), Raybould et al. (2019) TAP guidelines,
    and Rabia et al. (2018) viscosity predictors.

    Returns:
        Dict of liability → risk level (low/medium/high) with rationale.
    """
    from typing import Any

    risks: dict[str, Any] = {}

    # Aggregation: high CDR-H3 hydrophobicity
    h3_start, h3_end = 93, 102
    cdrh3 = heavy_seq[h3_start:h3_end + 1] if len(heavy_seq) > h3_end else ""
    if cdrh3:
        h3_phys = compute_cdr_physicochemical(cdrh3)
        if h3_phys["gravy"] > 1.5:
            risks["aggregation"] = "high — CDRH3 GRAVY > 1.5"
        elif h3_phys["gravy"] > 0.5:
            risks["aggregation"] = "medium — CDRH3 GRAVY > 0.5"
        else:
            risks["aggregation"] = "low"

        risks["polyreactivity"] = (
            "high" if h3_phys["gravy"] > 1.0 and h3_phys["length"] > 15
            else "medium" if h3_phys["gravy"] > 0.5
            else "low"
        )

    # High viscosity: net positive charge in variable domains
    vh_charge = compute_cdr_physicochemical(heavy_seq)["net_charge"]
    vl_charge = compute_cdr_physicochemical(light_seq)["net_charge"]
    total_charge = vh_charge + vl_charge
    risks["high_viscosity"] = (
        "high" if total_charge > 5
        else "medium" if total_charge > 2
        else "low"
    )

    # PTM risk: motifs NxS/T (deamidation), DG (isomerisation), MW/WM oxidation
    PTM_MOTIFS = {"NG": "deamidation", "NS": "deamidation",
                  "DG": "isomerisation", "DS": "isomerisation",
                  "MW": "oxidation", "WM": "oxidation"}
    found_ptm = []
    for chain in (heavy_seq, light_seq):
        for i in range(len(chain) - 1):
            motif = chain[i:i+2]
            if motif in PTM_MOTIFS:
                found_ptm.append(PTM_MOTIFS[motif])
    risks["ptm_liability"] = "high" if found_ptm else "low"
    risks["ptm_motifs_found"] = list(set(found_ptm))

    return risks

Antibody ML Tool Landscape

Tool / ModelTaskArchitectureTraining DataAccess
AbLang-2CDR sequence scoring, fillingRoBERTa (antibody)OAS (2B sequences)Open source
AntiBERTySequence embeddingBERTOAS paired sequencesOpen source
ProteinMPNN-AbDesignCDR design given backboneMessage passing NNPDB antibody structuresOpen source
IgFoldAntibody structure predictionTransformer + IPAOAS + PDBOpen source
TAP (Therapeutic Ab Profiler)Developability scoringBiophysical rulesApproved Ab databaseWeb server

Key Takeaways

Try it live

Explore a related model hands-on: open Antibody Engineering Simulator and change its parameters while it runs. Everything stays in your browser.

▶ Open Antibody Engineering Simulator simulation

What did you find?

Add reproduction steps (optional)