60 分钟真实数据快照已执行的网页阅读版

小药物怎样放进蛋白质口袋?

用伊马替尼与 ABL 的真实模型,像观察小积木放进凹槽一样,找到药物和它旁边的蛋白小部件。

生物信息学 · 第 07 课🔋 离线核心分析⏱ 60 分钟真实数据快照📦 numpy · pandas · scipy · matplotlib · py3Dmol · IPython
先看一张简笔画绿色是蛋白,橙色是小药物。上图认识两部分,下图看药物在口袋中的位置。紫圈选出附近的小部件,虚线标出量距离的位置。
绿色是蛋白,橙色是小药物。上图认识两部分,下图看药物在口袋中的位置。紫圈选出附近的小部件,虚线标出量距离的位置。1

从这里开始:先听故事,再看图片

先认识三个词。 蛋白质可以想成一长串小珠子折成的三维物体。每颗珠子对应一个氨基酸,在蛋白质里也叫残基。原子是组成珠子的更小部件。真实的蛋白质会弯、会动。

两种学法。 想先听懂,就读短讲解、看图片;代码和“多学一点”可以以后再看。想在设备上做一遍,就下载完整 .ipynb,在 VibeIt 中点 + → Import from Files,然后从上到下运行所有代码单元。

下面的统计图和可旋转分子模型,来自代码的实际运行;数据和简笔画已经装在 notebook 里面,不用另找文件。把 notebook 保存在当前工作文件夹即可;这节课不需要 AI 账号。

让工具先准备好

In [1] · Python
看看这一步的 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='07-imatinib-pocket'
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 里面的数据包

In [2] · Python
看看这一步的 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={'imatinib': '5c917b32818fda4715c7887577a22d275c25e7a80f9ebf0e6b55e923f2c1f187'}
def snapshot(key):
    return load_snapshot(RECIPE_ID,SNAPSHOT_HASHES[key],key)
print("Embedded snapshots:",list(SNAPSHOT_HASHES))
Embedded snapshots: ['imatinib']

准备几个会反复用到的小工具

In [3] · Python
看看这一步的 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
In [4] · Python
看看这一步的 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
In [5] · Python
看看这一步的 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
In [6] · Python
看看这一步的 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

今天的模型里有什么?

今天看的是小鼠 ABL 的一部分和药物在实验中一起出现的模型。我们研究这份真实记录,不随意猜一个新位置。2

一起看图、动手试试

1. 一块小积木,怎样影响一台大机器?

想象一台机器上有个小凹槽,平时有个工作零件放在那里。名叫 ABL 的蛋白质,也有用到 ATP 这个小分子的位置。伊马替尼这个药物会占住相关的口袋,让 ABL 更容易保持不工作的形状。转动模型,我们可以看看:药物坐在哪里,旁边有谁?

前面见过的血红素,像机器本来就需要的工作部件。这里的药物,则会影响机器工作。真实蛋白质会动,比硬积木灵活得多。

试着说一句:“今天要找到药物和它旁边的蛋白小部件。”2

2. 先看盒子标签,认清里面是谁

今天的模型,是科学家通过实验得到的:小鼠 ABL 蛋白的一部分,和伊马替尼放在一起的样子。文件编号叫 1IEP,不用背;它像盒子标签,帮助我们认清内容。

文件里有两份相似的蛋白,标成 A、B。我们只看 A 和它自己的药物。A/B 是标签,帮助我们选定一份蛋白。药物在文件里叫 STI。这份文件记录了实验中看清位置的部件。

表格这样读:chain 是标签。A 的说明记录了 293 个小部件,但实验图里能找到 274 个 Cα 参考点的位置;两个数字数的东西不同。Cα 是每颗珠子中用来标位置的一个特定原子。2

In [7] · Python
看看这一步的 Python 代码(可以先跳过)
KEY = "imatinib"
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()))
compound = data["compound"]["chem_comp"]
display(pd.DataFrame([dict(component=compound["id"], formula=compound["formula"],
    formula_weight_Da=compound["formula_weight"], formal_charge=compound["pdbx_formal_charge"])]))
drug = heavy[(heavy.chain == "A") & (heavy.residue == "STI")].copy()
target = heavy[(heavy.chain == "A") & (heavy.record == "ATOM")].copy()
assert len(drug) == 37 and not target.empty
print("Bound drug heavy atoms:", len(drug), "; protein heavy atoms:", len(target))
chain molecule deposited_sequence_length observed_CA_residues
0 A PROTO-ONCOGENE TYROSINE-PROTEIN KINASE ABL 293 274
1 B PROTO-ONCOGENE TYROSINE-PROTEIN KINASE ABL 293 274
PDB: 1IEP ; resolution (Å): [2.1]
Revision: 2023-08-09T00:00:00.000+00:00
Selected atom rows: 4710 ; usable heavy atoms: 4710 ; zero occupancy: 0
Non-polymer components: ['CL', 'HOH', 'STI']
component formula formula_weight_Da formal_charge
0 STI C29 H31 N7 O 493.603 0
Bound drug heavy atoms: 37 ; protein heavy atoms: 2229

3. 转一转,找出橙色的药物

第一幅图:单独看药物。它的形状和位置,是从同一个实验模型里取出来的,方便看清。

第二幅图:绿色丝带是较大的 ABL 蛋白,橙色细棒是药物。丝带帮助我们看清蛋白的走向。

动手试试:转动、放大模型,先找到橙色,再看绿色部分比它大多少。细棒连接帮助我们看清分子的形状。2

In [8] · Python
看看这一步的 Python 代码(可以先跳过)
drug_view = py3Dmol.view(width="100%", height=350)
drug_view.addModel(pdb_atom_text(drug), "pdb")
drug_view.setStyle({}, {"stick":{"colorscheme":"orangeCarbon"}})
drug_view.setHoverable({}, True, "function(a,v){if(!a.label)a.label=v.addLabel(a.atom,{position:a,backgroundColor:'white',fontColor:'black'});}",
    "function(a,v){if(a.label){v.removeLabel(a.label);delete a.label;}}")
drug_view.setBackgroundColor("white"); drug_view.zoomTo(); drug_view.zoom(.7)
display(HTML(drug_view._make_html()))
complex_view = py3Dmol.view(width="100%", height=440)
complex_view.addModel(pdb_atom_text(pd.concat([target, drug])), "pdb")
complex_view.setStyle({"chain":"A"}, {"cartoon":{"color":"#18785f"}})
complex_view.setStyle({"resn":"STI"}, {"stick":{"colorscheme":"orangeCarbon"}})
complex_view.setBackgroundColor("white"); complex_view.zoomTo(); complex_view.zoom(.7)
display(HTML(complex_view._make_html()))

3Dmol.js failed to load for some reason. Please check your browser console for error messages.

3Dmol.js failed to load for some reason. Please check your browser console for error messages.

4. 给药物找“近邻”

想象你在地图上,圈出住在家附近的邻居。我们也给药物找近邻:哪些蛋白小部件,有某个小点离药物足够近?量的是两边原子之间最近的直线距离。

这次约定:距离不超过 4 Å 就算近邻。Å 是很小很小的长度单位,现在不用背换算;先把 4 Å 当成我们画的边界。按这个规则,找到 21 个蛋白小部件。

柱形图这样看:一条柱子代表一个部件;柱子越短,最近距离越小。虚线是 4 Å 的边界。表中 label 是部件名称和编号,min_distance_A 是最近距离。3

In [9] · Python
看看这一步的 Python 代码(可以先跳过)
CUTOFF = 4.0
left_residues, right_residues, distances = residue_minimum_distances(drug, target)
target_profile = right_residues.copy()
target_profile["label"] = residue_labels(right_residues)
target_profile["min_distance_A"] = distances.min(axis=0)
pocket = target_profile[target_profile.min_distance_A <= CUTOFF].sort_values("min_distance_A", kind="stable")
print("Pocket residues:", len(pocket))
display(pocket.head(12))
fig, ax = plt.subplots(figsize=(8,5))
if len(pocket):
    ax.barh(pocket.label.iloc[::-1], pocket.min_distance_A.iloc[::-1], color="#18785f")
else:
    ax.text(.5,.5,"No residues within cutoff",ha="center",transform=ax.transAxes)
ax.axvline(CUTOFF, color="#bc6541", linestyle="--", label=f"Cutoff {CUTOFF:g} Å")
ax.set(xlabel="Minimum heavy-atom distance (Å)", title="1IEP: ABL residues near imatinib")
ax.legend(); plt.tight_layout(); plt.show()
Pocket residues: 21
chain resnum icode residue atom_count label min_distance_A
135 A 360 ILE 8 A:ILE360 2.668500
90 A 315 THR 7 A:THR315 2.883189
93 A 318 MET 8 A:MET318 2.899270
156 A 381 ASP 8 A:ASP381 2.900704
61 A 286 GLU 9 A:GLU286 2.995906
136 A 361 HIS 10 A:HIS361 3.161738
74 A 299 VAL 7 A:VAL299 3.250268
65 A 290 MET 8 A:MET290 3.308218
92 A 317 PHE 11 A:PHE317 3.326891
155 A 380 ALA 5 A:ALA380 3.329238
46 A 271 LYS 9 A:LYS271 3.350683
157 A 382 PHE 11 A:PHE382 3.404990
Computed figure from this code cell

5. 把颜色表当成一把距离尺

这张图是一张由彩色小格子组成的表。一行是药物的一个原子,一列是蛋白的一个近邻部件。每格回答:“这个药物原子,离这个部件中最近的原子有多远?”

黄色偏近,蓝紫色偏远。右边的色条就是尺子。超过 8 Å 的值会显示同一个最远颜色,仅凭颜色分不出谁更远。3

In [10] · Python
看看这一步的 Python 代码(可以先跳过)
atom_to_target = cdist(drug[["x","y","z"]].to_numpy(), target[["x","y","z"]].to_numpy())
groups = list(target.reset_index(drop=True).groupby(["chain","resnum","icode","residue"], sort=False).indices.values())
atom_residue = np.column_stack([atom_to_target[:,indices].min(axis=1) for indices in groups])
assert np.allclose(atom_residue.min(axis=0), target_profile.min_distance_A)
selected = np.flatnonzero(target_profile.min_distance_A.to_numpy() <= CUTOFF)
fig, ax = plt.subplots(figsize=(10,7))
if len(selected):
    image = ax.imshow(atom_residue[:,selected], aspect="auto", cmap="viridis_r", vmin=2, vmax=8)
    ax.set(xticks=range(len(selected)), xticklabels=target_profile.label.iloc[selected],
           yticks=range(len(drug)), yticklabels=drug.atom)
    plt.setp(ax.get_xticklabels(), rotation=70, ha="right")
    fig.colorbar(image, ax=ax, label="Minimum atom-to-residue distance (Å; scale capped at 8)")
else:
    ax.text(.5,.5,"Empty pocket at this cutoff",ha="center",transform=ax.transAxes)
ax.set(xlabel="ABL author residue ID", ylabel="STI drug atom name", title="Observed drug–pocket geometry")
plt.tight_layout(); plt.show()
Computed figure from this code cell

6. 长珠子串折起来,可以围住一块小积木

散点图里:绿点标出蛋白中的参考位置,橙点是药物原子,紫点标出选中的近邻部件。长珠子串折起来以后,串上相隔很远的珠子,也可能一起围住药物。

局部放大图里:紫色细棒是附近的蛋白原子,橙色细棒是药物。A:THR315 这样的标签像地址:A 这份蛋白里,编号 315、类型叫 THR 的部件。不用背这些字母。

转一转模型:看的方向变了,真实距离没变。就像转动一张地图,不会把房子搬走。3

In [11] · Python
看看这一步的 Python 代码(可以先跳过)
target_ca = target[target.atom == "CA"].copy()
target_ca["label"] = residue_labels(target_ca)
pocket_ca = target_ca[target_ca.label.isin(pocket.label)]
fig = plt.figure(figsize=(8,5)); ax = fig.add_subplot(111, projection="3d")
ax.scatter(target_ca.x,target_ca.y,target_ca.z,s=8,c="#18785f",alpha=.25,label="ABL Cα")
ax.scatter(drug.x,drug.y,drug.z,s=25,c="#bc6541",label="Imatinib heavy atoms")
ax.scatter(pocket_ca.x,pocket_ca.y,pocket_ca.z,s=30,c="#9974a5",label="Pocket residue Cα")
ax.set(xlabel="x (Å)",ylabel="y (Å)",zlabel="z (Å)",title="Sequence-distant residues form a drug pocket")
xyz=target[["x","y","z"]].to_numpy(); ax.set_box_aspect(np.ptp(xyz,axis=0))
ax.legend(loc="upper left"); plt.tight_layout(); plt.show()
target_labels = pd.Series(residue_labels(target), index=target.index)
pocket_atoms = target[target_labels.isin(pocket.label)]
pocket_view = py3Dmol.view(width="100%", height=440)
pocket_view.addModel(pdb_atom_text(pd.concat([pocket_atoms, drug])), "pdb")
pocket_view.setStyle({}, {"stick":{"color":"#9974a5","radius":.14}})
pocket_view.setStyle({"resn":"STI"}, {"stick":{"colorscheme":"orangeCarbon","radius":.2}})
for row in pocket.iloc[:4].itertuples():
    position = pocket_ca[pocket_ca.label == row.label]
    if len(position):
        r = position.iloc[0]
        pocket_view.addLabel(row.label, {"position":{"x":float(r.x),"y":float(r.y),"z":float(r.z)},
            "backgroundColor":"white","fontColor":"black","fontSize":12,"backgroundOpacity":.8})
pocket_view.setBackgroundColor("white"); pocket_view.zoomTo(); pocket_view.zoom(.7)
display(HTML(pocket_view._make_html()))
print("Close-up atoms:", len(pocket_atoms)+len(drug))
Computed figure from this code cell

3Dmol.js failed to load for some reason. Please check your browser console for error messages.

Close-up atoms: 208

7. 圈画得大一点,邻居会多一点

把近邻规则从 4 Å 改成 5 Å,圈画得宽一些,可能会多算几个部件。药物和蛋白并没有移动,变的是我们的计数规则。

曲线这样看:横着是距离边界,竖着是邻居数量。圈变大,原来的邻居不会被排除。

我们学到了什么?找到了药物,描述了它周围的部件。下一步可以通过实验测量结合情况和蛋白的工作状态。4

In [12] · Python
看看这一步的 Python 代码(可以先跳过)
threshold_table = pd.DataFrame([dict(cutoff_A=t,
    target_residues=int((target_profile.min_distance_A <= t).sum())) for t in [0.01,3.,3.5,4.,4.5,5.,6.]])
assert np.all(np.diff(threshold_table.target_residues) >= 0)
display(threshold_table)
fig,ax=plt.subplots(figsize=(7,3))
ax.plot(threshold_table.cutoff_A,threshold_table.target_residues,"o-",color="#416aa6")
ax.axvline(CUTOFF,ls="--",color="#bc6541")
ax.set(xlabel="Geometric cutoff (Å)",ylabel="Unique ABL residues",title="Definition sensitivity; not dose–response")
plt.tight_layout(); plt.show()
cutoff_A target_residues
0 0.01 0
1 3.00 5
2 3.50 13
3 4.00 21
4 4.50 25
5 5.00 28
6 6.00 32
Computed figure from this code cell

8. 检查尺子,再把结果装进文件夹

我们做两个容易理解的检查。第一,换一种办法,再量一次最近的那对小点。第二,把整个模型一起搬动:里面的距离应该不变,就像把积木房子整栋搬走,积木间的缝不会因此改变。

代码把表格、模型文件和摘要保存在 results/07-imatinib-pocket/。CSV 是以后还能打开的表格;PDB 保存选中的三维位置。边界太小时,找不到近邻也是正常结果。

讲给别人听:“用 4 Å 的规则,我找到了 21 个蛋白近邻部件。我可以用图片和表格,指出它们的位置。”4

In [13] · Python
看看这一步的 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()))
Empty selection: reported correctly
PASS: independent atom minimum, rigid translation, finite distances, empty selections and empty contacts
Exported: ['analysis-summary.json', 'residue-minimum-distances.csv', 'selected-complex.pdb', 'target-residue-distances.csv', 'threshold-sensitivity.csv']

自主练习与参考答案

  1. 把近邻边界从 4 Å 改成 5 Å,药物真的移动了吗?
  2. 一个人伸出手时,只量身体中心的距离,为什么可能漏掉靠近的手指?

参考解释。 变的是计数规则,不是药物位置。蛋白的侧链像伸出的手:外面的原子可能很近,Cα 参考原子却更远。近邻数量随选定的距离边界变化。

先保留当前 notebook 副本,再修改参数。把新图与原图一起比较,并记录改变了哪些输入、哪些计算和哪些结论。

可选:请 AI 帮忙改一点代码

提示词 1

只用本课列出的 NumPy/Pandas/SciPy/Matplotlib/py3Dmol 与标准库,增加 3.5/4/5 Å 的 CUTOFF 控制,并在三维复合物中给口袋原子重新上色。保留数据与作者编号,不进行对接、不报告结合能。

提示词 2

利用所选实验坐标,给每个药物 N/O 原子列出最近的蛋白原子,称为候选极性邻近关系,不叫已证实氢键。保留原子名、残基编号与 Å 单位;只用已列包。

人工验收。 手动运行全部受影响单元。口袋数量须单调不减,三维原子选择须与表格一致;用 np.linalg.norm 复算一项 N/O 距离,保留原有快照与平移校验。

检查修改后的源代码,再手动运行受影响单元及后续校验。要求助手解释修改的目的、使用的包和数据来源,并用实际输出核对解释。

可选:联网看看数据来源

In [14] · Python
看看这一步的 Python 代码(可以先跳过)
RUN_ONLINE_UPDATE=False
if RUN_ONLINE_UPDATE:
    import requests
    try:
        response=requests.get('https://data.rcsb.org/rest/v1/core/entry/1IEP',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 1IEP coordinates — CC0 1.0 (wwPDB data); credit depositors and primary publication; retrieved 2026-10-10. SHA-256 577386b4b8bc58a570facba0c8a843386842caef5ed7b60106c685d0f68d4218. Preserve complete original PDB; first-model, occupancy and chain selections are visible student code. No docking or coordinate registration.

  • Drug complex 1IEP entry metadata — CC0 1.0 (wwPDB data); credit depositors and primary publication; retrieved 2026-10-10. SHA-256 09499268eccba586c0443425240eccc171748fd24bc964c448e792c2ad96e4c2. Preserve experiment, revision and primary citation metadata.

  • Drug complex 1IEP polymer entity 1 — CC0 1.0 (wwPDB data); credit depositors and primary publication; retrieved 2026-10-10. SHA-256 010c30720f818eb5cd6d32507bacff456173ce04b1f5653a5e4dcbda9b998f8a. Preserve entity identity, source species, sequences and author chain IDs; do not infer a chain role from color.

  • Drug complex STI chemical component — CC0 1.0 (wwPDB data); credit depositors and primary publication; retrieved 2026-10-10. SHA-256 071661c3f0296df1a405b9157d5190d7cc0d50297c13b97a3ce2f7056007cc59. Preserve chemical identity, formula, descriptors and reported atom counts. Atom names and bound coordinates come from 1IEP, not ideal CCD coordinates.

接下来做什么? 先用自己的话,把图片讲给另一个人听。再试着改一个小参数,比较前后结果。换成其他真实结构时,先确认里面是谁,以及各部分在空间中的位置是否真的能放在一起比较。

注释

1 简笔画采用简化的形状和珠子数量。实验坐标来自所引 PDB 记录;统计图和可旋转模型保留了实际运行输出。

2 1IEP 包含鼠源 c-Abl 激酶结构域构建体和 STI(伊马替尼),分辨率为 2.10 Å。本课选择作者链 A 及其自身配体。

3 距离取正占有率重原子对的最小欧氏距离。解析时选择第一个模型及一个替代原子位置;保留作者链、残基编号和插入码。默认近邻边界为 4 Å;热图色阶为 2–8 Å,更大的值使用最远端颜色。

4 结合强度、相互作用类型和生物功能应结合相应实验证据评估。本课计数取决于坐标模型与距离边界。比较两个独立结构文件之前,需要建立可靠的共同空间定位。