Designing New Proteins with RFD4-Proteina¶
Background¶
Structure prediction takes a sequence and predicts its shape. This is the reverse. You describe what a protein has to do, and the model produces a protein that does it: backbone and sequence together, in about two seconds.
On Tuesday we used ESM-C to work with sequences. Today we generate sequence and 3D structure at the same time, under constraints we choose.
By the end of this session you will have made:
- A binder against PD-L1, a cancer drug target.
- A pocket built around a small molecule, designed from the molecule alone.
- An enzyme scaffold that holds catalytic residues in position.
- An antibody with redesigned loops against a tumour antigen.
- Proteins steered toward a fold you specify.
- A binder against a target you choose yourself.
- An independent check on one of your designs.
The worked examples are short. Most of the time goes into the cells marked Your turn, where you choose the design parameters and predict the outcome before running.
Run the setup cells, then work through the notebook in order. Each Your turn builds on the section above it.
Getting an API Key¶
- Go to build.nvidia.com/ipd/rfd4-proteina.
- Sign in, click Get API Key, and copy it. It starts with
nvapi-. - In Colab, click the key icon in the left sidebar, choose Add new secret, name it
NVIDIA_API_KEY, paste the value, and turn on Notebook access.
If you do not add it to the sidebar, the next cell will prompt you to paste it.
Note: no GPU is needed. The designing happens on NVIDIA's servers. If a cell reports that the deployment is busy or rate limited, it is retrying. This is normal when a whole class starts at once.
Setup¶
Run the next three cells.
# Your API key. Everything below needs it.
# Colab Secrets panel (🔑) if you set NVIDIA_API_KEY there, otherwise it asks.
import os
API_KEY = None
try:
from google.colab import userdata
API_KEY = userdata.get("NVIDIA_API_KEY")
except Exception:
pass
if not API_KEY:
import getpass
API_KEY = os.environ.get("NVIDIA_API_KEY") or getpass.getpass("NVIDIA API key (nvapi-...): ")
API_KEY = API_KEY.strip()
print(f"key loaded: {len(API_KEY)} chars, starts {API_KEY[:9]!r}")
if not API_KEY.startswith("nvapi-"):
print(" ^ that does not look like an invoke key - expect a 401/403 later.")
key loaded: 70 chars, starts 'nvapi-lHt'
%pip install --quiet py3Dmol requests gemmi
What You Are Actually Calling¶
Before the helper code, it is worth being clear about where the boundary lies between RFD4-Proteina and the convenience code in this notebook.
RFD4-Proteina is a web service. There is no Python library to install and no model on your machine. There is exactly one endpoint:
POST https://health.api.nvidia.com/v1/biology/ipd/rfd4-proteina/generative
Authorization: Bearer nvapi-...
Content-Type: application/json
You send a JSON body, you get a JSON body back. Everything in this notebook reduces to that one call.
The request body has six top-level fields, and each one maps onto a decision you make as a designer:
| Field | Required | What it decides |
|---|---|---|
task |
yes | which sampler path: generation, motif_scaffolding, enzyme_design, small_molecule_binder, antibody_design, partial_diffusion, inverse_folding |
input |
yes | the structure file and its conditioning: what is held fixed |
sampling |
no | how many designs, how many steps, which seed |
search |
no | the reward and the beam settings. Omit it and you get plain sampling |
metrics |
no | which metric families to compute on the result |
output |
no | whether to return structures and sequences |
So a minimal call is:
{
"task": "generation",
"input": {"format": "annotated_cif", "example_id": "demo",
"file": {"filename": "input.cif", "content": "<mmCIF text>"}},
"output": {"structure_format": "mmcif", "include_sequence": True}
}
Almost all of the difficulty in protein design with this model is in one field: the mmCIF
text inside input. That file carries the conditioning, and writing it correctly is the
subject of Parts B, D and J.
What the helper code is, and is not. The cells below write a module called
rfd4_demo.py. None of it is RFD4-Proteina. It is a thin wrapper that:
- assembles that JSON body and posts it, with retries when the server is busy;
- pulls the interesting fields out of a large nested response;
- writes and reads mmCIF conditioning files, which is fiddly text work;
- draws structures in the notebook.
The wrapper never hides the request. show_payload(...) prints any body before it is sent,
and you can replace any helper with a dictionary you write by hand. If you later use this
model from your own code, the only parts you need are the endpoint, the request schema
above, and whatever authors your input file.
The module is written in nine blocks so you can lift out the parts you need. Each cell appends to the same file, so run them in order.
Block 1: endpoint and imports¶
The service address and the two constants that name it. Nothing else in the module hardcodes a URL.
%%writefile rfd4_demo.py
"""
rfd4_demo.py — helpers for the RFD4-Proteina NIM teaching demo.
Everything here is thin on purpose: the point of the demo is that students
read the actual request bodies, so these helpers build inputs and summarize
responses but never hide the JSON.
Endpoint: POST {base}/biology/ipd/rfd4-proteina/generative
Auth: Authorization: Bearer <key>
"""
from __future__ import annotations
import json
import os
import time
from dataclasses import dataclass, field
import requests
DEFAULT_BASE = "https://health.api.nvidia.com/v1"
PATH = "/biology/ipd/rfd4-proteina/generative"
# --------------------------------------------------------------------------
# Input dialect 1: annotated_cif — a poly-glycine placeholder backbone.
#
# For `generation`, the model uses the input's TOKEN COUNT and any embedded
# conditions, not its coordinates: generation noises the structure away. So a
# straight-line poly-Gly chain of length L is a perfectly good way to say
# "design me L residues". This is how the NIM's own Example 1 does it.
# --------------------------------------------------------------------------
_ATOM_SITE_COLUMNS = [
"group_PDB", "type_symbol", "label_atom_id", "label_alt_id",
"label_comp_id", "label_asym_id", "label_entity_id", "label_seq_id",
"pdbx_PDB_ins_code", "auth_seq_id", "auth_comp_id", "auth_asym_id",
"auth_atom_id", "Cartn_x", "Cartn_y", "Cartn_z", "pdbx_PDB_model_num",
"id",
]
# (atom name, element, x-offset within the residue), residues spaced 3.8 A.
_BACKBONE = [("N", "N", 0.0), ("CA", "C", 1.5), ("C", "C", 3.0), ("O", "O", 3.6)]
_RESIDUE_STRIDE = 3.8
Overwriting rfd4_demo.py
Block 2: building inputs¶
polygly_cif and contig_cif author a design chain: a placeholder saying "make me
this many residues". annotated_cif_input and condition_spec_input wrap a file into
the input field of the request body. These two are the whole of the input dialect you
need until Part K.
%%writefile -a rfd4_demo.py
def polygly_cif(length: int, chain: str = "A") -> str:
"""An annotated mmCIF carrying `length` glycine residues in a line.
Use with input format 'annotated_cif' and task 'generation'. The chain is
a placeholder: its length is the design length, its coordinates are not
used.
"""
if length < 1:
raise ValueError("length must be >= 1")
lines = ["data_structure", "#", "loop_"]
lines += [f"_atom_site.{col} " for col in _ATOM_SITE_COLUMNS]
serial = 1
for res in range(1, length + 1):
base_x = (res - 1) * _RESIDUE_STRIDE
for atom, element, dx in _BACKBONE:
lines.append(
f"ATOM {element} {atom} . GLY {chain} 1 {res} . {res} GLY "
f"{chain} {atom} {base_x + dx:.1f} 0.0 0.0 1 {serial}"
)
serial += 1
lines.append("#")
return "\n".join(lines) + "\n"
# --------------------------------------------------------------------------
# Input dialect 2: condition_spec — a sampled contig.
#
# Instead of spelling out residues, declare an unindexed segment and let the
# model choose a length from a RANGE. One HETATM row stands in for the whole
# segment; `_rfd_design.sampled_contig` is what the sampler reads.
# This is the compact route to variable-length design.
# --------------------------------------------------------------------------
def contig_cif(min_len: int, max_len: int, chain: str = "A", name: str | None = None) -> str:
"""An annotated mmCIF declaring one unindexed segment of min_len-max_len.
Pass with input format 'condition_spec' and spec
{"input": <filename>, "input_as_annotated_cif": true}.
Set min_len == max_len to pin an exact length.
"""
if not (1 <= min_len <= max_len):
raise ValueError("need 1 <= min_len <= max_len")
contig = f"{min_len}-{max_len}"
fname = name or f"contig-{contig}.cif"
return f"""data_{fname}
#
_entry.id {fname}
#
_entity.id 0
_entity.type polymer
_entity.pdbx_description .
#
_entity_poly.entity_id 0
_entity_poly.type polypeptide(L)
_entity_poly.nstd_linkage no
_entity_poly.nstd_monomer yes
_entity_poly.pdbx_seq_one_letter_code X
_entity_poly.pdbx_seq_one_letter_code_can X
_entity_poly.pdbx_strand_id {chain}
_entity_poly.pdbx_target_identifier ?
#
_entity_poly_seq.entity_id 0
_entity_poly_seq.hetero n
_entity_poly_seq.mon_id {contig}
_entity_poly_seq.num 1
#
_chem_comp.id {contig}
_chem_comp.name {contig}
_chem_comp.type NON-POLYMER
_chem_comp.formula ?
_chem_comp.mon_nstd_parent_comp_id ?
_chem_comp.pdbx_synonyms ?
_chem_comp.formula_weight ?
#
_atom_site.group_PDB HETATM
_atom_site.type_symbol X
_atom_site.label_atom_id UNK
_atom_site.label_alt_id .
_atom_site.label_comp_id {contig}
_atom_site.label_asym_id {chain}
_atom_site.label_entity_id 0
_atom_site.label_seq_id 1
_atom_site.pdbx_PDB_ins_code .
_atom_site.auth_seq_id 1
_atom_site.auth_comp_id {contig}
_atom_site.auth_asym_id {chain}
_atom_site.auth_atom_id UNK
_atom_site.B_iso_or_equiv nan
_atom_site.occupancy 0.0
_atom_site.pdbx_formal_charge ?
_atom_site.mask_expsegmin_1_residue True
_atom_site.annotation_src-component_1_atom .
_atom_site.annotation_expsegmin_1_residue {min_len}
_atom_site.mask_coordinate_1_atom False
_atom_site.mask_sequence_1_residue False
_atom_site.mask_index_1_residue True
_atom_site.annotation_expsegmax_1_residue {max_len}
_atom_site.mask_expsegmax_1_residue True
_atom_site.condition_hbond_1_atom 0
_atom_site.condition_index-group_1_token 0
_atom_site.atom_array_index 0
_atom_site.Cartn_x nan
_atom_site.Cartn_y nan
_atom_site.Cartn_z nan
_atom_site.pdbx_PDB_model_num 1
_atom_site.id 1
#
_rfd_design.sampled_contig {contig}
#
_chem_comp_atom.comp_id {contig}
_chem_comp_atom.atom_id UNK
_chem_comp_atom.type_symbol X
_chem_comp_atom.charge 0
_chem_comp_atom.pdbx_aromatic_flag N
_chem_comp_atom.pdbx_leaving_atom_flag N
_chem_comp_atom.pdbx_model_Cartn_x_ideal nan
_chem_comp_atom.pdbx_model_Cartn_y_ideal nan
_chem_comp_atom.pdbx_model_Cartn_z_ideal nan
#
"""
# --------------------------------------------------------------------------
# Request builders
# --------------------------------------------------------------------------
def annotated_cif_input(cif_text: str, example_id: str = "design",
filename: str = "input.cif") -> dict:
return {
"format": "annotated_cif",
"example_id": example_id,
"file": {"filename": filename, "content": cif_text},
}
def condition_spec_input(cif_text: str, example_id: str = "design",
filename: str = "input.cif",
spec: dict | None = None) -> dict:
return {
"format": "condition_spec",
"example_id": example_id,
"spec": spec or {"input": filename, "input_as_annotated_cif": True},
"files": [{"filename": filename, "content": cif_text}],
}
# --------------------------------------------------------------------------
# Client
# --------------------------------------------------------------------------
Appending to rfd4_demo.py
Block 3: the client¶
Authentication, the POST itself, and retry with backoff on 429 (rate limited), 503 and
504 (no free GPU worker). RFD4Error carries the structured error body, which is worth
reading: the server validates conditioning before it spends GPU time, so a 422 usually
tells you exactly what is wrong with your input file.
%%writefile -a rfd4_demo.py
class RFD4Error(RuntimeError):
"""A non-200 from the NIM, with the structured error body attached."""
def __init__(self, status: int, body):
self.status = status
self.body = body
code = msg = "?"
if isinstance(body, dict) and isinstance(body.get("error"), dict):
code = body["error"].get("code", "?")
msg = body["error"].get("message", "?")
details = body["error"].get("details") or []
if details:
msg += " | " + " ; ".join(str(d) for d in details)
super().__init__(f"HTTP {status} [{code}] {msg}")
@dataclass
class RFD4Client:
api_key: str | None = None
base_url: str = DEFAULT_BASE
timeout: float = 900.0
max_retries: int = 4 # for 503 service_busy / 504, common in a full class
retry_backoff_s: float = 5.0 # doubles each attempt: 5, 10, 20, 40
verbose: bool = True
last_response: dict | None = field(default=None, repr=False)
last_elapsed_s: float | None = None
def __post_init__(self):
self.api_key = self.api_key or os.environ.get("NVIDIA_API_KEY", "")
self.base_url = self.base_url.rstrip("/")
hosted = "api.nvidia.com" in self.base_url
if hosted and not self.api_key:
raise ValueError(
"No API key. Set NVIDIA_API_KEY or pass api_key=... . "
"Get one at https://build.nvidia.com/ipd/rfd4-proteina")
if hosted and not self.api_key.startswith("nvapi-"):
print("warning: hosted NVIDIA keys normally start 'nvapi-'; "
"yours does not. If calls 401, that is why.")
@property
def url(self) -> str:
return self.base_url + PATH
def design(self, task: str, inp: dict, *, sampling: dict | None = None,
partial: dict | None = None, search: dict | None = None,
metrics: dict | None = None, output: dict | None = None,
runtime_metrics: bool = False, raw: bool = False) -> dict:
"""POST one design request. Raises RFD4Error on a non-200."""
payload: dict = {"task": task, "input": inp}
for key, value in (("sampling", sampling), ("partial", partial),
("search", search), ("metrics", metrics),
("output", output)):
if value is not None:
payload[key] = value
headers = {
"Authorization": f"Bearer {self.api_key}",
"Content-Type": "application/json",
"Accept": "application/json",
}
if runtime_metrics:
headers["X-Include-Runtime-Metrics"] = "true"
started = time.perf_counter()
attempt = 0
while True:
attempt += 1
response = requests.post(self.url, headers=headers, json=payload,
timeout=self.timeout)
try:
body = response.json()
except ValueError:
body = {"error": {"code": "non_json",
"message": response.text[:500]}}
# 503/504: no free GPU worker. 429: rate limited -- the hosted
# endpoint throttles back-to-back calls, which a whole class
# starting at once will absolutely trigger. Back off and retry.
if response.status_code in (429, 503, 504) and attempt <= self.max_retries:
wait = self.retry_backoff_s * (2 ** (attempt - 1))
if self.verbose:
why = ("rate limited" if response.status_code == 429
else "deployment busy")
print(f" [{response.status_code}] {why} — "
f"retry {attempt}/{self.max_retries} in {wait:.0f}s")
time.sleep(wait)
continue
break
self.last_elapsed_s = time.perf_counter() - started
if response.status_code == 401:
raise RFD4Error(401, {"error": {
"code": "unauthorized",
"message": "Your API key was rejected. Get one at "
"https://build.nvidia.com/ipd/rfd4-proteina (it starts "
"'nvapi-'). Note an NVCF *invoke* key is not the same as "
"an NGC key used to pull containers."}})
if response.status_code != 200:
raise RFD4Error(response.status_code, body)
self.last_response = body
return body if not raw else {"payload": payload, "response": body}
# --------------------------------------------------------------------------
# Reading responses
# --------------------------------------------------------------------------
Appending to rfd4_demo.py
Block 4: reading responses¶
A response is a large nested object. summarize prints the parts worth looking at,
metric_keys lists what was computed, metric_table lines up metrics across runs, and
cif_of extracts one design's structure. show_payload prints a request body with long
mmCIF blobs elided.
%%writefile -a rfd4_demo.py
def summarize(response: dict, show_sequence: bool = True) -> None:
"""Print the parts of a response worth looking at in a demo."""
model, runtime, sched = response["model"], response["runtime"], response["schedule"]
designs = response["designs"]
print(f"request_id {response['request_id']} task={response['task']}")
print(f"wall time {response['duration_ms'] / 1000:.1f} s "
f"for {len(designs)} design(s)")
print(f"backend {model['execution_backend']} dtype={model['dtype']} "
f"gpu={runtime.get('gpu')}")
print(f"oracle {model.get('oracle') or '[] (none mounted)'}")
print(f"schedule {sched['num_steps']} steps / "
f"{sched.get('trunk_passes')} trunk passes "
f"strategy={sched.get('recycle_strategy')}")
if response.get("search"):
s = response["search"]
print(f"search beam={s['beam_width']} branch={s['n_branch']} "
f"checkpoints={s['step_checkpoints']} rollouts={s['rollouts']} "
f"best_reward={s.get('best_reward')}")
print()
for d in designs:
diag = d["diagnostics"]
bits = [f"{d['design_id']:<28} origin={d.get('origin')}",
f"tokens={diag['num_tokens']}",
f"finite={diag['all_coordinates_finite']}"]
if d.get("search_reward") is not None:
bits.append(f"reward={d['search_reward']:.4f}")
if diag.get("input_ca_rmsd_angstrom") is not None:
bits.append(f"CA-RMSD={diag['input_ca_rmsd_angstrom']:.2f}A")
if diag.get("sequence_recovery") is not None:
bits.append(f"seq-rec={diag['sequence_recovery']:.2f}")
print(" " + " ".join(bits))
if show_sequence and d.get("sequence"):
seq = d["sequence"]
print(f" {seq[:80]}{'...' if len(seq) > 80 else ''} (len {len(seq)})")
if d.get("metrics"):
for k, v in sorted(d["metrics"].items()):
print(f" {k} = {v}")
if response.get("aggregated_metrics"):
print("\n aggregated:")
for row in response["aggregated_metrics"]:
print(f" {row['metric']:<45} {row['reduction']:<6} "
f"{row['value']} (n={row['n']})")
if response.get("warnings"):
print("\n WARNINGS <- always read these; a bad schedule still returns 200")
for w in response["warnings"]:
print(f" - {w}")
def metric_keys(response: dict) -> list[str]:
"""Every metric key present on any design. Use these in search weights."""
keys: set[str] = set()
for d in response["designs"]:
keys.update((d.get("metrics") or {}).keys())
return sorted(keys)
def save_structures(response: dict, outdir: str = "designs") -> list[str]:
"""Write each design's mmCIF to disk; returns the paths written."""
os.makedirs(outdir, exist_ok=True)
paths = []
for d in response["designs"]:
struct = d.get("structure")
if not struct:
continue
path = os.path.join(outdir, struct.get("filename") or f"{d['design_id']}.cif")
with open(path, "w") as fh:
fh.write(struct["content"])
paths.append(path)
return paths
def cif_of(response: dict, index: int = 0) -> str:
"""The annotated mmCIF text of one returned design, ready to feed back in."""
struct = response["designs"][index].get("structure")
if not struct:
raise ValueError("no structure on this design — "
"was output.structure_format set to 'none'?")
return struct["content"]
def show_payload(payload: dict, max_cif: int = 180) -> None:
"""Print a request body with long CIF blobs elided, so it stays readable."""
def trim(obj):
if isinstance(obj, dict):
return {k: (f"<{len(v)} chars of mmCIF>"
if k == "content" and isinstance(v, str) and len(v) > max_cif
else trim(v)) for k, v in obj.items()}
if isinstance(obj, list):
return [trim(v) for v in obj]
return obj
print(json.dumps(trim(payload), indent=2))
# --------------------------------------------------------------------------
# The example gallery
#
# NVIDIA publishes nine ready-made annotated mmCIF inputs — real conditioned
# design problems (a PD-L1 target with hotspots, a SAM ligand, a catalytic
# motif) already carrying their conditioning columns. These are the fastest
# route to interesting designs without running the authoring tools.
# --------------------------------------------------------------------------
Appending to rfd4_demo.py
Block 5: the example gallery¶
Downloads and caches NVIDIA's nine published inputs.
%%writefile -a rfd4_demo.py
HF_BASE = ("https://huggingface.co/nvidia/RFD4-Proteina-Diffusers"
"/resolve/main/examples")
# id -> (pretty label, NIM task, one-line description)
# NOTE: the repo's examples.json says "unconditional" for two of these; that is
# not a NIM task enum. Both map to `generation`, which is the sampler mode for
# "from noise, honouring whatever conditions the input carries".
GALLERY = {
"unconditional": ("Unconditional protein", "generation",
"250 residues from noise, nothing held"),
"protein_binder": ("Protein binder · PD-L1", "generation",
"PD-L1 held fixed with hotspots; grows a 50-120 residue binder"),
"small_molecule_binder": ("Small-molecule binder · SAM", "small_molecule_binder",
"a ligand and nothing else; builds a 150-residue pocket around it"),
"enzyme_design": ("Enzyme · catalytic motif", "enzyme_design",
"catalytic side-chain atoms; model places them in a 180-residue scaffold"),
"motif_scaffolding": ("Motif scaffolding · 1QJG", "motif_scaffolding",
"three motif residues at fixed positions, linkers designed"),
"antibody_design": ("Antibody · HER2 CDR H3", "antibody_design",
"framework and antigen fixed; CDR H3 redesigned"),
"nanobody": ("VHH · PD-L1 CDRs", "antibody_design",
"single-domain framework fixed; all CDRs redesigned"),
"inverse_folding": ("Inverse folding · 2KL8", "inverse_folding",
"backbone held; sequence resampled"),
"partial_diffusion": ("Partial diffusion · 2KL8", "partial_diffusion",
"restart the trajectory partway to diversify a structure"),
}
def fetch_example(example_id: str, cache_dir: str = "examples") -> str:
"""Download (and cache) one gallery input.cif. Returns the mmCIF text."""
import urllib.request
if example_id not in GALLERY:
raise KeyError(f"{example_id!r} not in gallery; try: {list(GALLERY)}")
os.makedirs(cache_dir, exist_ok=True)
path = os.path.join(cache_dir, f"{example_id}.cif")
if not os.path.exists(path):
url = f"{HF_BASE}/{example_id}/input.cif"
with urllib.request.urlopen(url, timeout=120) as response:
data = response.read()
with open(path, "wb") as fh:
fh.write(data)
with open(path) as fh:
return fh.read()
def fetch_gallery(cache_dir: str = "examples") -> dict:
"""Download every gallery input. Returns {id: mmCIF text}."""
out = {}
for example_id, (label, task, _) in GALLERY.items():
try:
out[example_id] = fetch_example(example_id, cache_dir)
print(f" ok {label:<30} task={task}")
except Exception as exc: # noqa: BLE001
print(f" FAIL {label:<30} {type(exc).__name__}: {exc}")
return out
Appending to rfd4_demo.py
Block 6: inspecting a conditioning file¶
parse_atom_site reads the _atom_site table by column name, because the gallery files
list their columns in different orders. describe_cif reports what conditioning a file
actually carries and how many atoms are flagged, which is how you check an input before
sending it.
%%writefile -a rfd4_demo.py
def parse_atom_site(cif_text: str) -> tuple[list[str], list[dict]]:
"""Return (columns, rows) for _atom_site, keyed by column name.
Handles both mmCIF dialects: the usual `loop_` table, and the single-row
key-value form that contig_cif() emits (one `_atom_site.col value` per
line, no ATOM record at all).
"""
columns = re.findall(r"_atom_site\.(\S+)", cif_text)
rows = []
for line in cif_text.split("\n"):
if line.startswith(("ATOM ", "HETATM ")):
fields = line.split()
if len(fields) == len(columns):
rows.append(dict(zip(columns, fields)))
if not rows:
# single-row form: `_atom_site.mask_index_1_residue True`
kv = {}
for line in cif_text.split("\n"):
match = re.match(r"_atom_site\.(\S+)\s+(\S.*?)\s*$", line)
if match:
kv[match.group(1)] = match.group(2)
if kv:
rows = [kv]
return columns, rows
def describe_cif(cif_text: str, label: str = "") -> dict:
"""What conditioning does this input actually carry?
Unlike the notebook's regex version, this reads values, not just column
names — so it can tell you how many atoms are actually flagged.
"""
columns, rows = parse_atom_site(cif_text)
contig = re.search(r"_rfd_design\.sampled_contig\s+(.+)", cif_text)
atom = sum(1 for r in rows if r["group_PDB"] == "ATOM")
het = sum(1 for r in rows if r["group_PDB"] == "HETATM")
chains = sorted({r.get("auth_asym_id", "?") for r in rows})
flags = {}
for col in columns:
if col.startswith(("mask_", "condition_")):
n_true = sum(1 for r in rows
if r[col] not in ("False", "0", ".", "?"))
flags[col] = (n_true, len(rows))
if label:
print(f"=== {label} ===")
print(f" atoms {atom} ATOM / {het} HETATM chains={chains}")
print(f" contig {contig.group(1).strip() if contig else '(none)'}")
seg = [r for r in rows if r.get("mask_expsegmin_1_residue") == "True"]
if seg:
ranges = [(r.get("annotation_expsegmin_1_residue"),
r.get("annotation_expsegmax_1_residue")) for r in seg]
print(f" design segs {len(seg)} ranges={ranges}")
for col, (n, total) in sorted(flags.items()):
short = (col.replace("_1_residue", "").replace("_1_atom", "")
.replace("_1_token", ""))
print(f" {short:<22} {n:>5} / {total} set")
return {"columns": columns, "n_rows": len(rows),
"contig": contig.group(1).strip() if contig else None,
"flags": flags}
# --------------------------------------------------------------------------
# 3D viewing
# --------------------------------------------------------------------------
Appending to rfd4_demo.py
Block 7: viewing structures¶
view shows one structure, view_design highlights a named chain, and view_conditioned
colours by what was held fixed versus designed, reading that split out of the returned
file rather than being told.
%%writefile -a rfd4_demo.py
def view(cif_text: str, width: int = 700, height: int = 480, spin: bool = False,
style: str = "spectrum"):
"""Show one structure. `style`: 'spectrum' or 'bychain'."""
try:
import py3Dmol
except ImportError:
print("pip install py3Dmol")
return None
v = py3Dmol.view(width=width, height=height)
v.addModel(cif_text, "cif")
if style == "bychain":
v.setStyle({"cartoon": {"colorscheme": "chainHetatm"}})
else:
v.setStyle({"cartoon": {"color": "spectrum"}})
# always show ligands/hetero atoms as sticks — otherwise the SAM and the
# catalytic side chains are invisible, which is the whole point of them
v.addStyle({"hetflag": True}, {"stick": {"radius": 0.22, "colorscheme": "greenCarbon"}})
v.zoomTo()
if spin:
v.spin(True)
return v.show()
def view_design(cif_text: str, designed_chain: str | None = None,
width: int = 700, height: int = 480):
"""Show a complex with the designed chain highlighted against a grey target.
Pass the chain you designed; everything else renders grey, so the new
protein stands out against what was held fixed.
"""
try:
import py3Dmol
except ImportError:
print("pip install py3Dmol")
return None
v = py3Dmol.view(width=width, height=height)
v.addModel(cif_text, "cif")
v.setStyle({"cartoon": {"color": "lightgrey"}})
if designed_chain:
v.setStyle({"chain": designed_chain},
{"cartoon": {"color": "orange"}})
v.addStyle({"hetflag": True},
{"stick": {"radius": 0.25, "colorscheme": "greenCarbon"}})
v.zoomTo()
return v.show()
def chains_in(cif_text: str) -> list[str]:
"""Chain ids present in a structure, so you know what to highlight."""
import re
ids = re.findall(r"^(?:ATOM|HETATM)\s+\S+\s+\S+\s+\S+\s+\S+\s+(\S+)",
cif_text, re.M)
return sorted(set(ids))
# --------------------------------------------------------------------------
# Small comparison helper (added for the teaching exercises).
# Reads only response["designs"][i]["metrics"], the same place summarize() reads.
# --------------------------------------------------------------------------
def metric_table(runs: dict, keys: list, index: int = 0) -> None:
"""Line up a few metrics across named runs. `runs` maps label -> response.
Prints one row per run using design[index]'s metrics, so you can compare
'what did pushing this reward actually do?' at a glance.
"""
short = [k.split(".")[-1] for k in keys]
rows = []
for label, resp in runs.items():
designs = resp.get("designs") or []
metrics = (designs[index].get("metrics") if len(designs) > index else {}) or {}
vals = []
for k in keys:
v = metrics.get(k)
vals.append(f"{v:.3f}" if isinstance(v, (int, float)) else "--")
rows.append((label, vals))
label_w = max([len("run")] + [len(r[0]) for r in rows]) + 2
col_w = [max(len(short[j]), *(len(r[1][j]) for r in rows)) + 2 for j in range(len(keys))]
header = f"{'run':<{label_w}}" + "".join(f"{short[j]:>{col_w[j]}}" for j in range(len(keys)))
print(header)
print("-" * len(header))
for label, vals in rows:
print(f"{label:<{label_w}}" + "".join(f"{vals[j]:>{col_w[j]}}" for j in range(len(keys))))
# ==========================================================================
# AUTHORING YOUR OWN INPUTS
#
# polygly_cif/contig_cif above author *design* chains. These author the other
# half: a frozen target with hotspots, and a motif contig from a real PDB or
# AlphaFold structure. Both work by templating off NVIDIA's own gallery files,
# so the _chem_comp* dictionary blocks are byte-identical to what the NIM
# already accepts -- only the conditioning is ours.
#
# Schema notes, read off the gallery files and confirmed against the live API:
# * hotspots are flagged PER ATOM (mask_hotspot_1_atom), not per residue
# * mask_index_1_residue True -> sequence position pinned
# False -> the model chooses placement (enzyme_design)
# * the interleaved contig dialect ALSO needs a _struct_conn loop bonding
# consecutive tokens, or the NIM 422s with
# [index_group_molecule_must_be_connected]. protein_binder and
# enzyme_design carry no _struct_conn and don't need one.
# * column ORDER differs between gallery files -- always parse by name
# ==========================================================================
Appending to rfd4_demo.py
Block 8: authoring your own inputs¶
The largest block, and the one specific to Part K. author_target writes a binder problem
from any structure: target chain frozen, epitope atoms flagged as hotspots.
author_motif writes a contig problem: keep these residues, design between them.
Both work by templating off a gallery file, so the chemical component dictionary blocks are identical to ones the server already accepts and only the conditioning is new. The underscore-prefixed functions are the mmCIF mechanics.
%%writefile -a rfd4_demo.py
import re
import urllib.request
BINDER_COLUMNS = ['group_PDB', 'type_symbol', 'label_atom_id', 'label_alt_id', 'label_comp_id', 'label_asym_id', 'label_entity_id', 'label_seq_id', 'pdbx_PDB_ins_code', 'auth_seq_id', 'auth_comp_id', 'auth_asym_id', 'auth_atom_id', 'id', 'B_iso_or_equiv', 'occupancy', 'pdbx_formal_charge', 'annotation_expsegmax_1_residue', 'annotation_expsegmin_1_residue', 'mask_coordinate_1_atom', 'mask_expsegmax_1_residue', 'mask_expsegmin_1_residue', 'mask_hotspot_1_atom', 'mask_sequence_1_residue', 'atom_array_index', 'Cartn_x', 'Cartn_y', 'Cartn_z', 'pdbx_PDB_model_num']
MOTIF_COLUMNS = ['group_PDB', 'type_symbol', 'label_atom_id', 'label_alt_id', 'label_comp_id', 'label_asym_id', 'label_entity_id', 'label_seq_id', 'pdbx_PDB_ins_code', 'auth_seq_id', 'auth_comp_id', 'auth_asym_id', 'auth_atom_id', 'B_iso_or_equiv', 'occupancy', 'pdbx_formal_charge', 'annotation_src-component_1_atom', 'annotation_expsegmin_1_residue', 'condition_hbond_1_atom', 'annotation_expsegmax_1_residue', 'mask_index_1_residue', 'mask_expsegmax_1_residue', 'condition_index-group_1_token', 'mask_expsegmin_1_residue', 'mask_sequence_1_residue', 'mask_coordinate_1_atom', 'atom_array_index', 'Cartn_x', 'Cartn_y', 'Cartn_z', 'pdbx_PDB_model_num', 'id']
BACKBONE_ATOMS = {"N", "CA", "C", "O", "OXT"}
THREE_TO_ONE = {
"ALA": "A", "ARG": "R", "ASN": "N", "ASP": "D", "CYS": "C", "GLN": "Q",
"GLU": "E", "GLY": "G", "HIS": "H", "ILE": "I", "LEU": "L", "LYS": "K",
"MET": "M", "PHE": "F", "PRO": "P", "SER": "S", "THR": "T", "TRP": "W",
"TYR": "Y", "VAL": "V",
}
# --------------------------------------------------------------------------
# Reading structures
# --------------------------------------------------------------------------
def fetch_structure(source: str, cache_dir: str = "structures") -> str:
"""A 4-character PDB id, a UniProt accession (AlphaFold), or a local path.
Returns a local path to an mmCIF file.
"""
if os.path.exists(source):
return source
os.makedirs(cache_dir, exist_ok=True)
if re.fullmatch(r"[0-9][A-Za-z0-9]{3}", source):
path = os.path.join(cache_dir, f"{source.lower()}.cif")
url = f"https://files.rcsb.org/download/{source.upper()}.cif"
else: # treat as a UniProt accession
path = os.path.join(cache_dir, f"AF-{source}.cif")
# Ask the API for the current file; the model version number changes
# (v4 -> v6 so far), so hardcoding one eventually 404s.
import json as _json
api = f"https://alphafold.ebi.ac.uk/api/prediction/{source.upper()}"
with urllib.request.urlopen(api, timeout=60) as response:
entries = _json.load(response)
if not entries:
raise ValueError(f"no AlphaFold model for {source!r}")
url = entries[0]["cifUrl"]
if not os.path.exists(path):
with urllib.request.urlopen(url, timeout=120) as response:
data = response.read()
with open(path, "wb") as fh:
fh.write(data)
return path
def _load_chain(structure_path: str, chain_id: str):
"""Return [(resnum, resname, [(atom_name, element, x, y, z), ...]), ...]."""
import gemmi
st = gemmi.read_structure(structure_path)
st.setup_entities()
st.remove_alternative_conformations()
st.remove_hydrogens()
model = st[0]
for chain in model:
if chain.name != chain_id:
continue
residues = []
for res in chain:
if res.name not in THREE_TO_ONE:
continue # protein residues only
atoms = [(a.name, a.element.name, a.pos.x, a.pos.y, a.pos.z)
for a in res]
residues.append((res.seqid.num, res.name, atoms))
if not residues:
raise ValueError(f"chain {chain_id!r} has no standard residues")
return residues
present = sorted({c.name for c in model})
raise ValueError(f"chain {chain_id!r} not found; chains present: {present}")
# --------------------------------------------------------------------------
# Template surgery: reuse the gallery's chemical dictionary blocks
# --------------------------------------------------------------------------
def _split_template(template_cif: str) -> dict:
"""Pull the reusable dictionary blocks out of a gallery input."""
blocks = {}
for name in ("_chem_comp.", "_chem_comp_bond.", "_chem_comp_atom."):
key = name.strip("._")
# a loop_ ... block running until the next lone '#'
pattern = re.compile(
r"(loop_\s*\n(?:" + re.escape(name) + r"\w+\s*\n)+.*?)(?=\n#)",
re.S)
match = pattern.search(template_cif)
blocks[key] = match.group(1).rstrip() if match else ""
return blocks
def _strip_contig_rows(block: str, prefix_cols: int = 1) -> str:
"""Drop the template's own 'MIN-MAX' pseudo-component rows."""
out = []
for line in block.split("\n"):
if re.match(r"^\d+-\d+\s", line.strip()):
continue
out.append(line)
return "\n".join(out)
def _fmt(value) -> str:
if isinstance(value, bool):
return "True" if value else "False"
if isinstance(value, float):
return f"{value:.3f}"
return str(value)
def _atom_site_loop(columns: list[str], rows: list[dict]) -> str:
lines = ["loop_"] + [f"_atom_site.{c} " for c in columns]
widths = {c: max([len(c)] + [len(_fmt(r[c])) for r in rows])
for c in columns}
for row in rows:
lines.append(" ".join(_fmt(row[c]).ljust(widths[c]) for c in columns).rstrip())
return "\n".join(lines)
# --------------------------------------------------------------------------
# 1. author_target — a frozen target + hotspots, ready for task="generation"
# --------------------------------------------------------------------------
def author_target(structure: str,
freeze_chain: str,
hotspots: list[int],
binder_length: tuple[int, int] = (50, 120),
template_cif: str | None = None,
hotspot_atoms: str = "sidechain",
design_chain: str = "B") -> str:
"""Author a binder-design input: target chain frozen, epitope marked.
structure PDB id, UniProt accession, or path to a local mmCIF/PDB
freeze_chain the chain to hold fixed (your target)
hotspots author residue numbers of the epitope ("bind here")
binder_length (min, max) residues for the binder the model will grow
template_cif gallery protein_binder/input.cif text; fetched if omitted
hotspot_atoms 'sidechain' (gallery-style) or 'all'
Use with annotated_cif_input(...) and task="generation".
"""
lo, hi = binder_length
if not (1 <= lo <= hi):
raise ValueError("need 1 <= min <= max for binder_length")
if not hotspots:
raise ValueError("give at least one hotspot residue; without an "
"epitope the model will bind wherever it likes")
if template_cif is None:
with urllib.request.urlopen(
f"{HF_BASE}/protein_binder/input.cif", timeout=120) as r:
template_cif = r.read().decode()
path = fetch_structure(structure)
residues = _load_chain(path, freeze_chain)
present = {num for num, _, _ in residues}
missing = [h for h in hotspots if h not in present]
if missing:
raise ValueError(
f"hotspot residues not in chain {freeze_chain}: {missing}. "
f"Chain spans {min(present)}-{max(present)}.")
contig = f"{lo}-{hi}"
hot = set(hotspots)
rows, serial = [], 0
for resnum, resname, atoms in residues:
for atom_name, element, x, y, z in atoms:
is_hot = (resnum in hot and
(hotspot_atoms == "all" or atom_name not in BACKBONE_ATOMS))
rows.append({
"group_PDB": "ATOM", "type_symbol": element,
"label_atom_id": atom_name, "label_alt_id": ".",
"label_comp_id": resname, "label_asym_id": freeze_chain,
"label_entity_id": 1, "label_seq_id": resnum,
"pdbx_PDB_ins_code": ".", "auth_seq_id": resnum,
"auth_comp_id": resname, "auth_asym_id": freeze_chain,
"auth_atom_id": atom_name, "id": serial,
"B_iso_or_equiv": 0.0, "occupancy": 1.0,
"pdbx_formal_charge": "?",
"annotation_expsegmax_1_residue": -1,
"annotation_expsegmin_1_residue": -1,
"mask_coordinate_1_atom": True,
"mask_expsegmax_1_residue": False,
"mask_expsegmin_1_residue": False,
"mask_hotspot_1_atom": is_hot,
"mask_sequence_1_residue": True,
"atom_array_index": serial,
"Cartn_x": x, "Cartn_y": y, "Cartn_z": z,
"pdbx_PDB_model_num": 1,
})
serial += 1
n_hot_atoms = sum(1 for r in rows if r["mask_hotspot_1_atom"])
if n_hot_atoms == 0:
raise ValueError(
"no hotspot atoms were marked — your hotspot residues are "
"glycines or backbone-only. Pass hotspot_atoms='all'.")
# the design chain: one HETATM row carrying the length range
rows.append({
"group_PDB": "HETATM", "type_symbol": "X", "label_atom_id": "UNK",
"label_alt_id": ".", "label_comp_id": contig,
"label_asym_id": design_chain, "label_entity_id": 2,
"label_seq_id": 1, "pdbx_PDB_ins_code": ".", "auth_seq_id": 1,
"auth_comp_id": contig, "auth_asym_id": design_chain,
"auth_atom_id": "UNK", "id": serial,
"B_iso_or_equiv": "nan", "occupancy": 0.0, "pdbx_formal_charge": "?",
"annotation_expsegmax_1_residue": hi,
"annotation_expsegmin_1_residue": lo,
"mask_coordinate_1_atom": False,
"mask_expsegmax_1_residue": True,
"mask_expsegmin_1_residue": True,
"mask_hotspot_1_atom": False,
"mask_sequence_1_residue": False,
"atom_array_index": serial,
"Cartn_x": "nan", "Cartn_y": "nan", "Cartn_z": "nan",
"pdbx_PDB_model_num": 1,
})
seq = "".join(THREE_TO_ONE[r[1]] for r in residues)
blocks = _split_template(template_cif)
chem_comp = _strip_contig_rows(blocks["chem_comp"])
chem_comp_atom = _strip_contig_rows(blocks["chem_comp_atom"])
# re-insert OUR contig pseudo-component
chem_comp = chem_comp.replace(
"_chem_comp.formula_weight \n",
f"_chem_comp.formula_weight \n{contig} {contig} NON-POLYMER ? ? ? ?\n", 1)
chem_comp_atom += f"\n{contig} UNK X 0 N N nan nan nan"
poly_seq = "\n".join(f"1 n {name:<6} {num}" for num, name, _ in residues)
seq_wrapped = "\n".join(seq[i:i + 80] for i in range(0, len(seq), 80))
return f"""data_authored_target
#
_entry.id authored_target
#
loop_
_entity.id
_entity.type
_entity.pdbx_description
1 polymer .
2 polymer .
#
loop_
_entity_poly.entity_id
_entity_poly.type
_entity_poly.nstd_linkage
_entity_poly.nstd_monomer
_entity_poly.pdbx_seq_one_letter_code
_entity_poly.pdbx_seq_one_letter_code_can
_entity_poly.pdbx_strand_id
_entity_poly.pdbx_target_identifier
1 polypeptide(L) no no
;{seq_wrapped}
;
;{seq_wrapped}
;
{freeze_chain} ?
2 polypeptide(L) no yes X X {design_chain} ?
#
loop_
_entity_poly_seq.entity_id
_entity_poly_seq.hetero
_entity_poly_seq.mon_id
_entity_poly_seq.num
{poly_seq}
2 n {contig} 1
#
{chem_comp}
#
{blocks["chem_comp_bond"]}
#
{_atom_site_loop(BINDER_COLUMNS, rows)}
#
{chem_comp_atom}
#
"""
# --------------------------------------------------------------------------
# 2. author_motif — contig-style motif scaffolding from a real structure
# --------------------------------------------------------------------------
def author_motif(structure: str,
chain: str,
keep: list[int],
linkers: list[tuple[int, int]] | None = None,
template_cif: str | None = None,
pin_index: bool = True,
sidechain_only: bool = False) -> str:
"""Author a motif-scaffolding input from residues of a real structure.
keep author residue numbers to hold in exact geometry
linkers (min, max) ranges BETWEEN and AROUND the kept residues.
len(linkers) must be len(keep) + 1. Defaults to 10-20 at the
ends and 15-30 between.
pin_index True -> motif_scaffolding behaviour (sequence positions fixed)
False -> enzyme_design behaviour (model places the motif)
sidechain_only
keep only side-chain atoms, as enzyme_design does
Returns an annotated mmCIF carrying _rfd_design.sampled_contig.
Use with task="motif_scaffolding" (or "enzyme_design" if pin_index=False).
"""
keep = list(keep) # order matters: it IS the contig order
if not keep:
raise ValueError("keep at least one motif residue")
if linkers is None:
linkers = ([(10, 20)] + [(15, 30)] * (len(keep) - 1) + [(10, 20)])
if len(linkers) != len(keep) + 1:
raise ValueError(
f"need {len(keep) + 1} linker ranges for {len(keep)} kept "
f"residues (one before, one after, one between each pair); "
f"got {len(linkers)}")
if template_cif is None:
with urllib.request.urlopen(
f"{HF_BASE}/motif_scaffolding/input.cif", timeout=120) as r:
template_cif = r.read().decode()
path = fetch_structure(structure)
residues = {num: (name, atoms) for num, name, atoms in _load_chain(path, chain)}
missing = [k for k in keep if k not in residues]
if missing:
raise ValueError(f"motif residues not in chain {chain}: {missing}")
# contig string, alternating designed segments and kept residues
parts = []
for i, (lo, hi) in enumerate(linkers):
parts.append(f"{lo}-{hi}")
if i < len(keep):
parts.append(f"{chain}{keep[i]}")
contig = ",".join(parts)
rows, serial, label_seq, group = [], 0, 0, 0
comps = set()
for i, (lo, hi) in enumerate(linkers):
seg = f"{lo}-{hi}"
comps.add(seg)
rows.append({
"group_PDB": "HETATM", "type_symbol": "X", "label_atom_id": "UNK",
"label_alt_id": ".", "label_comp_id": seg, "label_asym_id": chain,
"label_entity_id": 0, "label_seq_id": label_seq,
"pdbx_PDB_ins_code": ".", "auth_seq_id": label_seq,
"auth_comp_id": seg, "auth_asym_id": chain, "auth_atom_id": "UNK",
"B_iso_or_equiv": "nan", "occupancy": 0.0,
"pdbx_formal_charge": "?",
"annotation_src-component_1_atom": ".",
"annotation_expsegmin_1_residue": lo,
"condition_hbond_1_atom": 0,
"annotation_expsegmax_1_residue": hi,
"mask_index_1_residue": True,
"mask_expsegmax_1_residue": True,
"condition_index-group_1_token": 0,
"mask_expsegmin_1_residue": True,
"mask_sequence_1_residue": False,
"mask_coordinate_1_atom": False,
"atom_array_index": serial,
"Cartn_x": "nan", "Cartn_y": "nan", "Cartn_z": "nan",
"pdbx_PDB_model_num": 1, "id": serial + 1,
})
serial += 1
label_seq += 1
if i < len(keep):
resnum = keep[i]
resname, atoms = residues[resnum]
comps.add(resname)
group += 1
for atom_name, element, x, y, z in atoms:
if sidechain_only and atom_name in BACKBONE_ATOMS:
continue
rows.append({
"group_PDB": "ATOM", "type_symbol": element,
"label_atom_id": atom_name, "label_alt_id": ".",
"label_comp_id": resname, "label_asym_id": chain,
"label_entity_id": 0, "label_seq_id": label_seq,
"pdbx_PDB_ins_code": ".", "auth_seq_id": label_seq,
"auth_comp_id": resname, "auth_asym_id": chain,
"auth_atom_id": atom_name,
"B_iso_or_equiv": 0.0, "occupancy": 1.0,
"pdbx_formal_charge": "?",
"annotation_src-component_1_atom": f"{chain}{resnum}",
"annotation_expsegmin_1_residue": -1,
"condition_hbond_1_atom": 0,
"annotation_expsegmax_1_residue": -1,
"mask_index_1_residue": pin_index,
"mask_expsegmax_1_residue": False,
# grouping tokens is how the unindexed (enzyme_design)
# dialect ties a motif's atoms together. With the index
# pinned, the gallery leaves this at 0.
"condition_index-group_1_token": 0 if pin_index else group,
"mask_expsegmin_1_residue": False,
"mask_sequence_1_residue": True,
"mask_coordinate_1_atom": True,
"atom_array_index": serial,
"Cartn_x": x, "Cartn_y": y, "Cartn_z": z,
"pdbx_PDB_model_num": 1, "id": serial + 1,
})
serial += 1
label_seq += 1
blocks = _split_template(template_cif)
chem_comp = _strip_contig_rows(blocks["chem_comp"])
chem_comp_atom = _strip_contig_rows(blocks["chem_comp_atom"])
extra = "".join(f"{c} {c} NON-POLYMER ? ? ? ?\n"
for c in sorted(c for c in comps if "-" in c))
chem_comp = chem_comp.replace(
"_chem_comp.formula_weight \n",
f"_chem_comp.formula_weight \n{extra}", 1)
chem_comp_atom += "".join(f"\n{c} UNK X 0 N N nan nan nan"
for c in sorted(c for c in comps if "-" in c))
# ---- _struct_conn -------------------------------------------------
# The interleaved contig dialect must declare the covalent bonds joining
# consecutive tokens, or the NIM rejects the file with
# [index_group_molecule_must_be_connected] ... no path between them
# in the bond graph though they are one molecule
# A designed segment is one UNK token, so the links are C->UNK and UNK->N.
# protein_binder and enzyme_design carry no _struct_conn: the binder's two
# chains are separate molecules, and the unindexed enzyme tokens are not a
# connected chain. Only the pinned contig needs this.
tokens = []
for r in rows:
if not tokens or tokens[-1][0] != r["label_seq_id"]:
tokens.append((r["label_seq_id"], r["label_comp_id"],
"-" in r["label_comp_id"]))
conns, cid = [], 0
for (s1, c1, seg1), (s2, c2, seg2) in zip(tokens, tokens[1:]):
a1 = "UNK" if seg1 else "C"
a2 = "UNK" if seg2 else "N"
cid += 1
conns.append(f"{cid} covale sing {chain} {chain} {c1:<6} {c2:<6} "
f"{s1} {s2} {a1:<4} {a2:<4} . . 1 1")
struct_conn = "\n".join([
"loop_",
"_struct_conn.id ",
"_struct_conn.conn_type_id ",
"_struct_conn.pdbx_value_order ",
"_struct_conn.ptnr1_label_asym_id ",
"_struct_conn.ptnr2_label_asym_id ",
"_struct_conn.ptnr1_label_comp_id ",
"_struct_conn.ptnr2_label_comp_id ",
"_struct_conn.ptnr1_label_seq_id ",
"_struct_conn.ptnr2_label_seq_id ",
"_struct_conn.ptnr1_label_atom_id ",
"_struct_conn.ptnr2_label_atom_id ",
"_struct_conn.pdbx_ptnr1_PDB_ins_code ",
"_struct_conn.pdbx_ptnr2_PDB_ins_code ",
"_struct_conn.ptnr1_transformation_id ",
"_struct_conn.ptnr2_transformation_id ",
] + conns)
# one token per label_seq_id: X for a designed segment, the AA letter
# for a kept motif residue. Gallery 1QJG reads "XNXYXDX".
tokens, seen = [], set()
for r in rows:
if r["label_seq_id"] in seen:
continue
seen.add(r["label_seq_id"])
comp = r["label_comp_id"]
tokens.append("X" if "-" in comp else THREE_TO_ONE.get(comp, "X"))
one_letter = "".join(tokens)
poly_seq = "\n".join(
f"0 n {r['label_comp_id']:<8} {r['label_seq_id']}"
for i, r in enumerate(rows)
if i == 0 or rows[i - 1]["label_seq_id"] != r["label_seq_id"])
return f"""data_authored_motif
#
_entry.id authored_motif
#
loop_
_entity.id
_entity.type
_entity.pdbx_description
0 polymer .
#
_entity_poly.entity_id 0
_entity_poly.type polypeptide(L)
_entity_poly.nstd_linkage no
_entity_poly.nstd_monomer yes
_entity_poly.pdbx_seq_one_letter_code {one_letter}
_entity_poly.pdbx_seq_one_letter_code_can {one_letter}
_entity_poly.pdbx_strand_id {chain}
_entity_poly.pdbx_target_identifier ?
#
loop_
_entity_poly_seq.entity_id
_entity_poly_seq.hetero
_entity_poly_seq.mon_id
_entity_poly_seq.num
{poly_seq}
#
{chem_comp}
#
{struct_conn}
#
{blocks["chem_comp_bond"]}
#
_rfd_design.sampled_contig {contig}
#
{_atom_site_loop(MOTIF_COLUMNS, rows)}
#
{chem_comp_atom}
#
"""
# ==========================================================================
# INDEPENDENT REFOLDING -- the check that matters
#
# Every metric the NIM returns is computed on the model's OWN predicted
# structure. That is a closed loop: the thing being graded wrote the answer
# sheet. The standard break in that loop is to take the designed SEQUENCE
# ONLY and fold it with an unrelated model, then ask whether the two agree
# (pLDDT, and self-consistency CA-RMSD after superposition).
#
# NVIDIA's hosted ESMFold NIM reached end of life on 2026-08-25, so this uses
# the public ESM Atlas API: free, no key, but flaky and rate limited, single
# chain, ~400 residues max. We retry.
# --------------------------------------------------------------------------
# Fusions: two folded domains joined by a designed linker.
#
# condition_CoordinateGroup marks a rigid body. A domain with
# mask_coordinate False, mask_InternalCoordinate True and its own group
# keeps its internal geometry but may be placed anywhere, so the linker
# length decides where the tag ends up. condition_index-group must stay
# the same along the whole chain or the server rejects the backbone bond.
# --------------------------------------------------------------------------
FUSION_COLUMNS = [
"group_PDB", "type_symbol", "label_atom_id", "label_alt_id",
"label_comp_id", "label_asym_id", "label_entity_id", "label_seq_id",
"pdbx_PDB_ins_code", "auth_seq_id", "auth_comp_id", "auth_asym_id",
"auth_atom_id", "id", "B_iso_or_equiv", "occupancy", "pdbx_formal_charge",
"mask_index_1_residue", "mask_hotspot_1_atom",
"mask_InternalCoordinate_1_atom", "mask_sequence_1_residue",
"condition_index-group_1_token", "condition_CoordinateGroup_1_atom",
"mask_coordinate_1_atom", "annotation_expsegmax_1_residue",
"mask_expsegmax_1_residue", "mask_expsegmin_1_residue",
"annotation_expsegmin_1_residue", "atom_array_index",
"Cartn_x", "Cartn_y", "Cartn_z", "pdbx_PDB_model_num",
]
def _check_continuous(residues, label, limit=2.0):
"""Reject a domain with a chain break.
The server validates bond lengths and rejects any C--N longer than 2 A. A
crystal structure with disordered loops, or one whose non-standard residues
were dropped on load, will look continuous in the file and broken in the
geometry. Catch it here with a message that says what to do.
"""
bad = []
for (n1, r1, a1), (n2, r2, a2) in zip(residues, residues[1:]):
c = next((a for a in a1 if a[0] == "C"), None)
n = next((a for a in a2 if a[0] == "N"), None)
if not c or not n:
continue
d = sum((c[i + 2] - n[i + 2]) ** 2 for i in range(3)) ** 0.5
if d > limit:
bad.append((n1, n2, d))
if bad:
first = ", ".join(f"{a}->{b} ({d:.1f} A)" for a, b, d in bad[:4])
raise ValueError(
f"{label} has {len(bad)} chain break(s): {first}. The server rejects "
f"C--N bonds over {limit} A. Either the structure has disordered "
f"residues, or it contains a non-standard residue that was dropped "
f"on load (GFP's chromophore, CRO, is the usual case). Use a "
f"complete predicted model instead - pass a UniProt accession to "
f"fetch the AlphaFold structure.")
def _domain_rows(residues, chain, entity, start_label, coord_group, rigid,
serial, pin_absolute, index_group=0):
rows = []
label = start_label
for resnum, resname, atoms in residues:
for atom, element, x, y, z in atoms:
rows.append({
"group_PDB": "ATOM", "type_symbol": element,
"label_atom_id": atom, "label_alt_id": ".",
"label_comp_id": resname, "label_asym_id": chain,
"label_entity_id": entity, "label_seq_id": label,
"pdbx_PDB_ins_code": ".", "auth_seq_id": label,
"auth_comp_id": resname, "auth_asym_id": chain,
"auth_atom_id": atom, "id": serial,
"B_iso_or_equiv": 0.0, "occupancy": 1.0,
"pdbx_formal_charge": "?",
"mask_index_1_residue": True,
"mask_hotspot_1_atom": False,
"mask_InternalCoordinate_1_atom": rigid,
"mask_sequence_1_residue": True,
# one index group for the whole continuous chain: the server
# rejects a backbone bond whose two ends sit in different index
# groups, because that hides the connectivity from the model.
# Rigid bodies are distinguished by CoordinateGroup instead.
"condition_index-group_1_token": index_group,
"condition_CoordinateGroup_1_atom": coord_group,
"mask_coordinate_1_atom": pin_absolute,
"annotation_expsegmax_1_residue": -1,
"mask_expsegmax_1_residue": False,
"mask_expsegmin_1_residue": False,
"annotation_expsegmin_1_residue": -1,
"atom_array_index": serial,
"Cartn_x": x, "Cartn_y": y, "Cartn_z": z,
"pdbx_PDB_model_num": 1,
})
serial += 1
label += 1
return rows, serial, label
def author_fusion(poi: str, poi_chain: str, tag: str, tag_chain: str,
linker: tuple[int, int] = (5, 25),
template_cif: str | None = None,
chain: str = "A") -> str:
"""Author a POI--linker--tag fusion problem.
poi / tag PDB id, UniProt accession, or local path for each domain
linker (min, max) residues the model may use to join them
Returns an annotated mmCIF for task="motif_scaffolding".
The POI is pinned absolutely; the tag is a rigid body the model places.
Only the linker is designed.
"""
lo, hi = linker
if not (1 <= lo <= hi):
raise ValueError("need 1 <= min <= max for linker")
if template_cif is None:
with urllib.request.urlopen(f"{HF_BASE}/protein_binder/input.cif",
timeout=120) as r:
template_cif = r.read().decode()
poi_res = _load_chain(fetch_structure(poi), poi_chain)
tag_res = _load_chain(fetch_structure(tag), tag_chain)
_check_continuous(poi_res, poi)
_check_continuous(tag_res, tag)
seg = f"{lo}-{hi}"
rows, serial, label = _domain_rows(poi_res, chain, 0, 0, 0, False, 0,
pin_absolute=True)
linker_label = label
rows.append({
"group_PDB": "HETATM", "type_symbol": "X", "label_atom_id": "UNK",
"label_alt_id": ".", "label_comp_id": seg, "label_asym_id": chain,
"label_entity_id": 0, "label_seq_id": linker_label,
"pdbx_PDB_ins_code": ".", "auth_seq_id": linker_label,
"auth_comp_id": seg, "auth_asym_id": chain, "auth_atom_id": "UNK",
"id": serial, "B_iso_or_equiv": "nan", "occupancy": 0.0,
"pdbx_formal_charge": "?",
"mask_index_1_residue": True, "mask_hotspot_1_atom": False,
"mask_InternalCoordinate_1_atom": False,
"mask_sequence_1_residue": False,
"condition_index-group_1_token": 0,
"condition_CoordinateGroup_1_atom": -1,
"mask_coordinate_1_atom": False,
"annotation_expsegmax_1_residue": hi,
"mask_expsegmax_1_residue": True,
"mask_expsegmin_1_residue": True,
"annotation_expsegmin_1_residue": lo,
"atom_array_index": serial,
"Cartn_x": "nan", "Cartn_y": "nan", "Cartn_z": "nan",
"pdbx_PDB_model_num": 1,
})
serial += 1
tag_rows, serial, label = _domain_rows(tag_res, chain, 0, linker_label + 1,
1, True, serial, pin_absolute=False)
rows += tag_rows
poi_seq = "".join(THREE_TO_ONE[r[1]] for r in poi_res)
tag_seq = "".join(THREE_TO_ONE[r[1]] for r in tag_res)
one = poi_seq + "X" + tag_seq
# bonds across the two joins
conns = [
f"1 covale sing {chain} {chain} {poi_res[-1][1]:<6} {seg:<6} "
f"{linker_label - 1} {linker_label} C UNK . . 1 1",
f"2 covale sing {chain} {chain} {seg:<6} {tag_res[0][1]:<6} "
f"{linker_label} {linker_label + 1} UNK N . . 1 1",
]
struct_conn = "\n".join([
"loop_", "_struct_conn.id ", "_struct_conn.conn_type_id ",
"_struct_conn.pdbx_value_order ", "_struct_conn.ptnr1_label_asym_id ",
"_struct_conn.ptnr2_label_asym_id ", "_struct_conn.ptnr1_label_comp_id ",
"_struct_conn.ptnr2_label_comp_id ", "_struct_conn.ptnr1_label_seq_id ",
"_struct_conn.ptnr2_label_seq_id ", "_struct_conn.ptnr1_label_atom_id ",
"_struct_conn.ptnr2_label_atom_id ", "_struct_conn.pdbx_ptnr1_PDB_ins_code ",
"_struct_conn.pdbx_ptnr2_PDB_ins_code ", "_struct_conn.ptnr1_transformation_id ",
"_struct_conn.ptnr2_transformation_id ",
] + conns)
blocks = _split_template(template_cif)
chem_comp = _strip_contig_rows(blocks["chem_comp"]).replace(
"_chem_comp.formula_weight \n",
f"_chem_comp.formula_weight \n{seg} {seg} NON-POLYMER ? ? ? ?\n", 1)
chem_comp_atom = _strip_contig_rows(blocks["chem_comp_atom"]) + \
f"\n{seg} UNK X 0 N N nan nan nan"
poly = []
n = 0
for _, name, _ in poi_res:
poly.append(f"0 n {name:<8} {n}"); n += 1
poly.append(f"0 n {seg:<8} {n}"); n += 1
for _, name, _ in tag_res:
poly.append(f"0 n {name:<8} {n}"); n += 1
seqw = "\n".join(one[i:i+80] for i in range(0, len(one), 80))
return f"""data_fusion
#
_entry.id fusion
#
loop_
_entity.id
_entity.type
_entity.pdbx_description
0 polymer .
#
_entity_poly.entity_id 0
_entity_poly.type polypeptide(L)
_entity_poly.nstd_linkage no
_entity_poly.nstd_monomer yes
_entity_poly.pdbx_seq_one_letter_code
;{seqw}
;
_entity_poly.pdbx_seq_one_letter_code_can
;{seqw}
;
_entity_poly.pdbx_strand_id {chain}
_entity_poly.pdbx_target_identifier ?
#
loop_
_entity_poly_seq.entity_id
_entity_poly_seq.hetero
_entity_poly_seq.mon_id
_entity_poly_seq.num
{chr(10).join(poly)}
#
{chem_comp}
#
{struct_conn}
#
{blocks["chem_comp_bond"]}
#
_rfd_design.sampled_contig {poi_chain}1-{len(poi_res)},{seg},{tag_chain}1-{len(tag_res)}
#
{_atom_site_loop(FUSION_COLUMNS, rows)}
#
{chem_comp_atom}
#
"""
Appending to rfd4_demo.py
Block 9: the independent check¶
Used in Part L. esmfold folds a sequence through the public ESM Atlas API,
kabsch_rmsd superposes two coordinate sets, and refold_check combines them into the
pLDDT and self-consistency RMSD pair.
%%writefile -a rfd4_demo.py
# ==========================================================================
ESMATLAS = "https://api.esmatlas.com/foldSequence/v1/pdb/"
def esmfold(sequence: str, retries: int = 10, pause: float = 20.0) -> str:
"""Fold a sequence with ESMFold via the public ESM Atlas API. Returns PDB text.
The service is free and needs no key, but it is flaky and rate-limited --
504s are normal, so we retry. Single chain, roughly 400 residues max.
"""
if len(sequence) > 400:
raise ValueError(f"ESM Atlas caps out around 400 residues; got {len(sequence)}")
body = sequence.strip().encode()
last = None
for attempt in range(1, retries + 1):
req = urllib.request.Request(ESMATLAS, data=body,
headers={"Content-Type": "text/plain"})
try:
with urllib.request.urlopen(req, timeout=180) as r:
text = r.read().decode()
if text.lstrip().startswith(("HEADER", "ATOM", "PARENT")):
return text
last = text[:200]
except Exception as exc: # noqa: BLE001
last = f"{type(exc).__name__}: {exc}"
print(f" ESM Atlas busy (attempt {attempt}/{retries}) - retrying in {pause:.0f}s")
time.sleep(pause)
raise RuntimeError(f"ESM Atlas did not return a structure. Last: {last}")
def ca_from_pdb(pdb_text: str):
out = []
for line in pdb_text.split("\n"):
if line.startswith("ATOM") and line[12:16].strip() == "CA":
out.append((float(line[30:38]), float(line[38:46]), float(line[46:54]),
float(line[60:66])))
return out
def ca_from_cif(cif_text: str, chain: str | None = None):
import re
cols = re.findall(r"_atom_site\.(\S+)", cif_text)
idx = {c: i for i, c in enumerate(cols)}
out = []
for line in cif_text.split("\n"):
if not line.startswith("ATOM "):
continue
f = line.split()
if len(f) != len(cols):
continue
if f[idx["label_atom_id"]] != "CA":
continue
if chain and f[idx["auth_asym_id"]] != chain:
continue
out.append((float(f[idx["Cartn_x"]]), float(f[idx["Cartn_y"]]),
float(f[idx["Cartn_z"]])))
return out
def kabsch_rmsd(p, q) -> float:
"""CA-RMSD after optimal superposition."""
import numpy as np
p = np.array(p, float); q = np.array(q, float)
p = p - p.mean(0); q = q - q.mean(0)
v, s, w = np.linalg.svd(p.T @ q)
d = np.sign(np.linalg.det(v @ w))
rot = v @ np.diag([1, 1, d]) @ w
p = p @ rot
return float(np.sqrt(((p - q) ** 2).sum() / len(p)))
def refold_check(response, index: int = 0, chain: str | None = None,
verbose: bool = True) -> dict:
"""Fold a design's SEQUENCE with ESMFold and compare to its own backbone.
This is the self-consistency test real design pipelines live by: the
generator proposed a structure, an independent model folds the sequence
from scratch, and you ask whether they agree.
"""
d = response["designs"][index]
seq = d.get("sequence")
cif = (d.get("structure") or {}).get("content")
if not seq or not cif:
raise ValueError("need include_sequence=True and structure_format='mmcif'")
designed = ca_from_cif(cif, chain)
if chain and len(designed) != len(seq):
seq = seq[-len(designed):] if len(seq) > len(designed) else seq
pdb = esmfold(seq)
folded = ca_from_pdb(pdb)
n = min(len(folded), len(designed))
plddt = sum(a[3] for a in folded) / len(folded)
if plddt > 1.5:
plddt /= 100.0
rmsd = kabsch_rmsd([a[:3] for a in folded[:n]], designed[:n]) if n >= 3 else float("nan")
if verbose:
print(f" sequence length {len(seq)}")
print(f" ESMFold mean pLDDT {plddt:.3f} (>0.7 = confident)")
print(f" self-consistency CA-RMSD {rmsd:.2f} A (<2 A = the two models agree)")
print()
print(" A design that passes both is a hypothesis worth testing.")
print(" One that fails has not been refuted either -- ESMFold is also just a model.")
return {"plddt": plddt, "scrmsd": rmsd, "pdb": pdb, "n_compared": n}
def conditioning_of(cif_text: str) -> dict:
"""Which residues did the model KEEP from your input, and which did it invent?
Reads `annotation_src-component_1_atom` from a returned design: '.' means the
residue was designed, anything else names the input residue it came from.
Returns {chain: {"fixed": [resids], "free": [resids]}}.
"""
import re
cols = re.findall(r"_atom_site\.(\S+)", cif_text)
if "annotation_src-component_1_atom" not in cols:
return {}
idx = {c: i for i, c in enumerate(cols)}
out: dict = {}
for line in cif_text.split("\n"):
if not line.startswith(("ATOM ", "HETATM ")):
continue
f = line.split()
if len(f) != len(cols):
continue
ch = f[idx["auth_asym_id"]]
try:
resi = int(f[idx["auth_seq_id"]])
except ValueError:
continue
key = "free" if f[idx["annotation_src-component_1_atom"]] == "." else "fixed"
slot = out.setdefault(ch, {"fixed": set(), "free": set()})
slot[key].add(resi)
return {ch: {k: sorted(v) for k, v in d.items()} for ch, d in out.items()}
def view_conditioned(cif_text: str, fixed_color: str = "red",
free_color: str = "lightgrey",
context_chains: list | None = None,
context_color: str = "lightgrey",
width: int = 700, height: int = 480):
"""Colour a design by what was HELD FIXED vs what the model INVENTED.
The split is read out of the returned file, not hard-coded: residues the
model carried over from your input are `fixed_color`, residues it designed
are `free_color`. `context_chains` (e.g. an antigen) render in grey.
"""
try:
import py3Dmol
except ImportError:
print("pip install py3Dmol")
return None
cond = conditioning_of(cif_text)
context = set(context_chains or [])
v = py3Dmol.view(width=width, height=height)
v.addModel(cif_text, "cif")
v.setStyle({"cartoon": {"color": free_color}})
for ch, groups in cond.items():
if ch in context:
v.setStyle({"chain": ch}, {"cartoon": {"color": context_color}})
continue
if groups["free"]:
v.setStyle({"chain": ch, "resi": groups["free"]},
{"cartoon": {"color": free_color}})
if groups["fixed"]:
v.setStyle({"chain": ch, "resi": groups["fixed"]},
{"cartoon": {"color": fixed_color}})
v.addStyle({"hetflag": True},
{"stick": {"radius": 0.25, "colorscheme": "greenCarbon"}})
v.zoomTo()
if not cond:
print(" (no src-component annotation in this file - nothing to split on)")
else:
for ch, g in sorted(cond.items()):
tag = " [context]" if ch in context else ""
print(f" chain {ch}: {len(g['fixed'])} held fixed ({fixed_color}), "
f"{len(g['free'])} designed ({free_color}){tag}")
return v.show()
Appending to rfd4_demo.py
import os, json
from rfd4_demo import (
RFD4Client, RFD4Error, GALLERY, fetch_example, fetch_gallery,
describe_cif, parse_atom_site,
annotated_cif_input, condition_spec_input, polygly_cif, contig_cif,
summarize, metric_keys, metric_table,
author_target, author_motif, author_fusion, fetch_structure,
esmfold, refold_check,
view, view_design, view_conditioned, conditioning_of,
chains_in, cif_of, save_structures,
)
client = RFD4Client(api_key=API_KEY)
print("connected to", client.url)
connected to https://health.api.nvidia.com/v1/biology/ipd/rfd4-proteina/generative
The Example Gallery¶
Before designing anything, download the inputs we will use for the rest of the session.
NVIDIA publishes nine ready-made inputs. Each is a structure file that already carries its own constraints: which atoms are frozen, which residues are the hotspot, how long the new part may be. Later parts of this notebook reuse these files, and the final part shows you how to write one yourself. Reading them is how you learn to write a design problem.
inputs = fetch_gallery()
print()
for k, (label, task, blurb) in GALLERY.items():
print(f"{label:<30} {blurb}")
ok Unconditional protein task=generation ok Protein binder · PD-L1 task=generation ok Small-molecule binder · SAM task=small_molecule_binder ok Enzyme · catalytic motif task=enzyme_design ok Motif scaffolding · 1QJG task=motif_scaffolding ok Antibody · HER2 CDR H3 task=antibody_design ok VHH · PD-L1 CDRs task=antibody_design ok Inverse folding · 2KL8 task=inverse_folding ok Partial diffusion · 2KL8 task=partial_diffusion Unconditional protein 250 residues from noise, nothing held Protein binder · PD-L1 PD-L1 held fixed with hotspots; grows a 50-120 residue binder Small-molecule binder · SAM a ligand and nothing else; builds a 150-residue pocket around it Enzyme · catalytic motif catalytic side-chain atoms; model places them in a 180-residue scaffold Motif scaffolding · 1QJG three motif residues at fixed positions, linkers designed Antibody · HER2 CDR H3 framework and antigen fixed; CDR H3 redesigned VHH · PD-L1 CDRs single-domain framework fixed; all CDRs redesigned Inverse folding · 2KL8 backbone held; sequence resampled Partial diffusion · 2KL8 restart the trajectory partway to diversify a structure
Part A: A Protein From Nothing¶
Start with the simplest possible request, so that later constraints have something to contrast against: 250 residues, nothing held fixed.
The output contains a sequence and a structure together. There is no second program converting a backbone into a sequence; the model produces both at once.
hello = client.design(
task="generation",
inp=annotated_cif_input(inputs["unconditional"], example_id="hello"),
sampling={"recycle_strategy": "thinking_medium", "num_replicates": 1},
output={"structure_format": "mmcif", "include_sequence": True},
)
d = hello["designs"][0]
print(f"{client.last_elapsed_s:.1f} s on an {hello['runtime']['gpu']}\n")
print("sequence:", d["sequence"])
view(cif_of(hello))
5.6 s on an NVIDIA H200 sequence: MKKIIKIIIKSIIIIIIIIIISIILIIIIIILSIIISIILNINFFISLVIVTLIIIFILIFISVKLIKKNKKKIIISIILLISSIIIIIIISIILIINNNIILSIILIIISIIIFIILLKKISIKKLIKISIILIIISIILLIISIILLFINIILSIILLIISIIINILILIILIIIKIKIKKKKNKKIKKLIKISIILIIISIILLIISIILLFSNNINIILIIILLIISIIINIIILIILKKIKKKLK
3Dmol.js failed to load for some reason. Please check your browser console for error messages.
That protein has not existed before. Run the cell again and you get a different one.
Your turn 1 — choose the size. polygly_cif(L) is the plainest way to request L
residues from noise. Change MY_LENGTH, run the cell, and look at the result. Try
something small (about 60) and something large (about 300).
Write down your prediction before you run it: which length takes longer, and will a short protein or a long one look more like a single tidy domain?
# Your turn 1
MY_LENGTH = 120 # <-- edit me (try 60, then 300)
mine = client.design(
task="generation",
inp=annotated_cif_input(polygly_cif(MY_LENGTH), example_id="yourturn-len"),
sampling={"recycle_strategy": "thinking_medium", "num_replicates": 1},
output={"structure_format": "mmcif", "include_sequence": True},
)
d = mine["designs"][0]
print(f"{MY_LENGTH} residues -> {client.last_elapsed_s:.1f} s, "
f"{d['diagnostics']['num_tokens']} tokens")
print(d["sequence"])
view(cif_of(mine))
120 residues -> 4.5 s, 120 tokens MEEKLEKIAKKIAEKIKSMSEEEKEKIIEKIPKEDKEKLEELCKELKLSEEETKKVLVVYKLVKEEAEKKKLSPEEIKKVAKETLLLLGFSEEKAEKLSEKITELIVEEKKKKKAAAAAA
3Dmol.js failed to load for some reason. Please check your browser console for error messages.
Part B: A Binder for PD-L1¶
Part A placed no constraints at all. Now we add the strongest kind: a second protein that must be left untouched, and a patch on it that the new protein must contact.
PD-L1 is the checkpoint protein tumours use to switch off T-cells. Blocking it is how Keytruda works. The input holds the entire PD-L1 structure frozen and marks its epitope as hotspots, meaning "bind here". The model grows a new protein against that surface.
The task is still generation. There is no separate binder mode. A binder is generation
where the input happens to hold a target fixed. The design problem lives in the file, not
in the task name. This idea returns in Part G, and you will write such a file yourself in
Part K.
The cell below first prints what conditioning the input carries, then runs the design.
describe_cif(inputs["protein_binder"]) # mask_hotspot = "bind here"
print("chains:", chains_in(inputs["protein_binder"]), " (A = PD-L1 target, B = your binder)")
binder = client.design(
task="generation",
inp=annotated_cif_input(inputs["protein_binder"], example_id="pdl1"),
sampling={"recycle_strategy": "thinking_medium", "num_replicates": 1},
metrics={"tags": ["design"]},
output={"structure_format": "mmcif", "include_sequence": True},
)
summarize(binder)
# Orange: the new binder (chain B). Grey: PD-L1, held fixed (chain A).
out = cif_of(binder)
print("chains in the result:", chains_in(out))
view_design(out, designed_chain="B") # B is the DESIGNED chain; A is PD-L1
atoms 924 ATOM / 1 HETATM chains=['A', 'B']
contig (none)
design segs 1 ranges=[('50', '120')]
mask_coordinate 924 / 925 set
mask_expsegmax 1 / 925 set
mask_expsegmin 1 / 925 set
mask_hotspot 6 / 925 set
mask_sequence 924 / 925 set
chains: ['A', 'B'] (A = PD-L1 target, B = your binder)
request_id local task=generation
wall time 1.8 s for 1 design(s)
backend bioir dtype=bfloat16 gpu=NVIDIA H200
oracle [] (none mounted)
schedule 60 steps / 185 trunk passes strategy=thinking_medium
design_0 origin=sample tokens=194 finite=True
NAFTVTVPKDLYVVEYGSNMTIECKFPVEKQLDLAALIVYWEMEDKNIIQFVHGEEDLKVQHSSYRQRARLLKDQLSLGN... (len 194)
bond_length_metrics.bond_length.frac_unreasonable = 0.0
bond_length_metrics.bond_length.frac_unreasonable.between_chain.nonpolymer_to_polymer = None
bond_length_metrics.bond_length.frac_unreasonable.between_chain.polymer_polymer = None
bond_length_metrics.bond_length.frac_unreasonable.between_residue.within_chain = 0.0
bond_length_metrics.bond_length.frac_unreasonable.within_residue = 0.0
clash_metrics.frac_atoms_clashing_1p5A = 0.0013377926421404682
clash_metrics.frac_atoms_clashing_inter_residue_ligand_ligand_1p5A = 0.0
clash_metrics.frac_atoms_clashing_inter_residue_polymer_ligand_1p5A = 0.0
clash_metrics.frac_atoms_clashing_inter_residue_polymer_polymer_1p5A = 0.0013377926421404682
clash_metrics.frac_atoms_clashing_intra_residue_ligand_1p5A = 0.0
clash_metrics.frac_atoms_clashing_intra_residue_polymer_1p5A = 0.0
design_motif_allatom_rmsd.rmsd = 0.639989972114563
sequence_adherence.n_violations = 0.0
sequence_adherence.sequence_adherence = 1.0
sequence_probability_adherence.n_one_conditions = 115.0
sequence_probability_adherence.n_one_violations = 0.0
sequence_probability_adherence.n_zero_conditions = 3105.0
sequence_probability_adherence.n_zero_violations = 0.0
sequence_probability_adherence.one_adherence = 1.0
sequence_probability_adherence.zero_adherence = 1.0
sse_distribution.helix_fraction = 0.3402061855670103
sse_distribution.helix_fraction_coi = 0.8354430379746836
sse_distribution.loop_fraction = 0.42783505154639173
sse_distribution.loop_fraction_coi = 0.16455696202531644
sse_distribution.num_ss_elements = 25.0
sse_distribution.num_ss_elements_coi = 11.0
sse_distribution.sheet_fraction = 0.23195876288659795
sse_distribution.sheet_fraction_coi = 0.0
aggregated:
bond_length_metrics.bond_length.frac_unreasonable mean 0.0 (n=1)
bond_length_metrics.bond_length.frac_unreasonable.between_residue.within_chain mean 0.0 (n=1)
bond_length_metrics.bond_length.frac_unreasonable.within_residue mean 0.0 (n=1)
clash_metrics.frac_atoms_clashing_1p5A mean 0.0013377926421404682 (n=1)
clash_metrics.frac_atoms_clashing_inter_residue_ligand_ligand_1p5A mean 0.0 (n=1)
clash_metrics.frac_atoms_clashing_inter_residue_polymer_ligand_1p5A mean 0.0 (n=1)
clash_metrics.frac_atoms_clashing_inter_residue_polymer_polymer_1p5A mean 0.0013377926421404682 (n=1)
clash_metrics.frac_atoms_clashing_intra_residue_ligand_1p5A mean 0.0 (n=1)
clash_metrics.frac_atoms_clashing_intra_residue_polymer_1p5A mean 0.0 (n=1)
design_motif_allatom_rmsd.rmsd median 0.639989972114563 (n=1)
sequence_adherence.n_violations mean 0.0 (n=1)
sequence_adherence.sequence_adherence mean 1.0 (n=1)
sequence_probability_adherence.n_one_conditions mean 115.0 (n=1)
sequence_probability_adherence.n_one_violations mean 0.0 (n=1)
sequence_probability_adherence.n_zero_conditions mean 3105.0 (n=1)
sequence_probability_adherence.n_zero_violations mean 0.0 (n=1)
sequence_probability_adherence.one_adherence mean 1.0 (n=1)
sequence_probability_adherence.zero_adherence mean 1.0 (n=1)
sse_distribution.helix_fraction mean 0.3402061855670103 (n=1)
sse_distribution.helix_fraction_coi mean 0.8354430379746836 (n=1)
sse_distribution.loop_fraction mean 0.42783505154639173 (n=1)
sse_distribution.loop_fraction_coi mean 0.16455696202531644 (n=1)
sse_distribution.num_ss_elements mean 25.0 (n=1)
sse_distribution.num_ss_elements_coi mean 11.0 (n=1)
sse_distribution.sheet_fraction mean 0.23195876288659795 (n=1)
sse_distribution.sheet_fraction_coi mean 0.0 (n=1)
chains in the result: ['A', 'B']
3Dmol.js failed to load for some reason. Please check your browser console for error messages.
Your turn 2 — generate several candidates. A binder is stochastic: each replicate is a different protein against the same PD-L1 surface. Ask for a few and compare them.
Questions to discuss: how many candidates would you generate computationally before committing one to the bench? What does each extra replicate cost here, compared to in the lab?
# Your turn 2
MY_REPLICATES = 3 # <-- edit me (1..4 is sensible)
binders = client.design(
task="generation",
inp=annotated_cif_input(inputs["protein_binder"], example_id="yourturn-pdl1"),
sampling={"recycle_strategy": "thinking_medium", "num_replicates": MY_REPLICATES},
metrics={"tags": ["design"]},
output={"structure_format": "mmcif", "include_sequence": True},
)
summarize(binders)
# Orange: your binder. Grey: PD-L1, held fixed.
for i in range(len(binders["designs"])):
print(f"--- design {i} ---")
view_design(cif_of(binders, i), designed_chain="B")
request_id local task=generation
wall time 3.8 s for 3 design(s)
backend bioir dtype=bfloat16 gpu=NVIDIA H200
oracle [] (none mounted)
schedule 60 steps / 185 trunk passes strategy=thinking_medium
design_0 origin=sample tokens=169 finite=True
NAFTVTVPKDLYVVEYGSNMTIECKFPVEKQLDLAALIVYWEMEDKNIIQFVHGEEDLKVQHSSYRQRARLLKDQLSLGN... (len 169)
bond_length_metrics.bond_length.frac_unreasonable = 0.0
bond_length_metrics.bond_length.frac_unreasonable.between_chain.nonpolymer_to_polymer = None
bond_length_metrics.bond_length.frac_unreasonable.between_chain.polymer_polymer = None
bond_length_metrics.bond_length.frac_unreasonable.between_residue.within_chain = 0.0
bond_length_metrics.bond_length.frac_unreasonable.within_residue = 0.0
clash_metrics.frac_atoms_clashing_1p5A = 0.0
clash_metrics.frac_atoms_clashing_inter_residue_ligand_ligand_1p5A = 0.0
clash_metrics.frac_atoms_clashing_inter_residue_polymer_ligand_1p5A = 0.0
clash_metrics.frac_atoms_clashing_inter_residue_polymer_polymer_1p5A = 0.0
clash_metrics.frac_atoms_clashing_intra_residue_ligand_1p5A = 0.0
clash_metrics.frac_atoms_clashing_intra_residue_polymer_1p5A = 0.0
design_motif_allatom_rmsd.rmsd = 0.6343433856964111
sequence_adherence.n_violations = 0.0
sequence_adherence.sequence_adherence = 1.0
sequence_probability_adherence.n_one_conditions = 115.0
sequence_probability_adherence.n_one_violations = 0.0
sequence_probability_adherence.n_zero_conditions = 3105.0
sequence_probability_adherence.n_zero_violations = 0.0
sequence_probability_adherence.one_adherence = 1.0
sequence_probability_adherence.zero_adherence = 1.0
sse_distribution.helix_fraction = 0.27218934911242604
sse_distribution.helix_fraction_coi = 0.8518518518518519
sse_distribution.loop_fraction = 0.44970414201183434
sse_distribution.loop_fraction_coi = 0.14814814814814814
sse_distribution.num_ss_elements = 23.0
sse_distribution.num_ss_elements_coi = 9.0
sse_distribution.sheet_fraction = 0.2781065088757396
sse_distribution.sheet_fraction_coi = 0.0
design_1 origin=sample tokens=183 finite=True
NAFTVTVPKDLYVVEYGSNMTIECKFPVEKQLDLAALIVYWEMEDKNIIQFVHGEEDLKVQHSSYRQRARLLKDQLSLGN... (len 183)
bond_length_metrics.bond_length.frac_unreasonable = 0.0006858710562414266
bond_length_metrics.bond_length.frac_unreasonable.between_chain.nonpolymer_to_polymer = None
bond_length_metrics.bond_length.frac_unreasonable.between_chain.polymer_polymer = None
bond_length_metrics.bond_length.frac_unreasonable.between_residue.within_chain = 0.0055248618784530384
bond_length_metrics.bond_length.frac_unreasonable.within_residue = 0.0
clash_metrics.frac_atoms_clashing_1p5A = 0.0
clash_metrics.frac_atoms_clashing_inter_residue_ligand_ligand_1p5A = 0.0
clash_metrics.frac_atoms_clashing_inter_residue_polymer_ligand_1p5A = 0.0
clash_metrics.frac_atoms_clashing_inter_residue_polymer_polymer_1p5A = 0.0
clash_metrics.frac_atoms_clashing_intra_residue_ligand_1p5A = 0.0
clash_metrics.frac_atoms_clashing_intra_residue_polymer_1p5A = 0.0
design_motif_allatom_rmsd.rmsd = 0.6506813168525696
sequence_adherence.n_violations = 0.0
sequence_adherence.sequence_adherence = 1.0
sequence_probability_adherence.n_one_conditions = 115.0
sequence_probability_adherence.n_one_violations = 0.0
sequence_probability_adherence.n_zero_conditions = 3105.0
sequence_probability_adherence.n_zero_violations = 0.0
sequence_probability_adherence.one_adherence = 1.0
sequence_probability_adherence.zero_adherence = 1.0
sse_distribution.helix_fraction = 0.273224043715847
sse_distribution.helix_fraction_coi = 0.7352941176470589
sse_distribution.loop_fraction = 0.5245901639344263
sse_distribution.loop_fraction_coi = 0.2647058823529412
sse_distribution.num_ss_elements = 21.0
sse_distribution.num_ss_elements_coi = 9.0
sse_distribution.sheet_fraction = 0.20218579234972678
sse_distribution.sheet_fraction_coi = 0.0
design_2 origin=sample tokens=199 finite=True
NAFTVTVPKDLYVVEYGSNMTIECKFPVEKQLDLAALIVYWEMEDKNIIQFVHGEEDLKVQHSSYRQRARLLKDQLSLGN... (len 199)
bond_length_metrics.bond_length.frac_unreasonable = 0.0
bond_length_metrics.bond_length.frac_unreasonable.between_chain.nonpolymer_to_polymer = None
bond_length_metrics.bond_length.frac_unreasonable.between_chain.polymer_polymer = None
bond_length_metrics.bond_length.frac_unreasonable.between_residue.within_chain = 0.0
bond_length_metrics.bond_length.frac_unreasonable.within_residue = 0.0
clash_metrics.frac_atoms_clashing_1p5A = 0.0
clash_metrics.frac_atoms_clashing_inter_residue_ligand_ligand_1p5A = 0.0
clash_metrics.frac_atoms_clashing_inter_residue_polymer_ligand_1p5A = 0.0
clash_metrics.frac_atoms_clashing_inter_residue_polymer_polymer_1p5A = 0.0
clash_metrics.frac_atoms_clashing_intra_residue_ligand_1p5A = 0.0
clash_metrics.frac_atoms_clashing_intra_residue_polymer_1p5A = 0.0
design_motif_allatom_rmsd.rmsd = 0.6422765851020813
sequence_adherence.n_violations = 0.0
sequence_adherence.sequence_adherence = 1.0
sequence_probability_adherence.n_one_conditions = 115.0
sequence_probability_adherence.n_one_violations = 0.0
sequence_probability_adherence.n_zero_conditions = 3105.0
sequence_probability_adherence.n_zero_violations = 0.0
sequence_probability_adherence.one_adherence = 1.0
sequence_probability_adherence.zero_adherence = 1.0
sse_distribution.helix_fraction = 0.0
sse_distribution.helix_fraction_coi = 0.0
sse_distribution.loop_fraction = 0.592964824120603
sse_distribution.loop_fraction_coi = 0.5952380952380952
sse_distribution.num_ss_elements = 25.0
sse_distribution.num_ss_elements_coi = 11.0
sse_distribution.sheet_fraction = 0.40703517587939697
sse_distribution.sheet_fraction_coi = 0.40476190476190477
aggregated:
bond_length_metrics.bond_length.frac_unreasonable mean 0.00022862368541380886 (n=3)
bond_length_metrics.bond_length.frac_unreasonable.between_residue.within_chain mean 0.0018416206261510127 (n=3)
bond_length_metrics.bond_length.frac_unreasonable.within_residue mean 0.0 (n=3)
clash_metrics.frac_atoms_clashing_1p5A mean 0.0 (n=3)
clash_metrics.frac_atoms_clashing_inter_residue_ligand_ligand_1p5A mean 0.0 (n=3)
clash_metrics.frac_atoms_clashing_inter_residue_polymer_ligand_1p5A mean 0.0 (n=3)
clash_metrics.frac_atoms_clashing_inter_residue_polymer_polymer_1p5A mean 0.0 (n=3)
clash_metrics.frac_atoms_clashing_intra_residue_ligand_1p5A mean 0.0 (n=3)
clash_metrics.frac_atoms_clashing_intra_residue_polymer_1p5A mean 0.0 (n=3)
design_motif_allatom_rmsd.rmsd median 0.6422765851020813 (n=3)
sequence_adherence.n_violations mean 0.0 (n=3)
sequence_adherence.sequence_adherence mean 1.0 (n=3)
sequence_probability_adherence.n_one_conditions mean 115.0 (n=3)
sequence_probability_adherence.n_one_violations mean 0.0 (n=3)
sequence_probability_adherence.n_zero_conditions mean 3105.0 (n=3)
sequence_probability_adherence.n_zero_violations mean 0.0 (n=3)
sequence_probability_adherence.one_adherence mean 1.0 (n=3)
sequence_probability_adherence.zero_adherence mean 1.0 (n=3)
sse_distribution.helix_fraction mean 0.181804464276091 (n=3)
sse_distribution.helix_fraction_coi mean 0.529048656499637 (n=3)
sse_distribution.loop_fraction mean 0.5224197100222878 (n=3)
sse_distribution.loop_fraction_coi mean 0.3360307085797282 (n=3)
sse_distribution.num_ss_elements mean 23.0 (n=3)
sse_distribution.num_ss_elements_coi mean 9.666666666666666 (n=3)
sse_distribution.sheet_fraction mean 0.29577582570162114 (n=3)
sse_distribution.sheet_fraction_coi mean 0.1349206349206349 (n=3)
--- design 0 ---
3Dmol.js failed to load for some reason. Please check your browser console for error messages.
--- design 1 ---
3Dmol.js failed to load for some reason. Please check your browser console for error messages.
--- design 2 ---
3Dmol.js failed to load for some reason. Please check your browser console for error messages.
Part C: A Pocket Around a Molecule¶
In Part B the thing held fixed was a protein. It does not have to be.
This input contains no protein at all, only the 28 atoms of SAM (S-adenosyl methionine). The request is to build a 150-residue protein that wraps around it.
describe_cif(inputs["small_molecule_binder"]) # ATOM 0 — only the ligand
pocket = client.design(
task="small_molecule_binder",
inp=annotated_cif_input(inputs["small_molecule_binder"], example_id="sam"),
sampling={"recycle_strategy": "thinking_medium", "num_replicates": 1},
metrics={"tags": ["design"]},
output={"structure_format": "mmcif", "include_sequence": True},
)
summarize(pocket)
view(cif_of(pocket)) # green sticks = SAM, cartoon = the pocket built around it
atoms 0 ATOM / 28 HETATM chains=['A', 'B']
contig 150-150
design segs 1 ranges=[('150', '150')]
condition_hbond 0 / 28 set
condition_index-group 1 / 28 set
mask_coordinate 0 / 28 set
mask_expsegmax 1 / 28 set
mask_expsegmin 1 / 28 set
mask_index 28 / 28 set
mask_sequence 27 / 28 set
request_id local task=small_molecule_binder
wall time 1.8 s for 1 design(s)
backend bioir dtype=bfloat16 gpu=NVIDIA H200
oracle [] (none mounted)
schedule 60 steps / 185 trunk passes strategy=thinking_medium
design_0 origin=sample tokens=177 finite=True
SEEEKKVKEIVSVALELNDEINNSYPEDTFAFSNGIAIYYAFAKLEIPYSEKLYSKASAALRLAILKRIEEKKPSVIIDL... (len 177)
bond_length_metrics.bond_length.frac_unreasonable = 0.0
bond_length_metrics.bond_length.frac_unreasonable.between_chain.nonpolymer_to_polymer = None
bond_length_metrics.bond_length.frac_unreasonable.between_chain.polymer_polymer = None
bond_length_metrics.bond_length.frac_unreasonable.between_residue.within_chain = 0.0
bond_length_metrics.bond_length.frac_unreasonable.within_residue = 0.0
clash_metrics.frac_atoms_clashing_1p5A = 0.0
clash_metrics.frac_atoms_clashing_inter_residue_ligand_ligand_1p5A = 0.0
clash_metrics.frac_atoms_clashing_inter_residue_polymer_ligand_1p5A = 0.0
clash_metrics.frac_atoms_clashing_inter_residue_polymer_polymer_1p5A = 0.0
clash_metrics.frac_atoms_clashing_intra_residue_ligand_1p5A = 0.0
clash_metrics.frac_atoms_clashing_intra_residue_polymer_1p5A = 0.0
ligand_contacts.frac_atoms_contacting_4p5A = 1.0
sequence_adherence.n_violations = 0.0
sequence_adherence.sequence_adherence = 1.0
sequence_probability_adherence.n_one_conditions = 0.0
sequence_probability_adherence.n_one_violations = 0.0
sequence_probability_adherence.n_zero_conditions = 28.0
sequence_probability_adherence.n_zero_violations = 0.0
sequence_probability_adherence.one_adherence = None
sequence_probability_adherence.zero_adherence = 1.0
sse_distribution.helix_fraction = 0.58
sse_distribution.helix_fraction_coi = 0.58
sse_distribution.loop_fraction = 0.2733333333333333
sse_distribution.loop_fraction_coi = 0.2733333333333333
sse_distribution.num_ss_elements = 19.0
sse_distribution.num_ss_elements_coi = 19.0
sse_distribution.sheet_fraction = 0.14666666666666667
sse_distribution.sheet_fraction_coi = 0.14666666666666667
aggregated:
bond_length_metrics.bond_length.frac_unreasonable mean 0.0 (n=1)
bond_length_metrics.bond_length.frac_unreasonable.between_residue.within_chain mean 0.0 (n=1)
bond_length_metrics.bond_length.frac_unreasonable.within_residue mean 0.0 (n=1)
clash_metrics.frac_atoms_clashing_1p5A mean 0.0 (n=1)
clash_metrics.frac_atoms_clashing_inter_residue_ligand_ligand_1p5A mean 0.0 (n=1)
clash_metrics.frac_atoms_clashing_inter_residue_polymer_ligand_1p5A mean 0.0 (n=1)
clash_metrics.frac_atoms_clashing_inter_residue_polymer_polymer_1p5A mean 0.0 (n=1)
clash_metrics.frac_atoms_clashing_intra_residue_ligand_1p5A mean 0.0 (n=1)
clash_metrics.frac_atoms_clashing_intra_residue_polymer_1p5A mean 0.0 (n=1)
ligand_contacts.frac_atoms_contacting_4p5A mean 1.0 (n=1)
sequence_adherence.n_violations mean 0.0 (n=1)
sequence_adherence.sequence_adherence mean 1.0 (n=1)
sequence_probability_adherence.n_one_conditions mean 0.0 (n=1)
sequence_probability_adherence.n_one_violations mean 0.0 (n=1)
sequence_probability_adherence.n_zero_conditions mean 28.0 (n=1)
sequence_probability_adherence.n_zero_violations mean 0.0 (n=1)
sequence_probability_adherence.zero_adherence mean 1.0 (n=1)
sse_distribution.helix_fraction mean 0.58 (n=1)
sse_distribution.helix_fraction_coi mean 0.58 (n=1)
sse_distribution.loop_fraction mean 0.2733333333333333 (n=1)
sse_distribution.loop_fraction_coi mean 0.2733333333333333 (n=1)
sse_distribution.num_ss_elements mean 19.0 (n=1)
sse_distribution.num_ss_elements_coi mean 19.0 (n=1)
sse_distribution.sheet_fraction mean 0.14666666666666667 (n=1)
sse_distribution.sheet_fraction_coi mean 0.14666666666666667 (n=1)
3Dmol.js failed to load for some reason. Please check your browser console for error messages.
Start from a molecule you want to bind and finish with a protein that binds it. This is most of early-stage biosensor and enzyme design.
Part D: Scaffolding a Catalytic Site¶
Parts B and C held a whole molecule fixed. Here only a few scattered residues are held, and the model must invent a protein that positions them correctly relative to each other.
An enzyme active site is a few residues held in an exact geometry. The task is to invent a protein that holds them there.
The input carries a contig string:
10-20, A38, 15-30, A14, 15-30, A99, 10-20
Read it left to right: 10 to 20 new residues, then keep residue A38 exactly where it is, then 15 to 30 new residues, keep A14, 15 to 30 new, keep A99, then 10 to 20 new. Three anchors. The model invents everything between them and chooses the linker lengths itself.
This contig format is the one you will reuse in Part K for designs of your own.
In the view below, the three held residues are red and everything the model invented is grey.
describe_cif(inputs["motif_scaffolding"])
motif = client.design(
task="motif_scaffolding",
inp=annotated_cif_input(inputs["motif_scaffolding"], example_id="1qjg"),
sampling={"recycle_strategy": "thinking_medium", "num_replicates": 1},
metrics={"tags": ["design"]},
output={"structure_format": "mmcif", "include_sequence": True},
)
summarize(motif)
# Red: the three motif residues, held in exact geometry.
# Grey: everything the model invented to hold them there.
view_conditioned(cif_of(motif), fixed_color="red", free_color="lightgrey")
atoms 28 ATOM / 4 HETATM chains=['A']
contig 10-20,A38,15-30,A14,15-30,A99,10-20
design segs 4 ranges=[('10', '20'), ('15', '30'), ('15', '30'), ('10', '20')]
condition_hbond 0 / 32 set
condition_index-group 0 / 32 set
mask_coordinate 28 / 32 set
mask_expsegmax 4 / 32 set
mask_expsegmin 4 / 32 set
mask_index 32 / 32 set
mask_sequence 28 / 32 set
[429] rate limited — retry 1/4 in 5s
request_id local task=motif_scaffolding
wall time 1.2 s for 1 design(s)
backend bioir dtype=bfloat16 gpu=NVIDIA H200
oracle [] (none mounted)
schedule 60 steps / 185 trunk passes strategy=thinking_medium
design_0 origin=sample tokens=73 finite=True
MKIKKKIYYIEGKNPKYPDVDSDIIYEIEANSLEEAKEKYYKEMKKINPNFKKEDIDKDKWKEKEIEIEIKEK (len 73)
bond_length_metrics.bond_length.frac_unreasonable = 0.0
bond_length_metrics.bond_length.frac_unreasonable.between_chain.nonpolymer_to_polymer = None
bond_length_metrics.bond_length.frac_unreasonable.between_chain.polymer_polymer = None
bond_length_metrics.bond_length.frac_unreasonable.between_residue.within_chain = 0.0
bond_length_metrics.bond_length.frac_unreasonable.within_residue = 0.0
clash_metrics.frac_atoms_clashing_1p5A = 0.0
clash_metrics.frac_atoms_clashing_inter_residue_ligand_ligand_1p5A = 0.0
clash_metrics.frac_atoms_clashing_inter_residue_polymer_ligand_1p5A = 0.0
clash_metrics.frac_atoms_clashing_inter_residue_polymer_polymer_1p5A = 0.0
clash_metrics.frac_atoms_clashing_intra_residue_ligand_1p5A = 0.0
clash_metrics.frac_atoms_clashing_intra_residue_polymer_1p5A = 0.0
design_motif_allatom_rmsd.rmsd = 0.1743185967206955
sequence_adherence.n_violations = 0.0
sequence_adherence.sequence_adherence = 1.0
sequence_probability_adherence.n_one_conditions = 3.0
sequence_probability_adherence.n_one_violations = 0.0
sequence_probability_adherence.n_zero_conditions = 81.0
sequence_probability_adherence.n_zero_violations = 0.0
sequence_probability_adherence.one_adherence = 1.0
sequence_probability_adherence.zero_adherence = 1.0
sse_distribution.helix_fraction = 0.2876712328767123
sse_distribution.helix_fraction_coi = 0.2876712328767123
sse_distribution.loop_fraction = 0.4246575342465753
sse_distribution.loop_fraction_coi = 0.4246575342465753
sse_distribution.num_ss_elements = 9.0
sse_distribution.num_ss_elements_coi = 9.0
sse_distribution.sheet_fraction = 0.2876712328767123
sse_distribution.sheet_fraction_coi = 0.2876712328767123
aggregated:
bond_length_metrics.bond_length.frac_unreasonable mean 0.0 (n=1)
bond_length_metrics.bond_length.frac_unreasonable.between_residue.within_chain mean 0.0 (n=1)
bond_length_metrics.bond_length.frac_unreasonable.within_residue mean 0.0 (n=1)
clash_metrics.frac_atoms_clashing_1p5A mean 0.0 (n=1)
clash_metrics.frac_atoms_clashing_inter_residue_ligand_ligand_1p5A mean 0.0 (n=1)
clash_metrics.frac_atoms_clashing_inter_residue_polymer_ligand_1p5A mean 0.0 (n=1)
clash_metrics.frac_atoms_clashing_inter_residue_polymer_polymer_1p5A mean 0.0 (n=1)
clash_metrics.frac_atoms_clashing_intra_residue_ligand_1p5A mean 0.0 (n=1)
clash_metrics.frac_atoms_clashing_intra_residue_polymer_1p5A mean 0.0 (n=1)
design_motif_allatom_rmsd.rmsd median 0.1743185967206955 (n=1)
sequence_adherence.n_violations mean 0.0 (n=1)
sequence_adherence.sequence_adherence mean 1.0 (n=1)
sequence_probability_adherence.n_one_conditions mean 3.0 (n=1)
sequence_probability_adherence.n_one_violations mean 0.0 (n=1)
sequence_probability_adherence.n_zero_conditions mean 81.0 (n=1)
sequence_probability_adherence.n_zero_violations mean 0.0 (n=1)
sequence_probability_adherence.one_adherence mean 1.0 (n=1)
sequence_probability_adherence.zero_adherence mean 1.0 (n=1)
sse_distribution.helix_fraction mean 0.2876712328767123 (n=1)
sse_distribution.helix_fraction_coi mean 0.2876712328767123 (n=1)
sse_distribution.loop_fraction mean 0.4246575342465753 (n=1)
sse_distribution.loop_fraction_coi mean 0.4246575342465753 (n=1)
sse_distribution.num_ss_elements mean 9.0 (n=1)
sse_distribution.num_ss_elements_coi mean 9.0 (n=1)
sse_distribution.sheet_fraction mean 0.2876712328767123 (n=1)
sse_distribution.sheet_fraction_coi mean 0.2876712328767123 (n=1)
chain A: 3 held fixed (red), 70 designed (lightgrey)
3Dmol.js failed to load for some reason. Please check your browser console for error messages.
Letting the model place the motif¶
enzyme_design scaffolds a catalytic site without telling the model where the residues go
in the sequence. The model decides the placement itself.
describe_cif(inputs["enzyme_design"])
enzyme = client.design(
task="enzyme_design",
inp=annotated_cif_input(inputs["enzyme_design"], example_id="enzyme"),
sampling={"recycle_strategy": "thinking_medium", "num_replicates": 1},
metrics={"tags": ["design"]},
output={"structure_format": "mmcif", "include_sequence": True},
)
summarize(enzyme)
print("\nnum_unindexed_insertions =",
enzyme["designs"][0]["diagnostics"]["num_unindexed_insertions"],
"<- motif residues the model placed on its own")
# Red: the catalytic residues. Grey: the scaffold built around them.
view_conditioned(cif_of(enzyme), fixed_color="red", free_color="lightgrey")
atoms 2 ATOM / 13 HETATM chains=['A', 'B', 'C']
contig 180-180
design segs 1 ranges=[('180', '180')]
condition_hbond 0 / 15 set
condition_index-group 14 / 15 set
mask_coordinate 7 / 15 set
mask_expsegmax 1 / 15 set
mask_expsegmin 1 / 15 set
mask_index 13 / 15 set
mask_sequence 14 / 15 set
request_id local task=enzyme_design
wall time 1.8 s for 1 design(s)
backend bioir dtype=bfloat16 gpu=NVIDIA H200
oracle [] (none mounted)
schedule 60 steps / 185 trunk passes strategy=thinking_medium
design_0 origin=sample tokens=193 finite=True
AAVLHTGAAPVVLVAVPDYTSADGAAAVAAAIAAGGDAVQVVGAAGIDYAAMAAAAVAALAAGAFAAVVVADVGFAVAPA... (len 192)
bond_length_metrics.bond_length.frac_unreasonable = 0.0
bond_length_metrics.bond_length.frac_unreasonable.between_chain.nonpolymer_to_polymer = None
bond_length_metrics.bond_length.frac_unreasonable.between_chain.polymer_polymer = None
bond_length_metrics.bond_length.frac_unreasonable.between_residue.within_chain = 0.0
bond_length_metrics.bond_length.frac_unreasonable.within_residue = 0.0
clash_metrics.frac_atoms_clashing_1p5A = 0.0
clash_metrics.frac_atoms_clashing_inter_residue_ligand_ligand_1p5A = 0.0
clash_metrics.frac_atoms_clashing_inter_residue_polymer_ligand_1p5A = 0.0
clash_metrics.frac_atoms_clashing_inter_residue_polymer_polymer_1p5A = 0.0
clash_metrics.frac_atoms_clashing_intra_residue_ligand_1p5A = 0.0
clash_metrics.frac_atoms_clashing_intra_residue_polymer_1p5A = 0.0
design_motif_allatom_rmsd.rmsd = 0.5915946960449219
ligand_contacts.frac_atoms_contacting_4p5A = 1.0
sequence_adherence.n_violations = 0.0
sequence_adherence.sequence_adherence = 1.0
sequence_probability_adherence.n_one_conditions = 0.0
sequence_probability_adherence.n_one_violations = 0.0
sequence_probability_adherence.n_zero_conditions = 56.0
sequence_probability_adherence.n_zero_violations = 0.0
sequence_probability_adherence.one_adherence = None
sequence_probability_adherence.zero_adherence = 1.0
sse_distribution.helix_fraction = 0.5
sse_distribution.helix_fraction_coi = 0.5
sse_distribution.loop_fraction = 0.3277777777777778
sse_distribution.loop_fraction_coi = 0.3277777777777778
sse_distribution.num_ss_elements = 25.0
sse_distribution.num_ss_elements_coi = 25.0
sse_distribution.sheet_fraction = 0.17222222222222222
sse_distribution.sheet_fraction_coi = 0.17222222222222222
aggregated:
bond_length_metrics.bond_length.frac_unreasonable mean 0.0 (n=1)
bond_length_metrics.bond_length.frac_unreasonable.between_residue.within_chain mean 0.0 (n=1)
bond_length_metrics.bond_length.frac_unreasonable.within_residue mean 0.0 (n=1)
clash_metrics.frac_atoms_clashing_1p5A mean 0.0 (n=1)
clash_metrics.frac_atoms_clashing_inter_residue_ligand_ligand_1p5A mean 0.0 (n=1)
clash_metrics.frac_atoms_clashing_inter_residue_polymer_ligand_1p5A mean 0.0 (n=1)
clash_metrics.frac_atoms_clashing_inter_residue_polymer_polymer_1p5A mean 0.0 (n=1)
clash_metrics.frac_atoms_clashing_intra_residue_ligand_1p5A mean 0.0 (n=1)
clash_metrics.frac_atoms_clashing_intra_residue_polymer_1p5A mean 0.0 (n=1)
design_motif_allatom_rmsd.rmsd median 0.5915946960449219 (n=1)
ligand_contacts.frac_atoms_contacting_4p5A mean 1.0 (n=1)
sequence_adherence.n_violations mean 0.0 (n=1)
sequence_adherence.sequence_adherence mean 1.0 (n=1)
sequence_probability_adherence.n_one_conditions mean 0.0 (n=1)
sequence_probability_adherence.n_one_violations mean 0.0 (n=1)
sequence_probability_adherence.n_zero_conditions mean 56.0 (n=1)
sequence_probability_adherence.n_zero_violations mean 0.0 (n=1)
sequence_probability_adherence.zero_adherence mean 1.0 (n=1)
sse_distribution.helix_fraction mean 0.5 (n=1)
sse_distribution.helix_fraction_coi mean 0.5 (n=1)
sse_distribution.loop_fraction mean 0.3277777777777778 (n=1)
sse_distribution.loop_fraction_coi mean 0.3277777777777778 (n=1)
sse_distribution.num_ss_elements mean 25.0 (n=1)
sse_distribution.num_ss_elements_coi mean 25.0 (n=1)
sse_distribution.sheet_fraction mean 0.17222222222222222 (n=1)
sse_distribution.sheet_fraction_coi mean 0.17222222222222222 (n=1)
num_unindexed_insertions = 1 <- motif residues the model placed on its own
chain A: 0 held fixed (red), 180 designed (lightgrey)
chain B: 1 held fixed (red), 0 designed (lightgrey)
chain C: 1 held fixed (red), 0 designed (lightgrey)
3Dmol.js failed to load for some reason. Please check your browser console for error messages.
Your turn 3 — read the problem before solving it. This cell makes no API call. Run it, then answer:
- How many residues are held fixed in
motif_scaffolding, and how many is the model free to invent? - In
enzyme_design, what doesnum_unindexed_insertionsgreater than 0 tell you about who chose where the catalytic residues sit?
# Your turn 3
for name in ("motif_scaffolding", "enzyme_design"):
print(f"=== {name} ===")
describe_cif(inputs[name])
print()
=== motif_scaffolding ===
atoms 28 ATOM / 4 HETATM chains=['A']
contig 10-20,A38,15-30,A14,15-30,A99,10-20
design segs 4 ranges=[('10', '20'), ('15', '30'), ('15', '30'), ('10', '20')]
condition_hbond 0 / 32 set
condition_index-group 0 / 32 set
mask_coordinate 28 / 32 set
mask_expsegmax 4 / 32 set
mask_expsegmin 4 / 32 set
mask_index 32 / 32 set
mask_sequence 28 / 32 set
=== enzyme_design ===
atoms 2 ATOM / 13 HETATM chains=['A', 'B', 'C']
contig 180-180
design segs 1 ranges=[('180', '180')]
condition_hbond 0 / 15 set
condition_index-group 14 / 15 set
mask_coordinate 7 / 15 set
mask_expsegmax 1 / 15 set
mask_expsegmin 1 / 15 set
mask_index 13 / 15 set
mask_sequence 14 / 15 set
Part E: Joining Two Domains With a Designed Linker¶
Part D held three single residues. The same contig machinery handles something you are much more likely to need: joining two whole folded domains.
Tagging a protein of interest with GFP is the standard case. You know both structures. The question is what joins them, and how long it has to be. Too short and the tag cannot reach, or it distorts the domain it is reporting on. Too long and the two domains tumble independently, which costs you rigidity and adds protease-susceptible sequence.
This is also a solubility experiment in disguise. A GFP fusion only fluoresces if the GFP folds, which is why fluorescence-on-expression is a standard readout for whether the partner behaved. Keep that in mind for the discussion in Part G.
How the two domains are held. A new conditioning column does the work here:
| Column | Meaning |
|---|---|
mask_coordinate |
this atom's absolute position is given |
mask_InternalCoordinate |
this atom's position relative to its group is given |
condition_CoordinateGroup |
which rigid body the atom belongs to, or -1 for none |
A domain with mask_coordinate False, mask_InternalCoordinate True and its own
CoordinateGroup is a rigid body: the model may rotate and translate it but not
distort it. That is what lets the linker decide where GFP ends up, instead of you fixing
its position in advance.
author_fusion pins the first domain, makes the second a rigid body, and designs only the
residues between them.
# Ubiquitin as a stand-in protein of interest, with GFP as the tag.
# "1UBQ" is a PDB id; "P42212" is a UniProt accession, which fetches the
# AlphaFold model. The model may use between 5 and 25 residues to join them.
fusion_input = author_fusion("1UBQ", "A", "P42212", "A", linker=(5, 25))
describe_cif(fusion_input, label="ubiquitin - linker - GFP")
=== ubiquitin - linker - GFP ===
atoms 2498 ATOM / 1 HETATM chains=['A']
contig A1-76,5-25,A1-238
design segs 1 ranges=[('5', '25')]
condition_CoordinateGroup 1897 / 2499 set
condition_index-group 0 / 2499 set
mask_InternalCoordinate 1896 / 2499 set
mask_coordinate 602 / 2499 set
mask_expsegmax 1 / 2499 set
mask_expsegmin 1 / 2499 set
mask_hotspot 0 / 2499 set
mask_index 2499 / 2499 set
mask_sequence 2498 / 2499 set
{'columns': ['group_PDB',
'type_symbol',
'label_atom_id',
'label_alt_id',
'label_comp_id',
'label_asym_id',
'label_entity_id',
'label_seq_id',
'pdbx_PDB_ins_code',
'auth_seq_id',
'auth_comp_id',
'auth_asym_id',
'auth_atom_id',
'id',
'B_iso_or_equiv',
'occupancy',
'pdbx_formal_charge',
'mask_index_1_residue',
'mask_hotspot_1_atom',
'mask_InternalCoordinate_1_atom',
'mask_sequence_1_residue',
'condition_index-group_1_token',
'condition_CoordinateGroup_1_atom',
'mask_coordinate_1_atom',
'annotation_expsegmax_1_residue',
'mask_expsegmax_1_residue',
'mask_expsegmin_1_residue',
'annotation_expsegmin_1_residue',
'atom_array_index',
'Cartn_x',
'Cartn_y',
'Cartn_z',
'pdbx_PDB_model_num'],
'n_rows': 2499,
'contig': 'A1-76,5-25,A1-238',
'flags': {'mask_index_1_residue': (2499, 2499),
'mask_hotspot_1_atom': (0, 2499),
'mask_InternalCoordinate_1_atom': (1896, 2499),
'mask_sequence_1_residue': (2498, 2499),
'condition_index-group_1_token': (0, 2499),
'condition_CoordinateGroup_1_atom': (1897, 2499),
'mask_coordinate_1_atom': (602, 2499),
'mask_expsegmax_1_residue': (1, 2499),
'mask_expsegmin_1_residue': (1, 2499)}}
Read the counts. Every residue of both domains has its sequence held. One segment carries
an expandable 5 to 25 range. The CoordinateGroup column splits the atoms into the two
rigid bodies.
Note on structures. The tag above is an AlphaFold model rather than a crystal
structure. Crystal structures of GFP have disordered loops, and its chromophore is a
non-standard residue, so the chain they produce has gaps in it. The server requires a
continuous backbone, and author_fusion checks for breaks before you send anything.
Now design the linker.
linked = client.design(
task="motif_scaffolding",
inp=annotated_cif_input(fusion_input, example_id="fusion"),
sampling={"recycle_strategy": "thinking_medium", "num_replicates": 1},
metrics={"tags": ["design"]},
output={"structure_format": "mmcif", "include_sequence": True},
)
summarize(linked)
# Red: the two domains, held. Grey: the linker the model designed.
view_conditioned(cif_of(linked), fixed_color="red", free_color="lightgrey")
request_id local task=motif_scaffolding
wall time 2.7 s for 1 design(s)
backend bioir dtype=bfloat16 gpu=NVIDIA H200
oracle [] (none mounted)
schedule 60 steps / 185 trunk passes strategy=thinking_medium
design_0 origin=sample tokens=320 finite=True
MQIFVKTLTGKTITLEVEPSDTIENVKAKIQDKEGIPPDQQRLIFAGKQLEDGRTLSDYNIQKESTLHLVLRLRGGGGGG... (len 320)
bond_length_metrics.bond_length.frac_unreasonable = 0.0
bond_length_metrics.bond_length.frac_unreasonable.between_chain.nonpolymer_to_polymer = None
bond_length_metrics.bond_length.frac_unreasonable.between_chain.polymer_polymer = None
bond_length_metrics.bond_length.frac_unreasonable.between_residue.within_chain = 0.0
bond_length_metrics.bond_length.frac_unreasonable.within_residue = 0.0
clash_metrics.frac_atoms_clashing_1p5A = 0.0
clash_metrics.frac_atoms_clashing_inter_residue_ligand_ligand_1p5A = 0.0
clash_metrics.frac_atoms_clashing_inter_residue_polymer_ligand_1p5A = 0.0
clash_metrics.frac_atoms_clashing_inter_residue_polymer_polymer_1p5A = 0.0
clash_metrics.frac_atoms_clashing_intra_residue_ligand_1p5A = 0.0
clash_metrics.frac_atoms_clashing_intra_residue_polymer_1p5A = 0.0
design_motif_allatom_rmsd.rmsd = 0.604200005531311
sequence_adherence.n_violations = 0.0
sequence_adherence.sequence_adherence = 1.0
sequence_probability_adherence.n_one_conditions = 314.0
sequence_probability_adherence.n_one_violations = 0.0
sequence_probability_adherence.n_zero_conditions = 8478.0
sequence_probability_adherence.n_zero_violations = 0.0
sequence_probability_adherence.one_adherence = 1.0
sequence_probability_adherence.zero_adherence = 1.0
sse_distribution.helix_fraction = 0.05625
sse_distribution.helix_fraction_coi = 0.05625
sse_distribution.loop_fraction = 0.5125
sse_distribution.loop_fraction_coi = 0.5125
sse_distribution.num_ss_elements = 39.0
sse_distribution.num_ss_elements_coi = 39.0
sse_distribution.sheet_fraction = 0.43125
sse_distribution.sheet_fraction_coi = 0.43125
aggregated:
bond_length_metrics.bond_length.frac_unreasonable mean 0.0 (n=1)
bond_length_metrics.bond_length.frac_unreasonable.between_residue.within_chain mean 0.0 (n=1)
bond_length_metrics.bond_length.frac_unreasonable.within_residue mean 0.0 (n=1)
clash_metrics.frac_atoms_clashing_1p5A mean 0.0 (n=1)
clash_metrics.frac_atoms_clashing_inter_residue_ligand_ligand_1p5A mean 0.0 (n=1)
clash_metrics.frac_atoms_clashing_inter_residue_polymer_ligand_1p5A mean 0.0 (n=1)
clash_metrics.frac_atoms_clashing_inter_residue_polymer_polymer_1p5A mean 0.0 (n=1)
clash_metrics.frac_atoms_clashing_intra_residue_ligand_1p5A mean 0.0 (n=1)
clash_metrics.frac_atoms_clashing_intra_residue_polymer_1p5A mean 0.0 (n=1)
design_motif_allatom_rmsd.rmsd median 0.604200005531311 (n=1)
sequence_adherence.n_violations mean 0.0 (n=1)
sequence_adherence.sequence_adherence mean 1.0 (n=1)
sequence_probability_adherence.n_one_conditions mean 314.0 (n=1)
sequence_probability_adherence.n_one_violations mean 0.0 (n=1)
sequence_probability_adherence.n_zero_conditions mean 8478.0 (n=1)
sequence_probability_adherence.n_zero_violations mean 0.0 (n=1)
sequence_probability_adherence.one_adherence mean 1.0 (n=1)
sequence_probability_adherence.zero_adherence mean 1.0 (n=1)
sse_distribution.helix_fraction mean 0.05625 (n=1)
sse_distribution.helix_fraction_coi mean 0.05625 (n=1)
sse_distribution.loop_fraction mean 0.5125 (n=1)
sse_distribution.loop_fraction_coi mean 0.5125 (n=1)
sse_distribution.num_ss_elements mean 39.0 (n=1)
sse_distribution.num_ss_elements_coi mean 39.0 (n=1)
sse_distribution.sheet_fraction mean 0.43125 (n=1)
sse_distribution.sheet_fraction_coi mean 0.43125 (n=1)
chain A: 314 held fixed (red), 6 designed (lightgrey)
3Dmol.js failed to load for some reason. Please check your browser console for error messages.
# How long a linker did it choose, and what distance does it cover?
import math
split = conditioning_of(cif_of(linked))["A"]
designed = split["free"]
cols, rows = parse_atom_site(cif_of(linked))
ca = {int(r["auth_seq_id"]): (float(r["Cartn_x"]), float(r["Cartn_y"]),
float(r["Cartn_z"]))
for r in rows if r["label_atom_id"] == "CA"}
first, last = min(designed) - 1, max(designed) + 1
span = math.dist(ca[first], ca[last])
print(f"linker length {len(designed)} residues (you allowed 5-25)")
print(f"distance spanned {span:.1f} A")
print(f"per residue {span / len(designed):.2f} A")
print()
print("For reference: a fully extended chain covers about 3.5 A per residue,")
print("an alpha helix about 1.5 A. Where does yours sit, and what does that")
print("tell you about the conformation it chose?")
linker length 6 residues (you allowed 5-25) distance spanned 15.7 A per residue 2.62 A For reference: a fully extended chain covers about 3.5 A per residue, an alpha helix about 1.5 A. Where does yours sit, and what does that tell you about the conformation it chose?
Your turn 4 — change the linker budget. Re-run the two cells above with
linker=(2, 4) and then with linker=(40, 60). Record the length chosen, the distance
spanned, and the clash fraction each time.
Questions:
- At
(2, 4)the model cannot reach a comfortable geometry. What does it do instead: refuse, distort something, or accept a strained bond? Checkdesign_motif_allatom_rmsdagainst the earlier run, since that measures how far the held domains were pushed. - At
(40, 60), what secondary structure does the linker adopt, and is a long linker actually free? Compareloop_fraction. - Common practice is a glycine-serine repeat such as (GGGGS)x3. The model was not told to use one. Look at the linker sequence it produced: what residues did it choose, and what would you expect each choice to do to flexibility and protease sensitivity?
- This design has not been validated. Part L folds a designed sequence with an independent model; a fusion is a good candidate for that check, because the thing you most want to know is whether the GFP still folds.
Part F: Antibody Loops¶
Part D held a few residues and designed everything around them. This is the same idea with the proportions reversed: most of the protein is held, and only short stretches are redesigned.
Antibodies recognise their target through six short loops, the CDRs. The rest is a constant framework. So the design problem is: freeze the framework, freeze the antigen, redesign the loops.
This example is a nanobody, a single-domain antibody from llamas, against PD-L1.
In the view below, the framework held fixed is blue, the redesigned CDR loops are orange, and the antigen is grey.
vhh = client.design(
task="antibody_design",
inp=annotated_cif_input(inputs["nanobody"], example_id="vhh"),
sampling={"recycle_strategy": "thinking_medium", "num_replicates": 1},
metrics={"tags": ["design"]},
output={"structure_format": "mmcif", "include_sequence": True},
)
summarize(vhh)
print("chains:", chains_in(cif_of(vhh)))
# Blue: the framework, supplied and held fixed.
# Orange: the CDR loops the model redesigned.
# Grey: PD-L1, the antigen.
view_conditioned(cif_of(vhh), fixed_color="blue", free_color="orange",
context_chains=["A"])
request_id local task=antibody_design
wall time 2.0 s for 1 design(s)
backend bioir dtype=bfloat16 gpu=NVIDIA H200
oracle [] (none mounted)
schedule 60 steps / 185 trunk passes strategy=thinking_medium
design_0 origin=sample tokens=228 finite=True
FTVTVPKDLYVVEYGSNMTIECKFPVEKQLDLAALIVYWEMEDKNIIQFVHGEEDLKVQHSSYRQRARLLKDQLSLGNAA... (len 228)
bond_length_metrics.bond_length.frac_unreasonable = 0.0
bond_length_metrics.bond_length.frac_unreasonable.between_chain.nonpolymer_to_polymer = None
bond_length_metrics.bond_length.frac_unreasonable.between_chain.polymer_polymer = None
bond_length_metrics.bond_length.frac_unreasonable.between_residue.within_chain = 0.0
bond_length_metrics.bond_length.frac_unreasonable.within_residue = 0.0
clash_metrics.frac_atoms_clashing_1p5A = 0.0
clash_metrics.frac_atoms_clashing_inter_residue_ligand_ligand_1p5A = 0.0
clash_metrics.frac_atoms_clashing_inter_residue_polymer_ligand_1p5A = 0.0
clash_metrics.frac_atoms_clashing_inter_residue_polymer_polymer_1p5A = 0.0
clash_metrics.frac_atoms_clashing_intra_residue_ligand_1p5A = 0.0
clash_metrics.frac_atoms_clashing_intra_residue_polymer_1p5A = 0.0
design_motif_allatom_rmsd.rmsd = 0.2129235416650772
sequence_adherence.n_violations = 0.0
sequence_adherence.sequence_adherence = 1.0
sequence_probability_adherence.n_one_conditions = 196.0
sequence_probability_adherence.n_one_violations = 0.0
sequence_probability_adherence.n_zero_conditions = 5292.0
sequence_probability_adherence.n_zero_violations = 0.0
sequence_probability_adherence.one_adherence = 1.0
sequence_probability_adherence.zero_adherence = 1.0
sse_distribution.helix_fraction = 0.03070175438596491
sse_distribution.helix_fraction_coi = 0.0
sse_distribution.loop_fraction = 0.5877192982456141
sse_distribution.loop_fraction_coi = 0.6
sse_distribution.num_ss_elements = 33.0
sse_distribution.num_ss_elements_coi = 17.0
sse_distribution.sheet_fraction = 0.3815789473684211
sse_distribution.sheet_fraction_coi = 0.4
aggregated:
bond_length_metrics.bond_length.frac_unreasonable mean 0.0 (n=1)
bond_length_metrics.bond_length.frac_unreasonable.between_residue.within_chain mean 0.0 (n=1)
bond_length_metrics.bond_length.frac_unreasonable.within_residue mean 0.0 (n=1)
clash_metrics.frac_atoms_clashing_1p5A mean 0.0 (n=1)
clash_metrics.frac_atoms_clashing_inter_residue_ligand_ligand_1p5A mean 0.0 (n=1)
clash_metrics.frac_atoms_clashing_inter_residue_polymer_ligand_1p5A mean 0.0 (n=1)
clash_metrics.frac_atoms_clashing_inter_residue_polymer_polymer_1p5A mean 0.0 (n=1)
clash_metrics.frac_atoms_clashing_intra_residue_ligand_1p5A mean 0.0 (n=1)
clash_metrics.frac_atoms_clashing_intra_residue_polymer_1p5A mean 0.0 (n=1)
design_motif_allatom_rmsd.rmsd median 0.2129235416650772 (n=1)
sequence_adherence.n_violations mean 0.0 (n=1)
sequence_adherence.sequence_adherence mean 1.0 (n=1)
sequence_probability_adherence.n_one_conditions mean 196.0 (n=1)
sequence_probability_adherence.n_one_violations mean 0.0 (n=1)
sequence_probability_adherence.n_zero_conditions mean 5292.0 (n=1)
sequence_probability_adherence.n_zero_violations mean 0.0 (n=1)
sequence_probability_adherence.one_adherence mean 1.0 (n=1)
sequence_probability_adherence.zero_adherence mean 1.0 (n=1)
sse_distribution.helix_fraction mean 0.03070175438596491 (n=1)
sse_distribution.helix_fraction_coi mean 0.0 (n=1)
sse_distribution.loop_fraction mean 0.5877192982456141 (n=1)
sse_distribution.loop_fraction_coi mean 0.6 (n=1)
sse_distribution.num_ss_elements mean 33.0 (n=1)
sse_distribution.num_ss_elements_coi mean 17.0 (n=1)
sse_distribution.sheet_fraction mean 0.3815789473684211 (n=1)
sse_distribution.sheet_fraction_coi mean 0.4 (n=1)
chains: ['A', 'H']
chain A: 113 held fixed (blue), 0 designed (orange) [context]
chain H: 83 held fixed (blue), 32 designed (orange)
3Dmol.js failed to load for some reason. Please check your browser console for error messages.
Only the loops changed. The rest is the framework we supplied. Note where the orange residues fall: three separate stretches of sequence, which are the three CDRs.
Part G: Two Ways to Control the Output¶
Parts A to E all used the same control without naming it: a constraint written into the input file. There is a second, independent control, and the rest of this notebook uses both. They act differently and fail differently.
| Conditioning | Reward-guided search | |
|---|---|---|
| What you supply | a constraint in the input: a fixed motif, a hotspot, a length | a score computed on candidate designs |
| When it acts | throughout generation, at every step | at checkpoints, deciding which trajectories continue |
| How strong | a strong bias, not a guarantee | optimises whatever you measure, including loopholes |
Conditioning says "work within this". Reward says "more of that".
Conditioning: supplying a constraint¶
You have already conditioned on a frozen target (Part B) and a fixed motif (Part D). The lightest constraint is length. There are two ways to set it:
polygly_cif(L)pins an exact length. You used this in Your turn 1.contig_cif(min, max)declares a range and lets the model choose. This uses thecondition_specinput route, anddescribe_cifwill show the contig it carries.
# Look at what a ranged contig actually declares, before sending it.
describe_cif(contig_cif(80, 140))
atoms 0 ATOM / 1 HETATM chains=['A']
contig 80-140
design segs 1 ranges=[('80', '140')]
condition_hbond 0 / 1 set
condition_index-group 0 / 1 set
mask_coordinate 0 / 1 set
mask_expsegmax 1 / 1 set
mask_expsegmin 1 / 1 set
mask_index 1 / 1 set
mask_sequence 0 / 1 set
{'columns': ['group_PDB',
'type_symbol',
'label_atom_id',
'label_alt_id',
'label_comp_id',
'label_asym_id',
'label_entity_id',
'label_seq_id',
'pdbx_PDB_ins_code',
'auth_seq_id',
'auth_comp_id',
'auth_asym_id',
'auth_atom_id',
'B_iso_or_equiv',
'occupancy',
'pdbx_formal_charge',
'mask_expsegmin_1_residue',
'annotation_src-component_1_atom',
'annotation_expsegmin_1_residue',
'mask_coordinate_1_atom',
'mask_sequence_1_residue',
'mask_index_1_residue',
'annotation_expsegmax_1_residue',
'mask_expsegmax_1_residue',
'condition_hbond_1_atom',
'condition_index-group_1_token',
'atom_array_index',
'Cartn_x',
'Cartn_y',
'Cartn_z',
'pdbx_PDB_model_num',
'id'],
'n_rows': 1,
'contig': '80-140',
'flags': {'mask_expsegmin_1_residue': (1, 1),
'mask_coordinate_1_atom': (0, 1),
'mask_sequence_1_residue': (0, 1),
'mask_index_1_residue': (1, 1),
'mask_expsegmax_1_residue': (1, 1),
'condition_hbond_1_atom': (0, 1),
'condition_index-group_1_token': (0, 1)}}
Your turn 4 — set a length range (optional). Choose a minimum and maximum, then read
the length that came back. If your deployment does not accept the condition_spec route,
the cell will say so and you can use polygly_cif(L) from Your turn 1 instead.
# Your turn 4
MIN_LEN, MAX_LEN = 80, 140 # <-- edit me
spec_cif = contig_cif(MIN_LEN, MAX_LEN)
try:
ranged = client.design(
task="generation",
inp=condition_spec_input(spec_cif, example_id="yourturn-contig"),
sampling={"recycle_strategy": "thinking_medium", "num_replicates": 1},
output={"structure_format": "mmcif", "include_sequence": True},
)
d = ranged["designs"][0]
print(f"asked {MIN_LEN}-{MAX_LEN}; model chose {d['diagnostics']['num_tokens']} tokens")
view(cif_of(ranged))
except RFD4Error as e:
print("This deployment didn't accept the condition_spec route:", e)
print("Fine — use polygly_cif(L) from Your turn #1 to pin an exact length instead.")
[429] rate limited — retry 1/4 in 5s
[429] rate limited — retry 2/4 in 10s
asked 80-140; model chose 133 tokens
3Dmol.js failed to load for some reason. Please check your browser console for error messages.
Reward-guided search: supplying a score¶
At checkpoints during generation, the model branches, scores each branch with a reward you define, keeps the best, and continues. Compute is directed toward the trajectories that are scoring well. This is not "generate ten and pick one"; unpromising paths are abandoned part way through generation.
First, find out what can be scored. The cell below asks the API for the metric keys on a plain 100-residue chain. Unguided designs are strongly helical at any length, which matters shortly.
Note: the steering runs below use 150 residues rather than 100. The reason is structural. A beta sheet needs several strands lying alongside each other, and a 100-residue chain has barely enough room for one. At 100 residues the sheet reward is close to all-or-nothing; at 150, partial results become possible. Length is a design constraint, not only a cost.
describe_cif(polygly_cif(100))
# What can we actually score? Ask the API.
probe = client.design(
task="generation",
inp=annotated_cif_input(polygly_cif(100), example_id="probe"),
sampling={"recycle_strategy": "thinking_medium", "num_replicates": 1},
metrics={"tags": ["design"]},
output={"structure_format": "none"},
)
print("metrics you can reward on:")
for k in metric_keys(probe):
print(" ", k)
# three we'll reuse below
SHEET = "sse_distribution.sheet_fraction"
HELIX = "sse_distribution.helix_fraction"
CLASH = "clash_metrics.frac_atoms_clashing_1p5A"
atoms 400 ATOM / 0 HETATM chains=['A'] contig (none)
[429] rate limited — retry 1/4 in 5s
metrics you can reward on: bond_length_metrics.bond_length.frac_unreasonable bond_length_metrics.bond_length.frac_unreasonable.between_chain.nonpolymer_to_polymer bond_length_metrics.bond_length.frac_unreasonable.between_chain.polymer_polymer bond_length_metrics.bond_length.frac_unreasonable.between_residue.within_chain bond_length_metrics.bond_length.frac_unreasonable.within_residue clash_metrics.frac_atoms_clashing_1p5A clash_metrics.frac_atoms_clashing_inter_residue_ligand_ligand_1p5A clash_metrics.frac_atoms_clashing_inter_residue_polymer_ligand_1p5A clash_metrics.frac_atoms_clashing_inter_residue_polymer_polymer_1p5A clash_metrics.frac_atoms_clashing_intra_residue_ligand_1p5A clash_metrics.frac_atoms_clashing_intra_residue_polymer_1p5A sequence_adherence.n_violations sequence_adherence.sequence_adherence sequence_probability_adherence.n_one_conditions sequence_probability_adherence.n_one_violations sequence_probability_adherence.n_zero_conditions sequence_probability_adherence.n_zero_violations sequence_probability_adherence.one_adherence sequence_probability_adherence.zero_adherence sse_distribution.helix_fraction sse_distribution.helix_fraction_coi sse_distribution.loop_fraction sse_distribution.loop_fraction_coi sse_distribution.num_ss_elements sse_distribution.num_ss_elements_coi sse_distribution.sheet_fraction sse_distribution.sheet_fraction_coi
What you can steer on, and how¶
The cell above printed every metric the server will compute for this design. Those keys are the vocabulary available to a reward. They fall into a few groups:
| Group | Example keys | What it measures |
|---|---|---|
| Secondary structure | sse_distribution.helix_fraction, .sheet_fraction, .num_ss_elements |
what fold the design adopts |
| Geometry quality | clash_metrics.frac_atoms_clashing_1p5A, bond_length_metrics.* |
whether the structure is physically sane |
| Conditioning obedience | hotspot_obedience.hotspot_obedience, sequence_adherence.* |
whether the model respected your input |
Which keys appear depends on what you asked for. metrics={"tags": ["design"]} gives the
general set above. Other tags add families: hotspot_conditioning adds hotspot obedience,
developability adds properties relevant to expression, interchain_interactions adds
contacts between chains. Tags whose names end in _refolding require a separate structure
prediction model mounted on the server, which the hosted endpoint does not have; asking for
them returns an error.
A reward is then built from four decisions:
- Which keys, and in which direction.
weightsmaps a key to a number. Positive asks for more, negative for less. The magnitude matters relative to the model's own prior: a weight of 1.0 on a property the model already avoids is a weak request. - When to act.
step_checkpointslists the steps at which the model branches and prunes. Early checkpoints act while the fold is still forming; by the late steps the topology is largely fixed. - How hard to select.
n_branchis the number of children per survivor andbeam_widthis how many survive. The candidate pool at each checkpoint isbeam_width × n_branch, and the total cost isbeam_width × n_branch × (number of checkpoints − 1)trajectories per replicate. The response reports the true figure insearch.rollouts, which is worth checking against your expectation. - Where to stop caring.
clip_valuesbounds a metric before it is weighted, so a reward stops rewarding improvement past a point you consider good enough.
Note that 2/2 and 4/1 cost the same compute. They differ in how much of the pool
survives, not in how much work is done.
The next cell puts this together.
The search block below reads as follows:
step_checkpoints: [0, 30, 60, 120]— branch and prune at these four points.weights: {SHEET: 1.0, CLASH: -2.0}— reward sheet, penalise clashes twice as heavily.clip_values— stop counting improvement once clashes are below 2%.n_branch: 2, beam_width: 2— each of the 2 survivors produces 2 children, so 4 candidates are scored at each checkpoint and the best 2 continue.
n_branch is the number of children per survivor, so the candidate pool at each checkpoint
is beam_width × n_branch. You will check this number against the API shortly.
The cell runs the steered design and an unsteered one for comparison.
steered = client.design(
task="generation",
inp=annotated_cif_input(polygly_cif(150), example_id="sheety"),
sampling={"num_steps": 120, "recycle_strategy": "thinking_medium",
"num_replicates": 2, "batch_size": 1},
search={
"step_checkpoints": [0, 30, 60, 120],
"reward_metrics_tags": ["design"],
"weights": {SHEET: 1.0, CLASH: -2.0},
"clip_values": {CLASH: {"min": 0.0, "max": 0.02}},
"n_branch": 2, "beam_width": 2,
},
metrics={"tags": ["design"]},
output={"structure_format": "mmcif", "include_sequence": True},
)
plain = client.design(
task="generation",
inp=annotated_cif_input(polygly_cif(150), example_id="unsteered"),
sampling={"num_steps": 120, "recycle_strategy": "thinking_medium", "num_replicates": 1},
metrics={"tags": ["design"]},
output={"structure_format": "mmcif", "include_sequence": True},
)
metric_table({"unsteered": plain, "steered -> sheet": steered}, [SHEET, HELIX])
print("\nUNSTEERED"); view(cif_of(plain))
print("STEERED toward sheet"); view(cif_of(steered))
[429] rate limited — retry 1/4 in 5s
[429] rate limited — retry 2/4 in 10s
run sheet_fraction helix_fraction -------------------------------------------------- unsteered 0.000 0.893 steered -> sheet 0.000 0.920 UNSTEERED
3Dmol.js failed to load for some reason. Please check your browser console for error messages.
STEERED toward sheet
3Dmol.js failed to load for some reason. Please check your browser console for error messages.
(One sample of each. This shows the mechanism, not a statistic.)
Reading the result¶
The common outcome is that the steered protein looks much like the unsteered one, still mostly helix and coil.
This is not because the search failed to prune. The response reports how many trajectories
were actually run, in search.rollouts:
rollouts = beam_width × n_branch × (number of checkpoints − 1)
With 2/2 and three checkpoints that is 8 rollouts to return 2 designs. Four candidates
were scored at each checkpoint and half were discarded, so pruning did occur. A
configuration with no selection at all would be n_branch: 1, where the pool is the same
size as the beam and nothing can be discarded. You will verify this in the next cell.
Three things do affect the outcome:
- The reward is working against the model's prior. Unguided designs are strongly helical,
so a weight of 1.0 is a weak push. Try
SHEET: 4.0with a lighterCLASH: -1.0. - Checkpoint placement. By the late steps the backbone topology is largely fixed. Early,
closely spaced checkpoints (
[0, 10, 20, 35, 55, 80, 120]) act while the fold is still forming. - Selection strength.
4/1scores the same four candidates per checkpoint but keeps only one. It costs the same compute as2/2.
There is a fourth consideration that outweighs all of these, and it is the subject of the next section: a single run cannot tell you whether steering worked. Measured over 25 runs at these settings, the sheet reward moved the fold in about 40% of them. If yours did not move, that is the most likely single outcome.
The cell below runs the same reward with the search settings increased.
strong = client.design(
task="generation",
inp=annotated_cif_input(polygly_cif(150), example_id="sheet-hard"),
sampling={"num_steps": 120, "recycle_strategy": "thinking_medium",
"num_replicates": 2, "batch_size": 1},
search={
"step_checkpoints": [0, 10, 20, 35, 55, 80, 120], # early + dense
"reward_metrics_tags": ["design"],
"weights": {SHEET: 4.0, CLASH: -1.0}, # push sheet, lighter clash
"clip_values": {CLASH: {"min": 0.0, "max": 0.02}},
"n_branch": 4, "beam_width": 1, # make 4, keep the best 1
},
metrics={"tags": ["design"]},
output={"structure_format": "mmcif", "include_sequence": True},
)
metric_table({"soft search (2/2)": steered,
"hard search (4/1, early)": strong}, [SHEET, HELIX, CLASH])
print("\nOne sample. See the next section before drawing a conclusion.")
view(cif_of(strong))
run sheet_fraction helix_fraction frac_atoms_clashing_1p5A ------------------------------------------------------------------------------------ soft search (2/2) 0.000 0.920 0.000 hard search (4/1, early) 0.247 0.373 0.000 One sample. See the next section before drawing a conclusion.
3Dmol.js failed to load for some reason. Please check your browser console for error messages.
Your turn 5 — design the reward. Choose a metric from the list printed below and a direction. A positive weight asks for more of that quantity, a negative weight for less. Keep the clash penalty.
Write down your prediction first: if you push helix_fraction up, what should happen to
sheet_fraction?
# The metrics you can reward on, reprinted here for convenience:
print("metrics you can reward on:")
for k in metric_keys(probe):
print(" ", k)
print()
# Your turn 5
TARGET = "sse_distribution.helix_fraction" # <-- any key the probe printed
STRENGTH = 1.0 # <-- how hard to push (try 0.5, 2.0)
mychoice = client.design(
task="generation",
inp=annotated_cif_input(polygly_cif(150), example_id="yourturn-reward"),
sampling={"num_steps": 120, "recycle_strategy": "thinking_medium",
"num_replicates": 2, "batch_size": 1},
search={
"step_checkpoints": [0, 30, 60, 120],
"reward_metrics_tags": ["design"],
"weights": {TARGET: STRENGTH, CLASH: -2.0},
"clip_values": {CLASH: {"min": 0.0, "max": 0.02}},
"n_branch": 2, "beam_width": 2,
},
metrics={"tags": ["design"]},
output={"structure_format": "mmcif", "include_sequence": True},
)
metric_table({"your design": mychoice}, [TARGET, SHEET, HELIX, CLASH])
view(cif_of(mychoice))
metrics you can reward on:
bond_length_metrics.bond_length.frac_unreasonable
bond_length_metrics.bond_length.frac_unreasonable.between_chain.nonpolymer_to_polymer
bond_length_metrics.bond_length.frac_unreasonable.between_chain.polymer_polymer
bond_length_metrics.bond_length.frac_unreasonable.between_residue.within_chain
bond_length_metrics.bond_length.frac_unreasonable.within_residue
clash_metrics.frac_atoms_clashing_1p5A
clash_metrics.frac_atoms_clashing_inter_residue_ligand_ligand_1p5A
clash_metrics.frac_atoms_clashing_inter_residue_polymer_ligand_1p5A
clash_metrics.frac_atoms_clashing_inter_residue_polymer_polymer_1p5A
clash_metrics.frac_atoms_clashing_intra_residue_ligand_1p5A
clash_metrics.frac_atoms_clashing_intra_residue_polymer_1p5A
sequence_adherence.n_violations
sequence_adherence.sequence_adherence
sequence_probability_adherence.n_one_conditions
sequence_probability_adherence.n_one_violations
sequence_probability_adherence.n_zero_conditions
sequence_probability_adherence.n_zero_violations
sequence_probability_adherence.one_adherence
sequence_probability_adherence.zero_adherence
sse_distribution.helix_fraction
sse_distribution.helix_fraction_coi
sse_distribution.loop_fraction
sse_distribution.loop_fraction_coi
sse_distribution.num_ss_elements
sse_distribution.num_ss_elements_coi
sse_distribution.sheet_fraction
sse_distribution.sheet_fraction_coi
run helix_fraction sheet_fraction helix_fraction frac_atoms_clashing_1p5A --------------------------------------------------------------------------------------- your design 0.913 0.000 0.913 0.000
3Dmol.js failed to load for some reason. Please check your browser console for error messages.
Repeating the same run¶
You now have one steered design and one unsteered design.
Before drawing a conclusion from them, run the same reward with the same settings several times. Nothing changes between runs except the random seed.
Write down your prediction first: across several more runs, do you expect a spread of sheet fractions around the value you saw, or something else?
# The same search configuration, run N times. Only the seed differs.
N_RUNS = 3 # <-- 3 is ~2 minutes. More is better; the class will pool anyway.
runs = []
for i in range(N_RUNS):
r = client.design(
task="generation",
inp=annotated_cif_input(polygly_cif(150), example_id=f"repeat{i}"),
sampling={"num_steps": 120, "recycle_strategy": "thinking_medium",
"num_replicates": 2, "batch_size": 1},
search={
"step_checkpoints": [0, 30, 60, 120],
"reward_metrics_tags": ["design"],
"weights": {SHEET: 1.0},
"n_branch": 2, "beam_width": 2,
},
metrics={"tags": ["design"]},
output={"structure_format": "none"},
)
vals = [d["metrics"][SHEET] for d in r["designs"]]
runs.append(vals)
bar = "".join("#" if v > 0.05 else "." for v in vals)
print(f"run {i}: sheet = {[round(v, 3) for v in vals]} {bar}")
flat = [v for vals in runs for v in vals]
moved = [v for v in flat if v > 0.05]
print(f"\n{len(moved)}/{len(flat)} designs moved.", end=" ")
print(f"When they moved: {[round(v,2) for v in moved]}" if moved
else "None moved this time - that is the common outcome.")
print("\nNOTE: with num_replicates=2 and beam_width=2 you get 4 designs, but")
print("they are 2 PAIRS: each pair are the beam survivors of one replicate and")
print("come back near-identical. The two replicates differ. Effective n = 2.")
run 0: sheet = [0.0, 0.0, 0.0, 0.0] ....
run 1: sheet = [0.0, 0.0, 0.08, 0.08] ..##
run 2: sheet = [0.0, 0.0, 0.0, 0.0] .... 2/12 designs moved. When they moved: [0.08, 0.08] NOTE: with num_replicates=2 and beam_width=2 you get 4 designs, but they are 2 PAIRS: each pair are the beam survivors of one replicate and come back near-identical. The two replicates differ. Effective n = 2.
What the repeats show¶
The reward produces a graded response. At 150 residues the measured sheet fractions land anywhere from 0.03 through 0.17, 0.25 and 0.40 up to 0.75. As sheet rises, helix falls from about 0.89 toward 0.57, 0.37 and in one case 0.09. The reward is not a switch.
At 100 residues it behaves more like a switch: when it moves at all it jumps to about 0.22, with nothing in between. This is the length effect described above, where the chain is too short to host a partial sheet.
The result is unreliable from run to run. Measured over 25 runs at these settings, about 40% moved. A single run therefore tells you little, and the most likely single outcome is that steering appears to do nothing, which is not what the pooled data shows.
Pooling the class results¶
Post your numbers to the shared sheet, one row per call, recording your sheet fraction. Thirty students produce about 30 samples in a minute. Look at the distribution together, then answer:
- What does the pooled distribution look like, and what would you have concluded from your own run alone?
- If you were publishing a method that works about 40% of the time, how many runs would you need before the number you report means anything? What would the error bars on a 10-run estimate look like?
- This is the same question as deciding how many designs to order from a gene synthesis company, where it costs real money.
Reward hacking, and what we could not demonstrate¶
The slides described how a reward can be satisfied in ways you did not intend.
We attempted to demonstrate this here and did not succeed. Across six reward
configurations — sheet alone, sheet at weight 4.0, maximise helix, maximise loop, maximise
the number of secondary structure elements — the clash fraction came back as 0.00000
every time. The model did not produce the cheap, broken, high-scoring structures the
demonstration needed.
The reason is worth understanding. Geometric rewards of this kind are measurements, computed directly from coordinates by code with no free parameters. There is very little distance between the number and the property it represents.
A solubility predictor is different. It is a learned model, trained on finite data, with its own shortcuts. If it learned that bacterial-like sequences score highly, rather than that well-behaved proteins score highly, then search will produce bacterial-looking sequences, score them 0.99, and the problem will not surface until the expression plate three weeks later.
Questions to discuss:
- Why is a clash count harder to hack than a solubility predictor? What property does one have that the other lacks?
- Suppose you train a solubility head on your own expression data. Name two ways it could be accurate on your held-out set and still wrong on RFD4's designs. Consider what the training distribution contains that the designs do not.
- What would you measure alongside the reward to catch this before the plate?
Part H: Settings That Cost Compute¶
Part G changed what the model was asked for. These settings change how hard it looks, without introducing new biology.
num_steps— denoising steps. Fewer is faster and rougher.num_replicates— how many designs are returned.- the
searchbudget —n_branch,beam_width, and the number ofstep_checkpoints. Cost isbeam_width × n_branch × (checkpoints − 1)trajectories per replicate, and the response reports the real figure insearch.rollouts.
recycle_strategy is a fourth. We have used thinking_medium throughout. Other settings
exist on the deployment; check its examples page before relying on a particular name.
Run the cell with two different values and compare the wall times.
# Your turn 6
MY_STEPS = 60 # <-- fewer = faster, rougher (try 60 vs 150)
MY_REPLICATES = 1 # <-- more candidates = more time
budget = client.design(
task="generation",
inp=annotated_cif_input(polygly_cif(100), example_id="budget"),
sampling={"num_steps": MY_STEPS, "recycle_strategy": "thinking_medium",
"num_replicates": MY_REPLICATES},
output={"structure_format": "mmcif", "include_sequence": True},
)
print(f"{MY_STEPS} steps x {MY_REPLICATES} replicate(s) -> {client.last_elapsed_s:.1f} s")
view(cif_of(budget))
60 steps x 1 replicate(s) -> 4.1 s
3Dmol.js failed to load for some reason. Please check your browser console for error messages.
Part I: Variations on a Design (Partial Diffusion)¶
Everything so far started from noise. If you already have a design worth keeping, you can start part way along the trajectory instead.
A small restart gives a close relative, a large restart a more distant one. This explores the area around a result rather than starting again, which is what you want after a design passes a filter and you need variants of it.
for frac in (0.2, 0.5, 0.8):
r = client.design(
task="partial_diffusion",
inp=annotated_cif_input(inputs["partial_diffusion"], example_id=f"var{frac}"),
sampling={"recycle_strategy": "thinking_medium", "num_replicates": 1},
partial={"trajectory_fraction_steps": frac},
metrics={"tags": ["design"]},
output={"structure_format": "mmcif", "include_sequence": True},
)
g = r["designs"][0]["diagnostics"]
print(f"restart {frac:>4} -> moved {g.get('input_ca_rmsd_angstrom')} A, "
f"kept {g.get('sequence_recovery')} of the sequence")
restart 0.2 -> moved 0.4459744989871979 A, kept 0.864406779661017 of the sequence
restart 0.5 -> moved 1.109445571899414 A, kept 0.2033898305084746 of the sequence
[429] rate limited — retry 1/4 in 5s
restart 0.8 -> moved 2.7255396842956543 A, kept 0.11864406779661017 of the sequence
Your turn 7 — set the restart point. Choose your own fractions. Predict which fraction
keeps more of the original sequence, a small one or a large one, then read
sequence_recovery and check.
# Your turn 7
MY_FRACTIONS = [0.3, 0.6] # <-- edit me
for frac in MY_FRACTIONS:
r = client.design(
task="partial_diffusion",
inp=annotated_cif_input(inputs["partial_diffusion"], example_id=f"yt-var{frac}"),
sampling={"recycle_strategy": "thinking_medium", "num_replicates": 1},
partial={"trajectory_fraction_steps": frac},
metrics={"tags": ["design"]},
output={"structure_format": "mmcif", "include_sequence": True},
)
g = r["designs"][0]["diagnostics"]
print(f"restart {frac}: moved {g.get('input_ca_rmsd_angstrom')} A, "
f"kept {g.get('sequence_recovery')} of the sequence")
restart 0.3: moved 0.7111566066741943 A, kept 0.5932203389830508 of the sequence
[429] rate limited — retry 1/4 in 5s
restart 0.6: moved 1.6677685976028442 A, kept 0.1864406779661017 of the sequence
Part J: Open Design Problem¶
This cell combines both controls from Part G with the budget settings from Part H. Nothing new is introduced; it collects every decision you have made so far into one place.
Choose a goal, or set your own:
- A. A small sheet-rich protein of about 70 residues with few clashes.
- B. A new PD-L1 binder, different from the one in Part B, with a reward you choose.
- C. A SAM pocket using
task="small_molecule_binder"oninputs["small_molecule_binder"], steered to reduce clashes.
The cell prints a summary. Record the length, the reward you chose, and the time it took, then answer: did the design move the metric you targeted, and how would you know whether that reflects the reward or the seed?
# Open design problem: fill in the four choices below.
# 1) the task
MY_TASK = "generation" # or "small_molecule_binder", "antibody_design", ...
# 2) the input — uncomment ONE (or write your own)
MY_INPUT = annotated_cif_input(polygly_cif(70), example_id="challenge") # A
# MY_INPUT = annotated_cif_input(inputs["protein_binder"], example_id="challenge") # B
# MY_INPUT = annotated_cif_input(inputs["small_molecule_binder"], example_id="challenge") # C
# 3) compute budget
MY_SAMPLING = {"num_steps": 120, "recycle_strategy": "thinking_medium",
"num_replicates": 2, "batch_size": 1}
# 4) your reward (set MY_SEARCH = None for no search at all)
MY_SEARCH = {
"step_checkpoints": [0, 30, 60, 120],
"reward_metrics_tags": ["design"],
"weights": {SHEET: 1.0, CLASH: -2.0}, # <-- your call
"clip_values": {CLASH: {"min": 0.0, "max": 0.02}},
"n_branch": 2, "beam_width": 2,
}
result = client.design(
task=MY_TASK, inp=MY_INPUT,
sampling=MY_SAMPLING, search=MY_SEARCH,
metrics={"tags": ["design"]},
output={"structure_format": "mmcif", "include_sequence": True},
)
print(f"\n{client.last_elapsed_s:.1f} s")
summarize(result)
view(cif_of(result))
[429] rate limited — retry 1/4 in 5s
35.0 s
request_id local task=generation
wall time 28.3 s for 4 design(s)
backend bioir dtype=bfloat16 gpu=NVIDIA H200
oracle [] (none mounted)
schedule 120 steps / 370 trunk passes strategy=custom
search beam=2 branch=2 checkpoints=[0, 30, 60, 120] rollouts=12 best_reward=0.0
req0_step120_b0_r0 origin=search_final tokens=70 finite=True reward=0.0000
MEELKEYVEELLEWAEKNPEVPEEFIEELKEIKSLLEEGKEEEAKKKLEELKEKYPELFEKFEKEKKKKE (len 70)
bond_length_metrics.bond_length.frac_unreasonable = 0.0
bond_length_metrics.bond_length.frac_unreasonable.between_chain.nonpolymer_to_polymer = None
bond_length_metrics.bond_length.frac_unreasonable.between_chain.polymer_polymer = None
bond_length_metrics.bond_length.frac_unreasonable.between_residue.within_chain = 0.0
bond_length_metrics.bond_length.frac_unreasonable.within_residue = 0.0
clash_metrics.frac_atoms_clashing_1p5A = 0.0
clash_metrics.frac_atoms_clashing_inter_residue_ligand_ligand_1p5A = 0.0
clash_metrics.frac_atoms_clashing_inter_residue_polymer_ligand_1p5A = 0.0
clash_metrics.frac_atoms_clashing_inter_residue_polymer_polymer_1p5A = 0.0
clash_metrics.frac_atoms_clashing_intra_residue_ligand_1p5A = 0.0
clash_metrics.frac_atoms_clashing_intra_residue_polymer_1p5A = 0.0
search.reward = 0.0
sequence_adherence.n_violations = 0.0
sequence_adherence.sequence_adherence = None
sequence_probability_adherence.n_one_conditions = 0.0
sequence_probability_adherence.n_one_violations = 0.0
sequence_probability_adherence.n_zero_conditions = 0.0
sequence_probability_adherence.n_zero_violations = 0.0
sequence_probability_adherence.one_adherence = None
sequence_probability_adherence.zero_adherence = None
sse_distribution.helix_fraction = 0.8714285714285714
sse_distribution.helix_fraction_coi = 0.8714285714285714
sse_distribution.loop_fraction = 0.12857142857142856
sse_distribution.loop_fraction_coi = 0.12857142857142856
sse_distribution.num_ss_elements = 9.0
sse_distribution.num_ss_elements_coi = 9.0
sse_distribution.sheet_fraction = 0.0
sse_distribution.sheet_fraction_coi = 0.0
req0_step120_b0_r1 origin=search_final tokens=70 finite=True reward=0.0000
MVAAAVAGAVAGAVVAAVAGAPAAAVVVAAVAGAVVGAVVGAVVADAVVAVVAAAVAGAVVGAVVAAVAA (len 70)
bond_length_metrics.bond_length.frac_unreasonable = 0.0
bond_length_metrics.bond_length.frac_unreasonable.between_chain.nonpolymer_to_polymer = None
bond_length_metrics.bond_length.frac_unreasonable.between_chain.polymer_polymer = None
bond_length_metrics.bond_length.frac_unreasonable.between_residue.within_chain = 0.0
bond_length_metrics.bond_length.frac_unreasonable.within_residue = 0.0
clash_metrics.frac_atoms_clashing_1p5A = 0.0
clash_metrics.frac_atoms_clashing_inter_residue_ligand_ligand_1p5A = 0.0
clash_metrics.frac_atoms_clashing_inter_residue_polymer_ligand_1p5A = 0.0
clash_metrics.frac_atoms_clashing_inter_residue_polymer_polymer_1p5A = 0.0
clash_metrics.frac_atoms_clashing_intra_residue_ligand_1p5A = 0.0
clash_metrics.frac_atoms_clashing_intra_residue_polymer_1p5A = 0.0
search.reward = 0.0
sequence_adherence.n_violations = 0.0
sequence_adherence.sequence_adherence = None
sequence_probability_adherence.n_one_conditions = 0.0
sequence_probability_adherence.n_one_violations = 0.0
sequence_probability_adherence.n_zero_conditions = 0.0
sequence_probability_adherence.n_zero_violations = 0.0
sequence_probability_adherence.one_adherence = None
sequence_probability_adherence.zero_adherence = None
sse_distribution.helix_fraction = 0.8714285714285714
sse_distribution.helix_fraction_coi = 0.8714285714285714
sse_distribution.loop_fraction = 0.12857142857142856
sse_distribution.loop_fraction_coi = 0.12857142857142856
sse_distribution.num_ss_elements = 7.0
sse_distribution.num_ss_elements_coi = 7.0
sse_distribution.sheet_fraction = 0.0
sse_distribution.sheet_fraction_coi = 0.0
req0_step120_b0_r0 origin=search_final tokens=70 finite=True reward=0.0000
AVAALVAIIAAAVAAGDFAAAVAAAVALAALDAAAAVAALVAIIAAAVAAGDFAAAVAAAVALAALDAAA (len 70)
bond_length_metrics.bond_length.frac_unreasonable = 0.0
bond_length_metrics.bond_length.frac_unreasonable.between_chain.nonpolymer_to_polymer = None
bond_length_metrics.bond_length.frac_unreasonable.between_chain.polymer_polymer = None
bond_length_metrics.bond_length.frac_unreasonable.between_residue.within_chain = 0.0
bond_length_metrics.bond_length.frac_unreasonable.within_residue = 0.0
clash_metrics.frac_atoms_clashing_1p5A = 0.0
clash_metrics.frac_atoms_clashing_inter_residue_ligand_ligand_1p5A = 0.0
clash_metrics.frac_atoms_clashing_inter_residue_polymer_ligand_1p5A = 0.0
clash_metrics.frac_atoms_clashing_inter_residue_polymer_polymer_1p5A = 0.0
clash_metrics.frac_atoms_clashing_intra_residue_ligand_1p5A = 0.0
clash_metrics.frac_atoms_clashing_intra_residue_polymer_1p5A = 0.0
search.reward = 0.0
sequence_adherence.n_violations = 0.0
sequence_adherence.sequence_adherence = None
sequence_probability_adherence.n_one_conditions = 0.0
sequence_probability_adherence.n_one_violations = 0.0
sequence_probability_adherence.n_zero_conditions = 0.0
sequence_probability_adherence.n_zero_violations = 0.0
sequence_probability_adherence.one_adherence = None
sequence_probability_adherence.zero_adherence = None
sse_distribution.helix_fraction = 0.9142857142857143
sse_distribution.helix_fraction_coi = 0.9142857142857143
sse_distribution.loop_fraction = 0.08571428571428572
sse_distribution.loop_fraction_coi = 0.08571428571428572
sse_distribution.num_ss_elements = 9.0
sse_distribution.num_ss_elements_coi = 9.0
sse_distribution.sheet_fraction = 0.0
sse_distribution.sheet_fraction_coi = 0.0
req0_step120_b0_r1 origin=search_final tokens=70 finite=True reward=0.0000
MIIKIIIFIILLIIAFIVGYILGKKKKKKLFIIFIIILIIISIILGFSIISIIIIIIVAIIGYIIGKKKK (len 70)
bond_length_metrics.bond_length.frac_unreasonable = 0.0
bond_length_metrics.bond_length.frac_unreasonable.between_chain.nonpolymer_to_polymer = None
bond_length_metrics.bond_length.frac_unreasonable.between_chain.polymer_polymer = None
bond_length_metrics.bond_length.frac_unreasonable.between_residue.within_chain = 0.0
bond_length_metrics.bond_length.frac_unreasonable.within_residue = 0.0
clash_metrics.frac_atoms_clashing_1p5A = 0.0
clash_metrics.frac_atoms_clashing_inter_residue_ligand_ligand_1p5A = 0.0
clash_metrics.frac_atoms_clashing_inter_residue_polymer_ligand_1p5A = 0.0
clash_metrics.frac_atoms_clashing_inter_residue_polymer_polymer_1p5A = 0.0
clash_metrics.frac_atoms_clashing_intra_residue_ligand_1p5A = 0.0
clash_metrics.frac_atoms_clashing_intra_residue_polymer_1p5A = 0.0
search.reward = 0.0
sequence_adherence.n_violations = 0.0
sequence_adherence.sequence_adherence = None
sequence_probability_adherence.n_one_conditions = 0.0
sequence_probability_adherence.n_one_violations = 0.0
sequence_probability_adherence.n_zero_conditions = 0.0
sequence_probability_adherence.n_zero_violations = 0.0
sequence_probability_adherence.one_adherence = None
sequence_probability_adherence.zero_adherence = None
sse_distribution.helix_fraction = 0.9
sse_distribution.helix_fraction_coi = 0.9
sse_distribution.loop_fraction = 0.1
sse_distribution.loop_fraction_coi = 0.1
sse_distribution.num_ss_elements = 7.0
sse_distribution.num_ss_elements_coi = 7.0
sse_distribution.sheet_fraction = 0.0
sse_distribution.sheet_fraction_coi = 0.0
aggregated:
bond_length_metrics.bond_length.frac_unreasonable mean 0.0 (n=4)
bond_length_metrics.bond_length.frac_unreasonable.between_residue.within_chain mean 0.0 (n=4)
bond_length_metrics.bond_length.frac_unreasonable.within_residue mean 0.0 (n=4)
clash_metrics.frac_atoms_clashing_1p5A mean 0.0 (n=4)
clash_metrics.frac_atoms_clashing_inter_residue_ligand_ligand_1p5A mean 0.0 (n=4)
clash_metrics.frac_atoms_clashing_inter_residue_polymer_ligand_1p5A mean 0.0 (n=4)
clash_metrics.frac_atoms_clashing_inter_residue_polymer_polymer_1p5A mean 0.0 (n=4)
clash_metrics.frac_atoms_clashing_intra_residue_ligand_1p5A mean 0.0 (n=4)
clash_metrics.frac_atoms_clashing_intra_residue_polymer_1p5A mean 0.0 (n=4)
search.reward mean 0.0 (n=4)
sequence_adherence.n_violations mean 0.0 (n=4)
sequence_probability_adherence.n_one_conditions mean 0.0 (n=4)
sequence_probability_adherence.n_one_violations mean 0.0 (n=4)
sequence_probability_adherence.n_zero_conditions mean 0.0 (n=4)
sequence_probability_adherence.n_zero_violations mean 0.0 (n=4)
sse_distribution.helix_fraction mean 0.8892857142857142 (n=4)
sse_distribution.helix_fraction_coi mean 0.8892857142857142 (n=4)
sse_distribution.loop_fraction mean 0.11071428571428571 (n=4)
sse_distribution.loop_fraction_coi mean 0.11071428571428571 (n=4)
sse_distribution.num_ss_elements mean 8.0 (n=4)
sse_distribution.num_ss_elements_coi mean 8.0 (n=4)
sse_distribution.sheet_fraction mean 0.0 (n=4)
sse_distribution.sheet_fraction_coi mean 0.0 (n=4)
WARNINGS <- always read these; a bad schedule still returns 200
- sampling.num_steps 120 does not match 'thinking_medium', which runs 60 steps, so its recycle shape was stretched onto 120 steps (370 trunk passes). Send num_steps alone for flat sampling, or omit it to run the strategy as the checkpoint defines it.
3Dmol.js failed to load for some reason. Please check your browser console for error messages.
Part K: A Binder Against Your Own Target¶
Part B used an input NVIDIA had already written. This part writes one.
The design move is identical; the work is in the input rather than the request. A binder problem is generation where the target is written into the file: the target chain frozen, and a few epitope residues marked as hotspots.
Step 1. Look at what an authored target carries, so you know what yours has to match.
Run the cell and read the columns: the frozen target, and the mask_hotspot residues.
describe_cif(inputs["protein_binder"])
print("chains:", chains_in(inputs["protein_binder"]), " (A = target, B = designed)")
atoms 924 ATOM / 1 HETATM chains=['A', 'B']
contig (none)
design segs 1 ranges=[('50', '120')]
mask_coordinate 924 / 925 set
mask_expsegmax 1 / 925 set
mask_expsegmin 1 / 925 set
mask_hotspot 6 / 925 set
mask_sequence 924 / 925 set
chains: ['A', 'B'] (A = target, B = designed)
Step 2. Design a binder against this target. You choose the parameters: how many candidates, and what reward shapes them. A reasonable binder reward keeps clashes down; look through the metric list for others and be able to justify your choice.
# Your turn 8
MY_REPLICATES = 4 # <-- how many candidates to compare
MY_REWARD = {CLASH: -2.0} # <-- shape the binder; add keys from the probe list
my_binders = client.design(
task="generation",
inp=annotated_cif_input(inputs["protein_binder"], example_id="my-binder"),
sampling={"num_steps": 120, "recycle_strategy": "thinking_medium",
"num_replicates": MY_REPLICATES, "batch_size": 1},
search={
"step_checkpoints": [0, 20, 45, 80, 120],
"reward_metrics_tags": ["design"],
"weights": MY_REWARD,
"n_branch": 3, "beam_width": 1, # make 3, keep the best 1 (real selection)
},
metrics={"tags": ["design"]},
output={"structure_format": "mmcif", "include_sequence": True},
)
summarize(my_binders)
for i in range(len(my_binders["designs"])):
print(f"--- candidate {i} (orange = your binder, grey = target) ---")
view_design(cif_of(my_binders, i), designed_chain="B")
request_id local task=generation
wall time 90.8 s for 4 design(s)
backend bioir dtype=bfloat16 gpu=NVIDIA H200
oracle [] (none mounted)
schedule 120 steps / 370 trunk passes strategy=custom
search beam=1 branch=3 checkpoints=[0, 20, 45, 80, 120] rollouts=12 best_reward=0.0
req0_step120_b0_r0 origin=search_final tokens=201 finite=True reward=0.0000
NAFTVTVPKDLYVVEYGSNMTIECKFPVEKQLDLAALIVYWEMEDKNIIQFVHGEEDLKVQHSSYRQRARLLKDQLSLGN... (len 201)
bond_length_metrics.bond_length.frac_unreasonable = 0.0
bond_length_metrics.bond_length.frac_unreasonable.between_chain.nonpolymer_to_polymer = None
bond_length_metrics.bond_length.frac_unreasonable.between_chain.polymer_polymer = None
bond_length_metrics.bond_length.frac_unreasonable.between_residue.within_chain = 0.0
bond_length_metrics.bond_length.frac_unreasonable.within_residue = 0.0
clash_metrics.frac_atoms_clashing_1p5A = 0.0
clash_metrics.frac_atoms_clashing_inter_residue_ligand_ligand_1p5A = 0.0
clash_metrics.frac_atoms_clashing_inter_residue_polymer_ligand_1p5A = 0.0
clash_metrics.frac_atoms_clashing_inter_residue_polymer_polymer_1p5A = 0.0
clash_metrics.frac_atoms_clashing_intra_residue_ligand_1p5A = 0.0
clash_metrics.frac_atoms_clashing_intra_residue_polymer_1p5A = 0.0
search.reward = 0.0
sequence_adherence.n_violations = 0.0
sequence_adherence.sequence_adherence = 1.0
sequence_probability_adherence.n_one_conditions = 115.0
sequence_probability_adherence.n_one_violations = 0.0
sequence_probability_adherence.n_zero_conditions = 3105.0
sequence_probability_adherence.n_zero_violations = 0.0
sequence_probability_adherence.one_adherence = 1.0
sequence_probability_adherence.zero_adherence = 1.0
sse_distribution.helix_fraction = 0.0
sse_distribution.helix_fraction_coi = 0.0
sse_distribution.loop_fraction = 0.6766169154228856
sse_distribution.loop_fraction_coi = 0.7209302325581395
sse_distribution.num_ss_elements = 21.0
sse_distribution.num_ss_elements_coi = 9.0
sse_distribution.sheet_fraction = 0.32338308457711445
sse_distribution.sheet_fraction_coi = 0.27906976744186046
req0_step120_b0_r0 origin=search_final tokens=232 finite=True reward=0.0000
NAFTVTVPKDLYVVEYGSNMTIECKFPVEKQLDLAALIVYWEMEDKNIIQFVHGEEDLKVQHSSYRQRARLLKDQLSLGN... (len 232)
bond_length_metrics.bond_length.frac_unreasonable = 0.0
bond_length_metrics.bond_length.frac_unreasonable.between_chain.nonpolymer_to_polymer = None
bond_length_metrics.bond_length.frac_unreasonable.between_chain.polymer_polymer = None
bond_length_metrics.bond_length.frac_unreasonable.between_residue.within_chain = 0.0
bond_length_metrics.bond_length.frac_unreasonable.within_residue = 0.0
clash_metrics.frac_atoms_clashing_1p5A = 0.0
clash_metrics.frac_atoms_clashing_inter_residue_ligand_ligand_1p5A = 0.0
clash_metrics.frac_atoms_clashing_inter_residue_polymer_ligand_1p5A = 0.0
clash_metrics.frac_atoms_clashing_inter_residue_polymer_polymer_1p5A = 0.0
clash_metrics.frac_atoms_clashing_intra_residue_ligand_1p5A = 0.0
clash_metrics.frac_atoms_clashing_intra_residue_polymer_1p5A = 0.0
search.reward = 0.0
sequence_adherence.n_violations = 0.0
sequence_adherence.sequence_adherence = 1.0
sequence_probability_adherence.n_one_conditions = 115.0
sequence_probability_adherence.n_one_violations = 0.0
sequence_probability_adherence.n_zero_conditions = 3105.0
sequence_probability_adherence.n_zero_violations = 0.0
sequence_probability_adherence.one_adherence = 1.0
sequence_probability_adherence.zero_adherence = 1.0
sse_distribution.helix_fraction = 0.0
sse_distribution.helix_fraction_coi = 0.0
sse_distribution.loop_fraction = 0.6120689655172413
sse_distribution.loop_fraction_coi = 0.6153846153846154
sse_distribution.num_ss_elements = 29.0
sse_distribution.num_ss_elements_coi = 15.0
sse_distribution.sheet_fraction = 0.3879310344827586
sse_distribution.sheet_fraction_coi = 0.38461538461538464
req0_step120_b0_r0 origin=search_final tokens=197 finite=True reward=0.0000
NAFTVTVPKDLYVVEYGSNMTIECKFPVEKQLDLAALIVYWEMEDKNIIQFVHGEEDLKVQHSSYRQRARLLKDQLSLGN... (len 197)
bond_length_metrics.bond_length.frac_unreasonable = 0.0
bond_length_metrics.bond_length.frac_unreasonable.between_chain.nonpolymer_to_polymer = None
bond_length_metrics.bond_length.frac_unreasonable.between_chain.polymer_polymer = None
bond_length_metrics.bond_length.frac_unreasonable.between_residue.within_chain = 0.0
bond_length_metrics.bond_length.frac_unreasonable.within_residue = 0.0
clash_metrics.frac_atoms_clashing_1p5A = 0.0
clash_metrics.frac_atoms_clashing_inter_residue_ligand_ligand_1p5A = 0.0
clash_metrics.frac_atoms_clashing_inter_residue_polymer_ligand_1p5A = 0.0
clash_metrics.frac_atoms_clashing_inter_residue_polymer_polymer_1p5A = 0.0
clash_metrics.frac_atoms_clashing_intra_residue_ligand_1p5A = 0.0
clash_metrics.frac_atoms_clashing_intra_residue_polymer_1p5A = 0.0
search.reward = 0.0
sequence_adherence.n_violations = 0.0
sequence_adherence.sequence_adherence = 1.0
sequence_probability_adherence.n_one_conditions = 115.0
sequence_probability_adherence.n_one_violations = 0.0
sequence_probability_adherence.n_zero_conditions = 3105.0
sequence_probability_adherence.n_zero_violations = 0.0
sequence_probability_adherence.one_adherence = 1.0
sequence_probability_adherence.zero_adherence = 1.0
sse_distribution.helix_fraction = 0.0
sse_distribution.helix_fraction_coi = 0.0
sse_distribution.loop_fraction = 0.6548223350253807
sse_distribution.loop_fraction_coi = 0.6707317073170732
sse_distribution.num_ss_elements = 21.0
sse_distribution.num_ss_elements_coi = 9.0
sse_distribution.sheet_fraction = 0.34517766497461927
sse_distribution.sheet_fraction_coi = 0.32926829268292684
req0_step120_b0_r0 origin=search_final tokens=174 finite=True reward=0.0000
NAFTVTVPKDLYVVEYGSNMTIECKFPVEKQLDLAALIVYWEMEDKNIIQFVHGEEDLKVQHSSYRQRARLLKDQLSLGN... (len 174)
bond_length_metrics.bond_length.frac_unreasonable = 0.0
bond_length_metrics.bond_length.frac_unreasonable.between_chain.nonpolymer_to_polymer = None
bond_length_metrics.bond_length.frac_unreasonable.between_chain.polymer_polymer = None
bond_length_metrics.bond_length.frac_unreasonable.between_residue.within_chain = 0.0
bond_length_metrics.bond_length.frac_unreasonable.within_residue = 0.0
clash_metrics.frac_atoms_clashing_1p5A = 0.0
clash_metrics.frac_atoms_clashing_inter_residue_ligand_ligand_1p5A = 0.0
clash_metrics.frac_atoms_clashing_inter_residue_polymer_ligand_1p5A = 0.0
clash_metrics.frac_atoms_clashing_inter_residue_polymer_polymer_1p5A = 0.0
clash_metrics.frac_atoms_clashing_intra_residue_ligand_1p5A = 0.0
clash_metrics.frac_atoms_clashing_intra_residue_polymer_1p5A = 0.0
search.reward = 0.0
sequence_adherence.n_violations = 0.0
sequence_adherence.sequence_adherence = 1.0
sequence_probability_adherence.n_one_conditions = 115.0
sequence_probability_adherence.n_one_violations = 0.0
sequence_probability_adherence.n_zero_conditions = 3105.0
sequence_probability_adherence.n_zero_violations = 0.0
sequence_probability_adherence.one_adherence = 1.0
sequence_probability_adherence.zero_adherence = 1.0
sse_distribution.helix_fraction = 0.28735632183908044
sse_distribution.helix_fraction_coi = 0.847457627118644
sse_distribution.loop_fraction = 0.4540229885057471
sse_distribution.loop_fraction_coi = 0.15254237288135594
sse_distribution.num_ss_elements = 21.0
sse_distribution.num_ss_elements_coi = 7.0
sse_distribution.sheet_fraction = 0.25862068965517243
sse_distribution.sheet_fraction_coi = 0.0
aggregated:
bond_length_metrics.bond_length.frac_unreasonable mean 0.0 (n=4)
bond_length_metrics.bond_length.frac_unreasonable.between_residue.within_chain mean 0.0 (n=4)
bond_length_metrics.bond_length.frac_unreasonable.within_residue mean 0.0 (n=4)
clash_metrics.frac_atoms_clashing_1p5A mean 0.0 (n=4)
clash_metrics.frac_atoms_clashing_inter_residue_ligand_ligand_1p5A mean 0.0 (n=4)
clash_metrics.frac_atoms_clashing_inter_residue_polymer_ligand_1p5A mean 0.0 (n=4)
clash_metrics.frac_atoms_clashing_inter_residue_polymer_polymer_1p5A mean 0.0 (n=4)
clash_metrics.frac_atoms_clashing_intra_residue_ligand_1p5A mean 0.0 (n=4)
clash_metrics.frac_atoms_clashing_intra_residue_polymer_1p5A mean 0.0 (n=4)
search.reward mean 0.0 (n=4)
sequence_adherence.n_violations mean 0.0 (n=4)
sequence_adherence.sequence_adherence mean 1.0 (n=4)
sequence_probability_adherence.n_one_conditions mean 115.0 (n=4)
sequence_probability_adherence.n_one_violations mean 0.0 (n=4)
sequence_probability_adherence.n_zero_conditions mean 3105.0 (n=4)
sequence_probability_adherence.n_zero_violations mean 0.0 (n=4)
sequence_probability_adherence.one_adherence mean 1.0 (n=4)
sequence_probability_adherence.zero_adherence mean 1.0 (n=4)
sse_distribution.helix_fraction mean 0.07183908045977011 (n=4)
sse_distribution.helix_fraction_coi mean 0.211864406779661 (n=4)
sse_distribution.loop_fraction mean 0.5993828011178137 (n=4)
sse_distribution.loop_fraction_coi mean 0.539897232035296 (n=4)
sse_distribution.num_ss_elements mean 23.0 (n=4)
sse_distribution.num_ss_elements_coi mean 10.0 (n=4)
sse_distribution.sheet_fraction mean 0.32877811842241617 (n=4)
sse_distribution.sheet_fraction_coi mean 0.248238361185043 (n=4)
WARNINGS <- always read these; a bad schedule still returns 200
- sampling.num_steps 120 does not match 'thinking_medium', which runs 60 steps, so its recycle shape was stretched onto 120 steps (370 trunk passes). Send num_steps alone for flat sampling, or omit it to run the strategy as the checkpoint defines it.
--- candidate 0 (orange = your binder, grey = target) ---
3Dmol.js failed to load for some reason. Please check your browser console for error messages.
--- candidate 1 (orange = your binder, grey = target) ---
3Dmol.js failed to load for some reason. Please check your browser console for error messages.
--- candidate 2 (orange = your binder, grey = target) ---
3Dmol.js failed to load for some reason. Please check your browser console for error messages.
--- candidate 3 (orange = your binder, grey = target) ---
3Dmol.js failed to load for some reason. Please check your browser console for error messages.
Step 3. Build an input for a target of your own. Two helpers do the authoring:
author_target(structure, freeze_chain, hotspots, binder_length=(50, 120))
author_motif(structure, chain, keep=[...]) # the motif and enzyme version
structure accepts a PDB id such as "4Z18", a UniProt accession, which retrieves the
AlphaFold model, or a path to a local file. hotspots are author residue numbers: the
epitope you want bound.
Both helpers build the input by templating off NVIDIA's gallery files, so the chemical component blocks are identical to ones the server already accepts and only the conditioning is new.
Note on the file format. Hotspots are flagged per atom, not per residue. The interleaved
motif format also requires a _struct_conn block bonding consecutive tokens; without it the
server rejects the file with index_group_molecule_must_be_connected, because a designed
segment is a single token with no bonds to its neighbours. The binder format does not need
this block, since its two chains are separate molecules.
# Your turn 9
MY_TARGET = "4Z18" # <-- PDB id, UniProt accession, or a file path
MY_CHAIN = "A" # <-- the chain to freeze
MY_HOTSPOTS = [56, 115, 123] # <-- your epitope, author numbering
MY_BINDER = (50, 120) # <-- how long the binder may be
my_input = author_target(MY_TARGET, freeze_chain=MY_CHAIN,
hotspots=MY_HOTSPOTS, binder_length=MY_BINDER)
# Does it carry the same conditioning as NVIDIA's own binder input?
describe_cif(inputs["protein_binder"], label="NVIDIA's gallery input")
print()
describe_cif(my_input, label="yours")
=== NVIDIA's gallery input ===
atoms 924 ATOM / 1 HETATM chains=['A', 'B']
contig (none)
design segs 1 ranges=[('50', '120')]
mask_coordinate 924 / 925 set
mask_expsegmax 1 / 925 set
mask_expsegmin 1 / 925 set
mask_hotspot 6 / 925 set
mask_sequence 924 / 925 set
=== yours ===
atoms 1707 ATOM / 1 HETATM chains=['A', 'B']
contig (none)
design segs 1 ranges=[('50', '120')]
mask_coordinate 1707 / 1708 set
mask_expsegmax 1 / 1708 set
mask_expsegmin 1 / 1708 set
mask_hotspot 20 / 1708 set
mask_sequence 1707 / 1708 set
{'columns': ['group_PDB',
'type_symbol',
'label_atom_id',
'label_alt_id',
'label_comp_id',
'label_asym_id',
'label_entity_id',
'label_seq_id',
'pdbx_PDB_ins_code',
'auth_seq_id',
'auth_comp_id',
'auth_asym_id',
'auth_atom_id',
'id',
'B_iso_or_equiv',
'occupancy',
'pdbx_formal_charge',
'annotation_expsegmax_1_residue',
'annotation_expsegmin_1_residue',
'mask_coordinate_1_atom',
'mask_expsegmax_1_residue',
'mask_expsegmin_1_residue',
'mask_hotspot_1_atom',
'mask_sequence_1_residue',
'atom_array_index',
'Cartn_x',
'Cartn_y',
'Cartn_z',
'pdbx_PDB_model_num'],
'n_rows': 1708,
'contig': None,
'flags': {'mask_coordinate_1_atom': (1707, 1708),
'mask_expsegmax_1_residue': (1, 1708),
'mask_expsegmin_1_residue': (1, 1708),
'mask_hotspot_1_atom': (20, 1708),
'mask_sequence_1_residue': (1707, 1708)}}
Compare the two outputs. Yours should show the same kinds of conditioning: every target
atom coordinate- and sequence-masked, one design segment carrying the length range, and a
mask_hotspot count greater than zero. If mask_hotspot is 0, the model will ignore your
epitope.
Now design against it.
# Design one binder against your target. If this returns a 422, read the
# message: the server validates the conditioning before using any GPU time.
mine = client.design(
task="generation",
inp=annotated_cif_input(my_input, example_id="my-target"),
sampling={"recycle_strategy": "thinking_medium", "num_replicates": 1},
metrics={"tags": ["design", "hotspot_conditioning"]},
output={"structure_format": "mmcif", "include_sequence": True},
)
summarize(mine)
print("\nchains:", chains_in(cif_of(mine)))
view_design(cif_of(mine), designed_chain="B") # orange = your binder
request_id local task=generation
wall time 39.3 s for 1 design(s)
backend bioir dtype=bfloat16 gpu=NVIDIA H200
oracle [] (none mounted)
schedule 60 steps / 185 trunk passes strategy=thinking_medium
design_0 origin=sample tokens=323 finite=True
FTVTVPKDLYVVEYGSNMTIECKFPVEKQLDLAALIVYWEMEDKNIIQFVHGEEDLKVQHSSYRQRARLLKDQLSLGNAA... (len 323)
bond_length_metrics.bond_length.frac_unreasonable = 0.0
bond_length_metrics.bond_length.frac_unreasonable.between_chain.nonpolymer_to_polymer = None
bond_length_metrics.bond_length.frac_unreasonable.between_chain.polymer_polymer = None
bond_length_metrics.bond_length.frac_unreasonable.between_residue.within_chain = 0.0
bond_length_metrics.bond_length.frac_unreasonable.within_residue = 0.0
clash_metrics.frac_atoms_clashing_1p5A = 0.0
clash_metrics.frac_atoms_clashing_inter_residue_ligand_ligand_1p5A = 0.0
clash_metrics.frac_atoms_clashing_inter_residue_polymer_ligand_1p5A = 0.0
clash_metrics.frac_atoms_clashing_inter_residue_polymer_polymer_1p5A = 0.0
clash_metrics.frac_atoms_clashing_intra_residue_ligand_1p5A = 0.0
clash_metrics.frac_atoms_clashing_intra_residue_polymer_1p5A = 0.0
design_motif_allatom_rmsd.rmsd = 0.6248973608016968
hotspot_obedience.hotspot_obedience = 0.7
sequence_adherence.n_violations = 0.0
sequence_adherence.sequence_adherence = 1.0
sequence_probability_adherence.n_one_conditions = 212.0
sequence_probability_adherence.n_one_violations = 0.0
sequence_probability_adherence.n_zero_conditions = 5724.0
sequence_probability_adherence.n_zero_violations = 0.0
sequence_probability_adherence.one_adherence = 1.0
sequence_probability_adherence.zero_adherence = 1.0
sse_distribution.helix_fraction = 0.0
sse_distribution.helix_fraction_coi = 0.0
sse_distribution.loop_fraction = 0.5356037151702786
sse_distribution.loop_fraction_coi = 0.6306306306306306
sse_distribution.num_ss_elements = 43.0
sse_distribution.num_ss_elements_coi = 15.0
sse_distribution.sheet_fraction = 0.46439628482972134
sse_distribution.sheet_fraction_coi = 0.36936936936936937
aggregated:
bond_length_metrics.bond_length.frac_unreasonable mean 0.0 (n=1)
bond_length_metrics.bond_length.frac_unreasonable.between_residue.within_chain mean 0.0 (n=1)
bond_length_metrics.bond_length.frac_unreasonable.within_residue mean 0.0 (n=1)
clash_metrics.frac_atoms_clashing_1p5A mean 0.0 (n=1)
clash_metrics.frac_atoms_clashing_inter_residue_ligand_ligand_1p5A mean 0.0 (n=1)
clash_metrics.frac_atoms_clashing_inter_residue_polymer_ligand_1p5A mean 0.0 (n=1)
clash_metrics.frac_atoms_clashing_inter_residue_polymer_polymer_1p5A mean 0.0 (n=1)
clash_metrics.frac_atoms_clashing_intra_residue_ligand_1p5A mean 0.0 (n=1)
clash_metrics.frac_atoms_clashing_intra_residue_polymer_1p5A mean 0.0 (n=1)
design_motif_allatom_rmsd.rmsd median 0.6248973608016968 (n=1)
hotspot_obedience.hotspot_obedience mean 0.7 (n=1)
sequence_adherence.n_violations mean 0.0 (n=1)
sequence_adherence.sequence_adherence mean 1.0 (n=1)
sequence_probability_adherence.n_one_conditions mean 212.0 (n=1)
sequence_probability_adherence.n_one_violations mean 0.0 (n=1)
sequence_probability_adherence.n_zero_conditions mean 5724.0 (n=1)
sequence_probability_adherence.n_zero_violations mean 0.0 (n=1)
sequence_probability_adherence.one_adherence mean 1.0 (n=1)
sequence_probability_adherence.zero_adherence mean 1.0 (n=1)
sse_distribution.helix_fraction mean 0.0 (n=1)
sse_distribution.helix_fraction_coi mean 0.0 (n=1)
sse_distribution.loop_fraction mean 0.5356037151702786 (n=1)
sse_distribution.loop_fraction_coi mean 0.6306306306306306 (n=1)
sse_distribution.num_ss_elements mean 43.0 (n=1)
sse_distribution.num_ss_elements_coi mean 15.0 (n=1)
sse_distribution.sheet_fraction mean 0.46439628482972134 (n=1)
sse_distribution.sheet_fraction_coi mean 0.36936936936936937 (n=1)
chains: ['A', 'B']
3Dmol.js failed to load for some reason. Please check your browser console for error messages.
Checking where it bound¶
Look for hotspot_obedience.hotspot_obedience in the output above. This metric asks
whether the designed chain contacts the residues you marked, which is a more useful measure
for a binder than the absence of clashes.
It is also a better quantity to steer on than secondary structure. The sheet reward in Part F works against the model's helical prior. Hotspot obedience does not, and an increase means the design improved in a way that matters biologically rather than only geometrically.
Before running the cell, predict the result. The unsteered run above scored near 0.5. How much do you expect three branches at four checkpoints to change it?
# The same target and seed. Only the reward differs.
HOT = "hotspot_obedience.hotspot_obedience"
steered_binder = client.design(
task="generation",
inp=annotated_cif_input(my_input, example_id="my-target-steered"),
sampling={"num_steps": 60, "recycle_strategy": "thinking_medium",
"num_replicates": 1, "batch_size": 1, "seed": 11},
search={
"step_checkpoints": [0, 20, 40, 60],
"reward_metrics_tags": ["design", "hotspot_conditioning"],
"weights": {HOT: 4.0, CLASH: -1.0}, # bind where I said; don't clash
"n_branch": 3, "beam_width": 1,
},
metrics={"tags": ["design", "hotspot_conditioning"]},
output={"structure_format": "mmcif", "include_sequence": True},
)
metric_table({"unsteered": mine, "steered -> hotspot": steered_binder}, [HOT, CLASH])
print(f"\nrollouts spent: {steered_binder['search']['rollouts']}")
view_design(cif_of(steered_binder), designed_chain="B")
run hotspot_obedience frac_atoms_clashing_1p5A ----------------------------------------------------------------- unsteered 0.700 0.000 steered -> hotspot 0.950 0.000 rollouts spent: 9
3Dmol.js failed to load for some reason. Please check your browser console for error messages.
Questions¶
- Obedience increased. Does that mean you have a binder? What has not been tested? Recall that the number was computed on a predicted structure that was never folded independently, expressed, or assayed.
- Name a target from your own work. Say which residues you would mark as the epitope and why, and state one reward you would add beyond obedience and clashes. That specification — target, epitope, reward — is the design.
- The reward you would want is something like predicted soluble yield. Nothing in today's metric list measures it. What would you have to build, and how would you know it had not learned the shortcut described in the slides?
Part L: Checking With an Independent Model¶
Every metric used so far, including the one you just improved, was computed by RFD4 on its
own predicted structure. A high hotspot_obedience means RFD4 believes its own design
binds where you asked.
The usual way to test that is to take the designed sequence alone and fold it with a model that has not seen RFD4's structure, then ask whether the two structures agree.
This gives two numbers, both of which design pipelines use as filters:
| Measure | What it asks | Typical threshold |
|---|---|---|
| pLDDT | does the second model have confidence in its own prediction? | above 0.7 |
| self-consistency RMSD | do the two structures agree after superposition? | below 2 Å |
We use ESMFold through the public ESM Atlas API, which is free and needs no key. This is the same model family as the ESM-C you used on Tuesday: ESM-C reads sequences, ESMFold folds them. The service is rate limited and sometimes slow, so the helper retries. It takes one chain of up to about 400 residues.
# Fold the designed sequence with ESMFold, using the sequence alone.
# `mine` is the binder from Part K. Any response object works here.
#
# ESM Atlas is a free public service and is often busy. The helper retries,
# but if it gives up, that is the service and not your design.
try:
check = refold_check(mine, chain="B") # chain B = the designed binder
except RuntimeError as exc:
print("ESM Atlas did not answer:", exc)
print("Try again in a few minutes. The design is unaffected.")
check = None
ESM Atlas busy (attempt 1/10) - retrying in 20s
ESM Atlas busy (attempt 2/10) - retrying in 20s
ESM Atlas busy (attempt 3/10) - retrying in 20s
ESM Atlas busy (attempt 4/10) - retrying in 20s
ESM Atlas busy (attempt 5/10) - retrying in 20s
ESM Atlas busy (attempt 6/10) - retrying in 20s
ESM Atlas busy (attempt 7/10) - retrying in 20s
ESM Atlas busy (attempt 8/10) - retrying in 20s
ESM Atlas busy (attempt 9/10) - retrying in 20s
ESM Atlas busy (attempt 10/10) - retrying in 20s
ESM Atlas did not answer: ESM Atlas did not return a structure. Last: HTTPError: HTTP Error 504: Gateway Timeout Try again in a few minutes. The design is unaffected.
Questions¶
- Suppose pLDDT is 0.95 and the RMSD is 0.4 Å. What has been established, and what has not? Consider that both models were trained on the same PDB.
- Suppose the RMSD is 8 Å. Is the design refuted? ESMFold is also a model, and it is known to perform poorly on sequences unlike anything in nature, which is what a de novo design is.
- This filter is inexpensive and the bench is not. Where would you place it in a campaign that starts with 10,000 designs and ends with 20 in a plate?
Summary¶
Three questions, three tools, in the order we met them:
- ESM-C — what sequences are plausible?
- RFD4-Proteina — what sequence and structure satisfy our constraints?
- The bench — which candidates produce soluble, active protein?
Everything today ended at a returned structure. A plausible structure is a hypothesis, not a result: nothing here has been expressed, folded in a cell, or assayed. The two controls in Part G are how you state that hypothesis precisely. Part G's repeats are why you should not trust a single run, and Part L is the cheapest available check before committing to an experiment.
Our running question is whether a stubborn enzyme can be redesigned for higher soluble, functional yield in E. coli. You now have the components: constrain the catalytic core, reward a predicted property, search, express a small panel, measure, update the reward, and repeat. The open problem is the one from Part G: building a reward that measures what you actually care about.
Further reading: ESM-C (EvolutionaryScale); RFD4-Proteina (NVIDIA and the Baker lab documentation).