抗体怎样贴在蛋白质表面?
用曲妥珠单抗与 HER2 的真实模型,跟着三种颜色,看看抗体的一部分怎样贴在蛋白表面。
从这里开始:先听故事,再看图片
先认识三个词。 蛋白质可以想成一长串小珠子折成的三维物体。每颗珠子对应一个氨基酸,在蛋白质里也叫残基。原子是组成珠子的更小部件。真实的蛋白质会弯、会动。
两种学法。 想先听懂,就读短讲解、看图片;代码和“多学一点”可以以后再看。想在设备上做一遍,就下载完整 .ipynb,在 VibeIt 中点 + → Import from Files,然后从上到下运行所有代码单元。
下面的统计图和可旋转分子模型,来自代码的实际运行;数据和简笔画已经装在 notebook 里面,不用另找文件。把 notebook 保存在当前工作文件夹即可;这节课不需要 AI 账号。
让工具先准备好
看看这一步的 Python 代码(可以先跳过)
from pathlib import Path
import base64, gzip, hashlib, json, platform
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from IPython.display import display, HTML
import py3Dmol
from scipy.spatial.distance import cdist
from IPython.display import display as _display
def display(value):
if isinstance(value,pd.DataFrame):
table=value.to_html(max_rows=12,escape=True)
style="<style>.vibeit-readable-table{max-width:100%;overflow-x:auto}.vibeit-readable-table table{width:max-content;max-width:none;border-collapse:collapse}.vibeit-readable-table td,.vibeit-readable-table th{white-space:nowrap!important;word-break:normal!important;overflow-wrap:normal!important}</style>"
return _display(HTML(style+'<div class="vibeit-readable-table">'+table+'</div>'))
return _display(value)
RECIPE_ID='08-trastuzumab-interface'
OUTPUT_DIR=Path.cwd()/"results"/RECIPE_ID
plt.rcParams.update({"figure.dpi":120,"font.size":10,"axes.spines.top":False,"axes.spines.right":False,
"axes.prop_cycle":plt.cycler(color=["#18785f","#416aa6","#bc6541","#9974a5"])})
print("Python:",platform.python_version(),"; NumPy:",np.__version__,"; Pandas:",pd.__version__)
print("Working folder:",Path.cwd())
print("Core data mode: embedded snapshot; no network requests")
Python: 3.10.8 ; NumPy: 1.26.4 ; Pandas: 2.3.3 Working folder: /Users/zhiluo/Developer/vibeit-cookbook/cookbook/downloads Core data mode: embedded snapshot; no network requests
打开 notebook 里面的数据包
看看这一步的 Python 代码(可以先跳过)
def load_snapshot(recipe_id, expected_sha256, snapshot_key):
"""Read embedded data from this notebook, including a renamed copy."""
for path in sorted(Path.cwd().glob("*.ipynb")):
try:
notebook = json.loads(path.read_text(encoding="utf-8"))
metadata = notebook.get("metadata", {}).get("vibeit_cookbook", {})
blob = metadata.get("snapshots", {}).get(snapshot_key)
if metadata.get("recipe_id") != recipe_id or not blob:
continue
if blob.get("sha256") != expected_sha256:
continue
raw = gzip.decompress(base64.b64decode(blob["gzip_base64"], validate=True))
if hashlib.sha256(raw).hexdigest() != expected_sha256:
raise ValueError("Embedded snapshot checksum mismatch: " + snapshot_key)
return json.loads(raw)
except (OSError, json.JSONDecodeError):
continue
raise FileNotFoundError(
"Save/open the complete .ipynb in the active working folder. "
"The embedded snapshot could not be located for " + recipe_id)
SNAPSHOT_HASHES={'trastuzumab': '147b0a3329c44a3ac92e5eda4c4e0e5b18511bf95c9938e3e0936567f9c252b5'}
def snapshot(key):
return load_snapshot(RECIPE_ID,SNAPSHOT_HASHES[key],key)
print("Embedded snapshots:",list(SNAPSHOT_HASHES))
Embedded snapshots: ['trastuzumab']
准备几个会反复用到的小工具
看看这一步的 Python 代码(可以先跳过)
def parse_pdb(text):
"""First model; choose highest occupancy altloc, then blank/A/lexical order."""
rows, first_model, inside = {}, None, True
for line in text.splitlines():
record = line[:6].strip()
if record == "MODEL":
number = line[10:14].strip()
if first_model is None:
first_model = number
inside = number == first_model
if record == "ENDMDL" and inside:
break
if record not in ("ATOM","HETATM") or not inside:
continue
try:
atom = dict(record=record,serial=int(line[6:11]),atom=line[12:16].strip(),altloc=line[16:17].strip(),
residue=line[17:20].strip(),chain=line[21:22].strip(),resnum=int(line[22:26]),icode=line[26:27].strip(),
x=float(line[30:38]),y=float(line[38:46]),z=float(line[46:54]),
occupancy=float(line[54:60].strip() or 0),bfactor=float(line[60:66].strip() or 0),element=line[76:78].strip())
except ValueError as exc:
raise ValueError("Invalid PDB numeric fields") from exc
key = (record,atom["chain"],atom["resnum"],atom["icode"],atom["residue"],atom["atom"])
rank = (atom["occupancy"],atom["altloc"]=="",atom["altloc"]=="A")
old = rows.get(key)
if old is None or rank > old[0] or (rank == old[0] and atom["altloc"] < old[1]["altloc"]):
rows[key] = (rank,atom)
columns = ["record","serial","atom","altloc","residue","chain","resnum","icode","x","y","z","occupancy","bfactor","element"]
atoms = pd.DataFrame([entry[1] for entry in rows.values()],columns=columns)
if atoms.empty:
raise ValueError("No atoms in the first model")
if not np.isfinite(atoms[["x","y","z","occupancy"]].to_numpy()).all():
raise ValueError("Nonfinite coordinates or occupancy")
return atoms
print("Method ready: parse_pdb")
Method ready: parse_pdb
看看这一步的 Python 代码(可以先跳过)
def residue_minimum_distances(left, right):
"""Minimum Euclidean atom distance per residue pair, retaining chain/insertion IDs.
Caller explicitly selects positive-occupancy heavy atoms. This measures
geometric proximity, not a hydrogen bond, affinity, buried area or energy.
At most one residue's atom-distance block is held in memory at a time.
"""
keys = ['chain', 'resnum', 'icode', 'residue']
columns = keys + ['atom_count']
if left.empty or right.empty:
raise ValueError('Both atom selections must be nonempty')
if not np.isfinite(left[['x','y','z']].to_numpy()).all() or not np.isfinite(right[['x','y','z']].to_numpy()).all():
raise ValueError('Coordinates must be finite')
left = left.reset_index(drop=True)
right = right.reset_index(drop=True)
left_groups = list(left.groupby(keys, sort=False).indices.items())
right_groups = list(right.groupby(keys, sort=False).indices.items())
left_residues = pd.DataFrame([list(key)+[len(indices)] for key,indices in left_groups], columns=columns)
right_residues = pd.DataFrame([list(key)+[len(indices)] for key,indices in right_groups], columns=columns)
right_codes = np.empty(len(right), dtype=int)
for j, (_, indices) in enumerate(right_groups):
right_codes[indices] = j
matrix = np.full((len(left_groups),len(right_groups)), np.inf)
right_xyz = right[['x','y','z']].to_numpy(dtype=float)
for i, (_, indices) in enumerate(left_groups):
block = cdist(left.iloc[indices][['x','y','z']].to_numpy(dtype=float), right_xyz)
np.minimum.at(matrix[i], right_codes, block.min(axis=0))
return left_residues, right_residues, matrix
print("Method ready: residue_minimum_distances")
Method ready: residue_minimum_distances
看看这一步的 Python 代码(可以先跳过)
def residue_labels(frame):
"""Display author residue identifiers without discarding insertion codes."""
return [f'{r.chain}:{r.residue}{int(r.resnum)}{r.icode}' for r in frame.itertuples()]
print("Method ready: residue_labels")
Method ready: residue_labels
看看这一步的 Python 代码(可以先跳过)
def pdb_atom_text(atoms):
"""Serialize an explicitly selected atom table for 3D display, without reorienting it."""
lines = []
for r in atoms.itertuples():
lines.append(f'{r.record:<6}{r.serial:5d} {r.atom:>4} {r.residue:>3} {r.chain:1}{r.resnum:4d}{r.icode:1} '
f'{r.x:8.3f}{r.y:8.3f}{r.z:8.3f}{r.occupancy:6.2f}{r.bfactor:6.2f} {r.element:>2} ')
return '\n'.join(lines) + '\nEND\n'
print("Method ready: pdb_atom_text")
Method ready: pdb_atom_text
今天的模型里有什么?
今天看的是抗体负责结合的 Fab 部分,和 HER2 的一部分在实验中一起出现的模型。2
一起看图、动手试试
1. 一只特别形状的“手”,怎样贴住蛋白表面?
上一课,小药物放进了蛋白的口袋。这一课的药物叫抗体,它本身也是蛋白质。可以把它负责结合的部分,想成一只有特别形状的“手”,能贴到另一个蛋白表面的合适位置。今天被它结合的蛋白,名字叫 HER2。
我们只看抗体负责结合的一小部分,叫 Fab。问题很简单:两者在哪里相遇,哪些小部件靠得近?
转动模型,从不同方向观察相遇的位置。2
2. 先给三个部分贴上名字标签
模型的文件编号是 1N8Z。里面有三个标签:A 是 Fab 轻链,B 是 Fab 重链,C 是 HER2。“链”就是一串蛋白小部件。“轻”和“重”是这两条链的名称。
这里的药物由蛋白链组成,不是一个小分子。文件还列出一个叫 NAG 的糖,
有些部件的编号后面带字母,像门牌号多了一个 A。这个字母要保留,才不会把两个不同部件当成同一个。2
看看这一步的 Python 代码(可以先跳过)
KEY = "trastuzumab"
data = snapshot(KEY)
atoms = parse_pdb(data["pdb"])
heavy = atoms[(atoms.occupancy > 0) & ~atoms.element.isin(["H", "D"])].copy()
assert heavy.serial.is_unique
ca = heavy[(heavy.record == "ATOM") & (heavy.atom == "CA")]
identity = []
for entity in data["entities"]:
for chain in entity["entity_poly"]["pdbx_strand_id"].split(","):
identity.append(dict(chain=chain, molecule=entity["rcsb_polymer_entity"]["pdbx_description"],
deposited_sequence_length=entity["entity_poly"]["rcsb_sample_sequence_length"],
observed_CA_residues=int((ca.chain == chain).sum())))
display(pd.DataFrame(identity))
print("PDB:", data["pdb_id"], "; resolution (Å):", data["entry"]["rcsb_entry_info"]["resolution_combined"])
print("Revision:", data["entry"]["rcsb_accession_info"]["revision_date"])
print("Selected atom rows:", len(atoms), "; usable heavy atoms:", len(heavy),
"; zero occupancy:", int((atoms.occupancy == 0).sum()))
print("Non-polymer components:", sorted(atoms.loc[atoms.record == "HETATM", "residue"].unique()))
drug = heavy[(heavy.record == "ATOM") & heavy.chain.isin(["A","B"])].copy()
target = heavy[(heavy.record == "ATOM") & (heavy.chain == "C")].copy()
assert set(drug.chain) == {"A","B"} and not target.empty
print("Fab heavy atoms:", len(drug), "; HER2 heavy atoms:", len(target))
print("Residues carrying insertion codes:",
len(drug.loc[drug.icode != "", ["chain","resnum","icode"]].drop_duplicates()))
| chain | molecule | deposited_sequence_length | observed_CA_residues | |
|---|---|---|---|---|
| 0 | A | Herceptin Fab (antibody) - light chain | 214 | 214 |
| 1 | B | Herceptin Fab (antibody) - heavy chain | 220 | 220 |
| 2 | C | Receptor protein-tyrosine kinase erbB-2 | 607 | 581 |
PDB: 1N8Z ; resolution (Å): [2.52] Revision: 2024-10-30T00:00:00.000+00:00 Selected atom rows: 7890 ; usable heavy atoms: 7890 ; zero occupancy: 0 Non-polymer components: ['HOH', 'NAG', 'SO4'] Fab heavy atoms: 3287 ; HER2 heavy atoms: 4491 Residues carrying insertion codes: 0
3. 跟着绿色、橙色、蓝色认模型
绿色是 HER2,橙色是 Fab 轻链 A,蓝色是 Fab 重链 B。转动模型,找找橙色和蓝色在哪里靠近绿色。它们是贴满整个蛋白,还是集中在一小块地方?
下面的散点图沿用同样的颜色,方便看整体摆放。坐标轴帮助我们记录模型中的位置。
这张图专心展示两边的蛋白链。2
看看这一步的 Python 代码(可以先跳过)
viewer = py3Dmol.view(width="100%",height=480)
viewer.addModel(pdb_atom_text(pd.concat([target,drug])),"pdb")
for chain,color in [("C","#18785f"),("A","#bc6541"),("B","#416aa6")]:
viewer.setStyle({"chain":chain},{"cartoon":{"color":color}})
viewer.setBackgroundColor("white");viewer.zoomTo();viewer.zoom(.7)
display(HTML(viewer._make_html()))
fig=plt.figure(figsize=(8,5));ax=fig.add_subplot(111,projection="3d")
for chain,color,label in [("C","#18785f","HER2 C"),("A","#bc6541","Fab light A"),("B","#416aa6","Fab heavy B")]:
frame=ca[ca.chain==chain]
ax.scatter(frame.x,frame.y,frame.z,s=10,c=color,alpha=.6,label=label)
ax.set(xlabel="x (Å)",ylabel="y (Å)",zlabel="z (Å)",title="1N8Z: an antibody meets a target surface")
ax.set_box_aspect(np.ptp(pd.concat([target,drug])[["x","y","z"]].to_numpy(),axis=0))
ax.legend(loc="upper left");plt.tight_layout();plt.show()
3Dmol.js failed to load for some reason. Please check your browser console for error messages.
4. 小部件的数量,和“配对”的数量,要分开数
还是用不超过 4 Å 就算近邻的规则。一对,就是一个 Fab 部件和一个 HER2 部件,它们最近的原子靠得足够近。结果是:14 个 Fab 部件、17 个 HER2 部件,组成 32 对近邻。
为什么数字不一样?想象三个小朋友,每人都和两位老师打招呼。小朋友有三个,老师有两位,但“小朋友—老师”的配对可以有六对。一个人可以出现在多对里,蛋白小部件也是一样。
柱形图这样读:橙柱数到 8 个轻链部件,蓝柱数到 6 个重链部件。两条链都参与了接触。3
看看这一步的 Python 代码(可以先跳过)
CUTOFF=4.0
left_residues,right_residues,distances=residue_minimum_distances(drug,target)
left_profile=left_residues.copy();left_profile["label"]=residue_labels(left_residues)
left_profile["min_distance_A"]=distances.min(axis=1)
target_profile=right_residues.copy();target_profile["label"]=residue_labels(right_residues)
target_profile["min_distance_A"]=distances.min(axis=0)
contact_mask=distances<=CUTOFF
contributions=pd.DataFrame([dict(Fab_chain=chain,
unique_Fab_residues=int(((left_profile.chain==chain)&(left_profile.min_distance_A<=CUTOFF)).sum()),
residue_pair_edges=int(contact_mask[left_profile.chain.to_numpy()==chain].sum())) for chain in ["A","B"]])
display(contributions)
print("Unique HER2 interface residues:",int((target_profile.min_distance_A<=CUTOFF).sum()),
"; residue-pair edges:",int(contact_mask.sum()))
display(target_profile[target_profile.min_distance_A<=CUTOFF].sort_values("min_distance_A").head(12))
fig,ax=plt.subplots(figsize=(6,3))
ax.bar(["Fab light A","Fab heavy B"],contributions.unique_Fab_residues,color=["#bc6541","#416aa6"])
ax.set(ylabel="Unique Fab residues within cutoff",title="Geometric contribution of each Fab chain (≤4 Å)")
plt.tight_layout();plt.show()
| Fab_chain | unique_Fab_residues | residue_pair_edges | |
|---|---|---|---|
| 0 | A | 8 | 16 |
| 1 | B | 6 | 16 |
Unique HER2 interface residues: 17 ; residue-pair edges: 32
| chain | resnum | icode | residue | atom_count | label | min_distance_A | |
|---|---|---|---|---|---|---|---|
| 566 | C | 593 | LYS | 9 | C:LYS593 | 2.416987 | |
| 543 | C | 560 | ASP | 8 | C:ASP560 | 2.600914 | |
| 554 | C | 571 | PRO | 7 | C:PRO571 | 2.887824 | |
| 552 | C | 569 | LYS | 9 | C:LYS569 | 2.957163 | |
| 569 | C | 596 | ASP | 8 | C:ASP596 | 2.985947 | |
| 575 | C | 602 | GLN | 9 | C:GLN602 | 3.047644 | |
| 555 | C | 572 | PRO | 7 | C:PRO572 | 3.109259 | |
| 541 | C | 558 | GLU | 9 | C:GLU558 | 3.133440 | |
| 556 | C | 573 | PHE | 11 | C:PHE573 | 3.416137 | |
| 553 | C | 570 | ASP | 8 | C:ASP570 | 3.495785 | |
| 540 | C | 557 | PRO | 7 | C:PRO557 | 3.511510 | |
| 571 | C | 598 | GLU | 9 | C:GLU598 | 3.520107 |
5. 在颜色表里找靠得近的配对
每一行是一个 Fab 部件,每一列是一个 HER2 部件。每格显示这对部件的最近原子距离。黄色偏近,蓝紫色偏远;跟着右边的颜色尺读,不用猜名称是什么意思。
图里只放入至少有一个近邻的部件。一个部件可以靠近这个伙伴,却离另一个较远,所以整行不一定都是黄色。超过 8 Å 的值会显示同一个最远颜色。3
看看这一步的 Python 代码(可以先跳过)
left_indices=np.flatnonzero(contact_mask.any(axis=1))
right_indices=np.flatnonzero(contact_mask.any(axis=0))
fig,ax=plt.subplots(figsize=(10,7))
if len(left_indices) and len(right_indices):
image=ax.imshow(distances[np.ix_(left_indices,right_indices)],aspect="auto",cmap="viridis_r",vmin=2,vmax=8)
ax.set(xticks=range(len(right_indices)),xticklabels=target_profile.label.iloc[right_indices],
yticks=range(len(left_indices)),yticklabels=left_profile.label.iloc[left_indices])
plt.setp(ax.get_xticklabels(),rotation=70,ha="right")
fig.colorbar(image,ax=ax,label="Minimum heavy-atom distance (Å; scale capped at 8)")
else:
ax.text(.5,.5,"No interface at this cutoff",ha="center",transform=ax.transAxes)
ax.set(xlabel="HER2 author residue ID",ylabel="Fab author residue ID",title="Antibody–target geometry; not interaction energy")
plt.tight_layout();plt.show()
6. 沿着珠子串,找相遇的小区域
想象把 HER2 的小部件,按顺序放成带编号的珠子串。这张图帮助我们沿着顺序找位置。横轴是 HER2 部件的编号,纵轴是它到 Fab 最近原子的距离。
绿点是能在模型中找到位置的 HER2 部件;紫点符合 4 Å 的近邻规则。看看紫点在哪里聚集:近邻集中在串上的一小块区域,没有铺满整串。
缺少的点,表示那个部件没有可用位置;3
看看这一步的 Python 代码(可以先跳过)
fig,ax=plt.subplots(figsize=(9,3))
ax.scatter(target_profile.resnum,target_profile.min_distance_A,s=9,c="#18785f",alpha=.6,label="Observed HER2 residues")
interface=target_profile[target_profile.min_distance_A<=CUTOFF]
ax.scatter(interface.resnum,interface.min_distance_A,s=28,c="#9974a5",label="Within cutoff")
ax.axhline(CUTOFF,ls="--",c="#bc6541")
ax.set(xlabel="HER2 deposited author residue number",ylabel="Minimum distance to Fab (Å)",title="A localized protein surface is recognized")
ax.legend();plt.tight_layout();plt.show()
7. 近邻更多,不一定“握得更牢”
把边界放大,可能数到更多近邻。模型没有动,变的是计数规则。曲线里的几条线,分别数 Fab 部件、HER2 部件和配对,它们数的不是同一种东西。
小药物和大抗体的大小、摆放方式不同。可以把图片和计数放在一起比较。
今天的结果描述两者相遇的地方。要知道它们结合得多牢,或怎样改变细胞的行为,科学家还要另做实验。4
看看这一步的 Python 代码(可以先跳过)
threshold_table=pd.DataFrame([dict(cutoff_A=t,Fab_residues=int((distances.min(axis=1)<=t).sum()),
HER2_residues=int((distances.min(axis=0)<=t).sum()),residue_pair_edges=int((distances<=t).sum()))
for t in [0.01,3.,3.5,4.,4.5,5.,6.]])
assert np.all(np.diff(threshold_table[["Fab_residues","HER2_residues","residue_pair_edges"]],axis=0)>=0)
display(threshold_table)
fig,ax=plt.subplots(figsize=(7,3))
for column in ["Fab_residues","HER2_residues","residue_pair_edges"]:
ax.plot(threshold_table.cutoff_A,threshold_table[column],"o-",label=column)
ax.axvline(CUTOFF,ls="--",c="#bc6541")
ax.set(xlabel="Geometric cutoff (Å)",ylabel="Count (denominator shown in legend)",title="Interface definition sensitivity")
ax.legend();plt.tight_layout();plt.show()
| cutoff_A | Fab_residues | HER2_residues | residue_pair_edges | |
|---|---|---|---|---|
| 0 | 0.01 | 0 | 0 | 0 |
| 1 | 3.00 | 6 | 5 | 6 |
| 2 | 3.50 | 10 | 10 | 16 |
| 3 | 4.00 | 14 | 17 | 32 |
| 4 | 4.50 | 20 | 19 | 42 |
| 5 | 5.00 | 21 | 19 | 47 |
| 6 | 6.00 | 26 | 22 | 76 |
8. 核对答案、保存结果,再讲一遍故事
换一种办法核对最近的一对,再把整个模型一起搬动,检查距离有没有保持不变。把表格和模型存进 results/08-trastuzumab-interface/。
如果边界太小,找不到配对,空表格就是诚实的答案。
讲给别人听:“小药物坐在口袋里;抗体的一部分贴在蛋白表面。两课都在量谁靠近谁。”4
看看这一步的 Python 代码(可以先跳过)
# Independent atom-pair calculation for the closest residue pair.
i, j = np.unravel_index(np.argmin(distances), distances.shape)
def residue_atoms(frame, row):
return frame[(frame.chain == row.chain) & (frame.resnum == row.resnum) &
(frame.icode == row.icode) & (frame.residue == row.residue)]
left_test = residue_atoms(drug, left_residues.iloc[i])
right_test = residue_atoms(target, right_residues.iloc[j])
manual_minimum = min(float(np.linalg.norm(a-b))
for a in left_test[["x","y","z"]].to_numpy() for b in right_test[["x","y","z"]].to_numpy())
assert np.isclose(manual_minimum, distances[i,j], atol=1e-12)
# A common rigid translation cannot change any distance.
shifted_drug = drug.copy(); shifted_target = target.copy()
shifted_drug[["x","y","z"]] += np.array([10., -7., 3.])
shifted_target[["x","y","z"]] += np.array([10., -7., 3.])
_, _, shifted_distances = residue_minimum_distances(shifted_drug, shifted_target)
assert np.allclose(distances, shifted_distances, atol=1e-12)
# Empty selections fail explicitly; a zero-contact threshold is a valid result.
try:
residue_minimum_distances(drug.iloc[:0], target)
except ValueError:
print("Empty selection: reported correctly")
else:
raise AssertionError("Empty selection was accepted")
assert not (distances <= 0.01).any()
assert np.isfinite(distances).all() and (distances >= 0).all()
print("PASS: independent atom minimum, rigid translation, finite distances, empty selections and empty contacts")
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
target_profile.to_csv(OUTPUT_DIR / "target-residue-distances.csv", index=False)
threshold_table.to_csv(OUTPUT_DIR / "threshold-sensitivity.csv", index=False)
pd.DataFrame(distances, index=residue_labels(left_residues), columns=residue_labels(right_residues)).to_csv(
OUTPUT_DIR / "residue-minimum-distances.csv")
(OUTPUT_DIR / "selected-complex.pdb").write_text(pdb_atom_text(pd.concat([target, drug])), encoding="ascii")
summary = dict(pdb_id=data["pdb_id"], cutoff_angstrom=CUTOFF,
atom_selection="positive occupancy heavy atoms; parser selects one altloc per atom",
target_chains=sorted(target.chain.unique()), drug_chains=sorted(drug.chain.unique()),
target_residues_within_cutoff=int((target_profile.min_distance_A <= CUTOFF).sum()),
contacting_residue_pairs=int((distances <= CUTOFF).sum()),
method="minimum atom distance, not affinity, interaction energy or buried surface area")
(OUTPUT_DIR / "analysis-summary.json").write_text(json.dumps(summary, indent=2)+"\n")
print("Exported:", sorted(p.name for p in OUTPUT_DIR.iterdir()))
left_profile.to_csv(OUTPUT_DIR / "Fab-residue-distances.csv",index=False)
rows,cols=np.nonzero(contact_mask)
contact_edges=pd.DataFrame(dict(Fab_residue=np.array(residue_labels(left_residues))[rows],
HER2_residue=np.array(residue_labels(right_residues))[cols],distance_A=distances[rows,cols]))
contact_edges.to_csv(OUTPUT_DIR / "interface-residue-pairs.csv",index=False)
print("Exported Fab profile and",len(contact_edges),"geometric interface edges")
Empty selection: reported correctly PASS: independent atom minimum, rigid translation, finite distances, empty selections and empty contacts
Exported: ['Fab-residue-distances.csv', 'analysis-summary.json', 'interface-residue-pairs.csv', 'residue-minimum-distances.csv', 'selected-complex.pdb', 'target-residue-distances.csv', 'threshold-sensitivity.csv'] Exported Fab profile and 32 geometric interface edges
自主练习与参考答案
- 三个小朋友,每人都和两位老师打招呼。总共几个人?可以组成几对“小朋友—老师”?
- 把边界缩到很小的 0.01 Å,表格里还会有哪些配对?
参考解释。 一共五个人,可以组成六对。人数和配对数是不同的计数。此时图与表格显示零近邻。
先保留当前 notebook 副本,再修改参数。把新图与原图一起比较,并记录改变了哪些输入、哪些计算和哪些结论。
可选:请 AI 帮忙改一点代码
提示词 1
只用已列包,在三维视图中分别标出 3.5/4/5 Å 下 HER2 界面残基。保留作者插入码,与导出的靶蛋白表核对,不将结果标为已验证表位。
提示词 2
分别增加 HER2 到 Fab 轻链 A、重链 B 的最短距离曲线。保留正占有率重原子选择与零接触场景,只用 NumPy/Pandas/SciPy/Matplotlib/py3Dmol 和标准库,不预测亲和力。
人工验收。 手动重新运行。每个 HER2 残基的整体最短距离须等于 A 单链与 B 单链结果的较小值;增加阈值后接触数量不能下降,CSV 保留插入码和空界面报告。
检查修改后的源代码,再手动运行受影响单元及后续校验。要求助手解释修改的目的、使用的包和数据来源,并用实际输出核对解释。
可选:联网看看数据来源
看看这一步的 Python 代码(可以先跳过)
RUN_ONLINE_UPDATE=False
if RUN_ONLINE_UPDATE:
import requests
try:
response=requests.get('https://data.rcsb.org/rest/v1/core/entry/1N8Z',timeout=20)
response.raise_for_status()
print("Source:",response.url)
print(response.text[:700])
except requests.RequestException as error:
print("Online extension unavailable:",type(error).__name__,str(error)[:160])
else:
print("Online extension skipped; embedded core data unchanged")
Online extension skipped; embedded core data unchanged
来源、快照与下一步
Drug complex 1N8Z coordinates — CC0 1.0 (wwPDB data); credit depositors and primary publication; retrieved 2026-10-10. SHA-256
ed934d36503335c6764b08880f9e50933d99eb13b9bf145339cfcaee150eb21e. Preserve complete original PDB; first-model, occupancy and chain selections are visible student code. No docking or coordinate registration.Drug complex 1N8Z entry metadata — CC0 1.0 (wwPDB data); credit depositors and primary publication; retrieved 2026-10-10. SHA-256
7d86208b30f290f045ce5444f071bca574e417fa1f2a1b8bac463cf7c19bff70. Preserve experiment, revision and primary citation metadata.Drug complex 1N8Z polymer entity 1 — CC0 1.0 (wwPDB data); credit depositors and primary publication; retrieved 2026-10-10. SHA-256
996883fdd4c89becba41a942b412553ae95c4ab762deb2b0137f6eba04a37cd7. Preserve entity identity, source species, sequences and author chain IDs; do not infer a chain role from color.Drug complex 1N8Z polymer entity 2 — CC0 1.0 (wwPDB data); credit depositors and primary publication; retrieved 2026-10-10. SHA-256
d853860110234b7dc3f3fe562976ddd867cdd688c97cac4af9f324dc9f53e462. Preserve entity identity, source species, sequences and author chain IDs; do not infer a chain role from color.Drug complex 1N8Z polymer entity 3 — CC0 1.0 (wwPDB data); credit depositors and primary publication; retrieved 2026-10-10. SHA-256
2bf10361f07b58c0f3121c10f0b61f7ded3ab2e01d0998b285f434e5b42cc448. Preserve entity identity, source species, sequences and author chain IDs; do not infer a chain role from color.
接下来做什么? 先用自己的话,把图片讲给另一个人听。再试着改一个小参数,比较前后结果。换成其他真实结构时,先确认里面是谁,以及各部分在空间中的位置是否真的能放在一起比较。
注释
1 简笔画采用简化的形状和珠子数量。实验坐标来自所引 PDB 记录;统计图和可旋转模型保留了实际运行输出。
2 1N8Z 包含人 HER2 胞外区构建体 C,以及曲妥珠单抗 Fab 轻链 A、重链 B,分辨率为 2.52 Å。曲妥珠单抗属于人源化抗体;来源实体的物种注释描述提交的构建体。
3 距离取正占有率重原子对的最小欧氏距离。解析时选择第一个模型及一个替代原子位置;保留作者链、残基编号和插入码。默认近邻边界为 4 Å;热图色阶为 2–8 Å,更大的值使用最远端颜色。
4 结合强度、相互作用类型和生物功能应结合相应实验证据评估。本课计数取决于坐标模型与距离边界。比较两个独立结构文件之前,需要建立可靠的共同空间定位。