Case 1:KRAS G12D¶

KRAS像是细胞内的“开关”(分子开关),属于 RAS/MAPK 信号通路的关键部分。它主要负责接收来自细胞膜表面的生长信号,然后指挥细胞进行生长、增殖或分化。

当 KRAS 基因发生特定突变时,这个“开关”就会被永远卡在“开启”(On)的状态。它不再需要外部信号的激活,就会疯狂地向细胞内部发送生长指令,从而导致细胞失控、异常增殖,最终诱发肿瘤。

G(Glycine,甘氨酸)D(Aspartic acid,天冬氨酸)

1. 获取RAS原癌基因家族,不同物种同源序列¶

In [8]:
from pathlib import Path
import csv
import io
import pandas as pd
import requests
from itables import options, show

options.warn_on_undocumented_option = False

# UniProt entries requested explicitly by the analysis task.
TARGET_ENTRIES = {
    "KRAS_HUMAN": "P01116",
    "KRAS_MOUSE": None,
    "KRAS_ZEBRAFISH": None,
    "NRAS_HUMAN": None,
    "HRAS_HUMAN": None,
}
TARGET_QUERIES = {
    "KRAS_MOUSE": "gene_exact:KRAS AND organism_id:10090",
    "KRAS_ZEBRAFISH": "gene_exact:KRAS AND organism_id:7955",
    "NRAS_HUMAN": "gene_exact:NRAS AND organism_id:9606",
    "HRAS_HUMAN": "gene_exact:HRAS AND organism_id:9606",
}
GENES = ("KRAS", "NRAS", "HRAS")
MIN_SEQUENCES = 10
MAX_SEQUENCES = 30
N_SEQUENCES = 20

UNIPROT_URL = "https://rest.uniprot.org/uniprotkb/search"


def uniprot_search(query, size=100):
    """Return UniProtKB records as dictionaries, including the protein sequence."""
    fields = (
        "accession,id,protein_name,gene_names,organism_name,length,sequence"
    )
    response = requests.get(
        UNIPROT_URL,
        params={
            "query": query,
            "format": "tsv",
            "fields": fields,
            "size": size,
        },
        timeout=60,
    )
    response.raise_for_status()
    rows = list(csv.DictReader(io.StringIO(response.text), delimiter="\t"))
    return [row for row in rows if row.get("Sequence")]


# Resolve the requested entry names using gene and species filters. The accession
# for KRAS_HUMAN is fixed explicitly as P01116. Some species only have an
# unreviewed UniProtKB record, so target queries do not require reviewed:true.
for entry_name, query in TARGET_QUERIES.items():
    records = uniprot_search(query, size=5)
    if not records:
        raise RuntimeError(f"UniProt entry not found for query: {query}")
    TARGET_ENTRIES[entry_name] = records[0]["Entry"]

# Fetch reviewed KRAS/NRAS/HRAS proteins across species as the homolog set.
gene_query = " OR ".join(f"gene_exact:{gene}" for gene in GENES)
homologs = uniprot_search(f"({gene_query}) AND reviewed:true", size=100)

# Keep requested records first, then add distinct homologs until the requested
# count is reached. N_SEQUENCES can be changed to any value from 10 to 30.
if not MIN_SEQUENCES <= N_SEQUENCES <= MAX_SEQUENCES:
    raise ValueError("N_SEQUENCES must be between 10 and 30")

requested_accessions = list(TARGET_ENTRIES.values())
records_by_accession = {record["Entry"]: record for record in homologs}
for accession in requested_accessions:
    if accession not in records_by_accession:
        records_by_accession[accession] = uniprot_search(
            f"accession:{accession}", size=1
        )[0]

selected = [records_by_accession[accession] for accession in requested_accessions]
selected_accessions = set(requested_accessions)
for record in homologs:
    if len(selected) >= N_SEQUENCES:
        break
    if record["Entry"] not in selected_accessions:
        selected.append(record)
        selected_accessions.add(record["Entry"])

if not MIN_SEQUENCES <= len(selected) <= MAX_SEQUENCES:
    raise RuntimeError(f"Expected 10-30 sequences, got {len(selected)}")

# Save a FASTA file for downstream alignment and a TSV table for inspection.
output_dir = Path("sequence_data")
output_dir.mkdir(exist_ok=True)
fasta_path = output_dir / "KRAS_NRAS_HRAS_homologs.fasta"
tsv_path = output_dir / "KRAS_NRAS_HRAS_homologs.tsv"

with fasta_path.open("w", encoding="utf-8") as fasta_file:
    for record in selected:
        description = (
            f"{record['Entry']}|{record['Organism']}|"
            f"{record['Protein names']}"
        )
        fasta_file.write(f">{description}\n")
        sequence = record["Sequence"]
        fasta_file.write("\n".join(sequence[i:i + 80] for i in range(0, len(sequence), 80)))
        fasta_file.write("\n")

with tsv_path.open("w", encoding="utf-8", newline="") as tsv_file:
    writer = csv.DictWriter(
        tsv_file,
        fieldnames=["Entry", "Entry Name", "Protein names", "Gene Names", "Organism", "Length"],
        delimiter="\t",
        extrasaction="ignore",
    )
    writer.writeheader()
    for record in selected:
        writer.writerow(record)

print(f"获取到 {len(selected)} 条蛋白序列")
print(f"FASTA: {fasta_path}")
print(f"信息表: {tsv_path}")
print("\n".join(f"{record['Entry']}\t{record['Organism']}\t{record['Length']} aa" for record in selected))

# Display a searchable, sortable, paginated table in the notebook and HTML export.
sequence_table = pd.DataFrame(selected)[
    ["Entry", "Entry Name", "Gene Names", "Organism", "Length", "Protein names"]
].rename(
    columns={
        "Entry": "UniProt编号",
        "Entry Name": "条目名称",
        "Gene Names": "基因名称",
        "Organism": "物种",
        "Length": "长度(aa)",
        "Protein names": "蛋白名称",
    }
)
show(
    sequence_table,
    paging=True,
    pageLength=10,
    searching=True,
    scrollX=True,
)
获取到 20 条蛋白序列
FASTA: sequence_data/KRAS_NRAS_HRAS_homologs.fasta
信息表: sequence_data/KRAS_NRAS_HRAS_homologs.tsv
P01116	Homo sapiens (Human)	189 aa
P32883	Mus musculus (Mouse)	189 aa
A0A2R8Q8S1	Danio rerio (Zebrafish) (Brachydanio rerio)	189 aa
P01111	Homo sapiens (Human)	189 aa
P01112	Homo sapiens (Human)	189 aa
Q04970	Rattus norvegicus (Rat)	189 aa
P08556	Mus musculus (Mouse)	189 aa
P20171	Rattus norvegicus (Rat)	189 aa
Q61411	Mus musculus (Mouse)	189 aa
P08644	Rattus norvegicus (Rat)	189 aa
P12825	Cavia porcellus (Guinea pig)	189 aa
P08642	Gallus gallus (Chicken)	189 aa
Q07983	Monodelphis domestica (Gray short-tailed opossum)	188 aa
P79737	Danio rerio (Zebrafish) (Brachydanio rerio)	188 aa
Q5F352	Gallus gallus (Chicken)	189 aa
Q5RD87	Pongo abelii (Sumatran orangutan) (Pongo pygmaeus abelii)	189 aa
Q95ME4	Monodelphis domestica (Gray short-tailed opossum)	189 aa
Q2MJK3	Sus scrofa (Pig)	189 aa
Q91806	Xenopus laevis (African clawed frog)	189 aa
Q60529	Mesocricetus auratus (Golden hamster)	96 aa
Loading ITables v2.9.1 from the internet... (need help?)
ⓘUniProt编号 条目名称 基因名称 物种 长度(aa) 蛋白名称
P01116RASK_HUMANKRAS KRAS2 RASK2Homo sapiens (Human)189GTPase KRas (EC 3.6.5.2) (K-Ras 2) (Ki-Ras) (c-K-ras) (c-Ki-ras) [Cleaved into: GTPase KRas, N-terminally processed]
P32883RASK_MOUSEKras Kras2Mus musculus (Mouse)189GTPase KRas (EC 3.6.5.2) (K-Ras 2) (Ki-Ras) (c-K-ras) (c-Ki-ras) [Cleaved into: GTPase KRas, N-terminally processed]
A0A2R8Q8S1A0A2R8Q8S1_DANREkras fa04e08 fc14b12 fc23g10 fj89d12 K-ras wu:fa04e08 wu:fc14b12 wu:fc23g10 wu:fj89d12 xRAS2 zgc:85725Danio rerio (Zebrafish) (Brachydanio rerio)189small monomeric GTPase (EC 3.6.5.2)
P01111RASN_HUMANNRAS HRAS1Homo sapiens (Human)189GTPase NRas (EC 3.6.5.2) (Transforming protein N-Ras)
P01112RASH_HUMANHRAS HRAS1Homo sapiens (Human)189GTPase HRas (EC 3.6.5.2) (H-Ras-1) (Ha-Ras) (Transforming protein p21) (c-H-ras) (p21ras) [Cleaved into: GTPase HRas, N-terminally processed]
Q04970RASN_RATNrasRattus norvegicus (Rat)189GTPase NRas (EC 3.6.5.2) (Transforming protein N-Ras)
P08556RASN_MOUSENrasMus musculus (Mouse)189GTPase NRas (EC 3.6.5.2) (Transforming protein N-Ras)
P20171RASH_RATHras Hras1Rattus norvegicus (Rat)189GTPase HRas (EC 3.6.5.2) (H-Ras-1) (Transforming protein p21) (c-H-ras) (p21ras) [Cleaved into: GTPase HRas, N-terminally processed]
Q61411RASH_MOUSEHras Hras1Mus musculus (Mouse)189GTPase HRas (EC 3.6.5.2) (H-Ras-1) (Transforming protein p21) (c-H-ras) (p21ras) [Cleaved into: GTPase HRas, N-terminally processed]
P08644RASK_RATKras Kras2Rattus norvegicus (Rat)189GTPase KRas (EC 3.6.5.2) (K-Ras 2) (Ki-Ras) (c-K-ras) (c-Ki-ras) [Cleaved into: GTPase KRas, N-terminally processed]
(10 more rows not shown)

2. 获取KRAS G12D蛋白结构¶

PDB 6WS4是 KRAS-G12D/K104Q、GDP-bound 的晶体结构,分辨率 1.84 Å。它同时含有 G12D 和 K104Q 两个突变,因此不能把结构观察到的所有差异都归因于 G12D。

In [9]:
from pathlib import Path
import requests

PDB_ID = "6WS4"
RCSB_PDB_URL = f"https://files.rcsb.org/download/{PDB_ID}.pdb"
structure_dir = Path("structure_data")
structure_dir.mkdir(exist_ok=True)
pdb_path = structure_dir / f"{PDB_ID}.pdb"

response = requests.get(RCSB_PDB_URL, timeout=60)
response.raise_for_status()
pdb_path.write_bytes(response.content)

if pdb_path.stat().st_size == 0:
    raise RuntimeError(f"结构文件为空: {pdb_path}")

print(f"PDB结构文件已下载: {pdb_path}")
print(f"文件大小: {pdb_path.stat().st_size:,} bytes")
print(f"RCSB PDB: https://www.rcsb.org/structure/{PDB_ID}")
PDB结构文件已下载: structure_data/6WS4.pdb
文件大小: 534,924 bytes
RCSB PDB: https://www.rcsb.org/structure/6WS4

3. 获取 KRAS_WT DNA 序列并生成 KRAS_G12D¶

使用 NCBI RefSeq 转录本 NM_004985.5 的 CDS 作为 KRAS_WT。按照 HGVS cDNA 坐标,将 c.35G>A 引入第 35 个 CDS 碱基,并翻译 WT 与 G12D 蛋白序列。

In [11]:
from io import StringIO
from pathlib import Path

import requests
from Bio import SeqIO
from Bio.Seq import Seq
from Bio.SeqRecord import SeqRecord

NCBI_ACCESSION = "NM_004985.5"
NCBI_EFETCH_URL = "https://eutils.ncbi.nlm.nih.gov/entrez/eutils/efetch.fcgi"
CDS_MUTATION_POSITION = 35  # HGVS c.35G>A; CDS positions are 1-based.
WT_BASE = "G"
MUTANT_BASE = "A"

response = requests.get(
    NCBI_EFETCH_URL,
    params={
        "db": "nuccore",
        "id": NCBI_ACCESSION,
        "rettype": "gb",
        "retmode": "text",
    },
    timeout=60,
)
response.raise_for_status()

# Parse the downloaded GenBank record with SeqIO and extract its CDS feature.
record = SeqIO.read(StringIO(response.text), "genbank")
cds_features = [feature for feature in record.features if feature.type == "CDS"]
if not cds_features:
    raise RuntimeError(f"未在 {NCBI_ACCESSION} 中找到 CDS 特征")

cds_feature = cds_features[0]
wt_cds = cds_feature.extract(record.seq)
if len(wt_cds) % 3 != 0:
    raise RuntimeError(f"CDS 长度不是 3 的倍数: {len(wt_cds)}")

mutation_index = CDS_MUTATION_POSITION - 1
reference_base = str(wt_cds[mutation_index]).upper()
if reference_base != WT_BASE:
    raise RuntimeError(
        f"c.{CDS_MUTATION_POSITION} 参考碱基应为 {WT_BASE},实际为 {reference_base}"
    )

# Generate KRAS_G12D by changing CDS c.35 G to A.
g12d_cds = Seq(
    str(wt_cds[:mutation_index])
    + MUTANT_BASE
    + str(wt_cds[mutation_index + 1:])
)

wt_protein = wt_cds.translate(to_stop=True)
g12d_protein = g12d_cds.translate(to_stop=True)
protein_position = (CDS_MUTATION_POSITION - 1) // 3 + 1
codon_start = (protein_position - 1) * 3
wt_codon = str(wt_cds[codon_start:codon_start + 3])
g12d_codon = str(g12d_cds[codon_start:codon_start + 3])
wt_amino_acid = str(wt_protein[protein_position - 1])
g12d_amino_acid = str(g12d_protein[protein_position - 1])

if wt_amino_acid != "G" or g12d_amino_acid != "D":
    raise RuntimeError(
        f"未得到预期 G12D 变化: {wt_amino_acid}{protein_position}{g12d_amino_acid}"
    )

sequence_dir = Path("sequence_data")
sequence_dir.mkdir(exist_ok=True)
wt_dna_path = sequence_dir / "KRAS_WT_CDS.fasta"
g12d_dna_path = sequence_dir / "KRAS_G12D_CDS.fasta"
protein_path = sequence_dir / "KRAS_WT_G12D_proteins.fasta"

SeqIO.write(
    SeqRecord(wt_cds, id="KRAS_WT", description=f"{NCBI_ACCESSION} CDS"),
    wt_dna_path,
    "fasta",
)
SeqIO.write(
    SeqRecord(g12d_cds, id="KRAS_G12D", description="KRAS c.35G>A CDS"),
    g12d_dna_path,
    "fasta",
)
SeqIO.write(
    [
        SeqRecord(wt_protein, id="KRAS_WT_protein", description="translated WT KRAS"),
        SeqRecord(g12d_protein, id="KRAS_G12D_protein", description="translated KRAS G12D"),
    ],
    protein_path,
    "fasta",
)

print(f"参考转录本: {NCBI_ACCESSION}")
print(f"WT CDS 长度: {len(wt_cds)} bp")
print(f"c.{CDS_MUTATION_POSITION}{WT_BASE}>{MUTANT_BASE}: {wt_codon} -> {g12d_codon}")
print(
    f"氨基酸变化: {wt_amino_acid}{protein_position}"
    f"{g12d_amino_acid} ({wt_amino_acid} -> {g12d_amino_acid})"
)
print(f"WT DNA FASTA: {wt_dna_path}")
print(f"G12D DNA FASTA: {g12d_dna_path}")
print(f"蛋白 FASTA: {protein_path}")
print("WT 蛋白序列:")
print(wt_protein)
print("KRAS_G12D 蛋白序列:")
print(g12d_protein)
参考转录本: NM_004985.5
WT CDS 长度: 567 bp
c.35G>A: GGT -> GAT
氨基酸变化: G12D (G -> D)
WT DNA FASTA: sequence_data/KRAS_WT_CDS.fasta
G12D DNA FASTA: sequence_data/KRAS_G12D_CDS.fasta
蛋白 FASTA: sequence_data/KRAS_WT_G12D_proteins.fasta
WT 蛋白序列:
MTEYKLVVVGAGGVGKSALTIQLIQNHFVDEYDPTIEDSYRKQVVIDGETCLLDILDTAGQEEYSAMRDQYMRTGEGFLCVFAINNTKSFEDIHHYREQIKRVKDSEDVPMVLVGNKCDLPSRTVDTKQAQDLARSYGIPFIETSAKTRQGVDDAFYTLVREIRKHKEKMSKDGKKKKKKSKTKCVIM
KRAS_G12D 蛋白序列:
MTEYKLVVVGADGVGKSALTIQLIQNHFVDEYDPTIEDSYRKQVVIDGETCLLDILDTAGQEEYSAMRDQYMRTGEGFLCVFAINNTKSFEDIHHYREQIKRVKDSEDVPMVLVGNKCDLPSRTVDTKQAQDLARSYGIPFIETSAKTRQGVDDAFYTLVREIRKHKEKMSKDGKKKKKKSKTKCVIM

4. MAFFT 多重序列比对,获取到 20 条RAS家族与同源蛋白序列,比对结果可视化,WebLogo可视化¶

In [17]:
from collections import Counter
from pathlib import Path
import subprocess

import logomaker
import matplotlib.pyplot as plt
import pandas as pd
from Bio import AlignIO, SeqIO

sequence_dir = Path("sequence_data")
alignment_dir = Path("alignment_data")
alignment_dir.mkdir(exist_ok=True)

input_fasta = sequence_dir / "KRAS_NRAS_HRAS_homologs.fasta"
alignment_fasta = alignment_dir / "KRAS_NRAS_HRAS_mafft.fasta"
alignment_png = alignment_dir / "KRAS_NRAS_HRAS_alignment.png"
logo_png = alignment_dir / "KRAS_NRAS_HRAS_WebLogo.png"

input_records = list(SeqIO.parse(input_fasta, "fasta"))
if len(input_records) != N_SEQUENCES:
    raise RuntimeError(
        f"输入 FASTA 应包含 {N_SEQUENCES} 条序列,实际为 {len(input_records)} 条"
    )

# Run MAFFT and save the aligned FASTA file.
with alignment_fasta.open("w", encoding="utf-8") as alignment_file:
    subprocess.run(
        ["mafft", "--auto", str(input_fasta)],
        check=True,
        stdout=alignment_file,
        stderr=subprocess.PIPE,
        text=True,
    )

alignment = AlignIO.read(alignment_fasta, "fasta")
if len(alignment) != len(input_records):
    raise RuntimeError("MAFFT 输出的序列数量与输入不一致")

alignment_length = alignment.get_alignment_length()
print(f"MAFFT 比对完成: {len(alignment)} 条序列, 比对长度 {alignment_length} aa")
print(f"比对结果: {alignment_fasta}")

# Visualize the alignment as a colored amino-acid character matrix.
amino_acid_colors = {
    **dict.fromkeys("AVLIMFWY", "#2c7fb8"),
    **dict.fromkeys("STNQ", "#41ab5d"),
    **dict.fromkeys("KRH", "#de2d26"),
    **dict.fromkeys("DE", "#756bb1"),
    **dict.fromkeys("CGP", "#fdae6b"),
    "-": "#bdbdbd",
}

fig_width = max(16, alignment_length / 12)
fig, axis = plt.subplots(figsize=(fig_width, 6))
for row_index, aligned_record in enumerate(alignment):
    for column_index, residue in enumerate(str(aligned_record.seq)):
        axis.text(
            column_index,
            row_index,
            residue,
            ha="center",
            va="center",
            fontsize=7,
            family="monospace",
            color=amino_acid_colors.get(residue, "#252525"),
        )

axis.set_xlim(-1, alignment_length)
axis.set_ylim(len(alignment), -1)
axis.set_xlabel("Alignment position")
axis.set_ylabel("Sequence")
axis.set_yticks(range(len(alignment)))
axis.set_yticklabels([record.id.split("|")[0] for record in alignment], fontsize=8)
axis.set_xticks(range(0, alignment_length, 10))
axis.grid(axis="x", color="#eeeeee", linewidth=0.5)
axis.set_title("KRAS / NRAS / HRAS MAFFT multiple sequence alignment")
fig.tight_layout()
fig.savefig(alignment_png, dpi=200, bbox_inches="tight")
plt.show()
plt.close(fig)

# Build an information-content WebLogo from the aligned amino-acid columns.
aligned_sequences = [str(record.seq) for record in alignment]
logo_counts = pd.DataFrame(
    [Counter(column) for column in zip(*aligned_sequences)]
).fillna(0)
logo_counts.index = range(1, alignment_length + 1)
logo_counts = logo_counts.reindex(columns=sorted(logo_counts.columns), fill_value=0)
logo_counts = logo_counts.drop(columns=["-"], errors="ignore")

logo_matrix = logomaker.transform_matrix(
    logo_counts,
    from_type="counts",
    to_type="information",
)

logo_fig_width = fig_width * 3
fig, axis = plt.subplots(figsize=(logo_fig_width, 5))
logomaker.Logo(logo_matrix, ax=axis, color_scheme="chemistry")
axis.set_xlabel("Alignment position")
axis.set_ylabel("Information (bits)")
axis.set_title("KRAS / NRAS / HRAS WebLogo")
axis.set_xticks(range(0, alignment_length, 10))
fig.tight_layout()
fig.savefig(logo_png, dpi=200, bbox_inches="tight")
plt.show()
plt.close(fig)

print(f"比对可视化图: {alignment_png}")
print(f"WebLogo 图: {logo_png}")
MAFFT 比对完成: 20 条序列, 比对长度 190 aa
比对结果: alignment_data/KRAS_NRAS_HRAS_mafft.fasta
No description has been provided for this image
No description has been provided for this image
比对可视化图: alignment_data/KRAS_NRAS_HRAS_alignment.png
WebLogo 图: alignment_data/KRAS_NRAS_HRAS_WebLogo.png

5. PyMOL 打开6WS4,定位G12D 可视化¶

KRAS 曾经被认为“不可成药”,表面“太光滑”(缺乏传统结合口袋)

In [20]:
from pathlib import Path

pdb_id = "6WS4"
structure_dir = Path("structure_data")
pdb_path = structure_dir / f"{pdb_id}.pdb"
pymol_script_path = structure_dir / f"{pdb_id}_G12D_visualization.pml"

if not pdb_path.exists():
    raise FileNotFoundError(f"未找到 PDB 文件: {pdb_path}")

# 6WS4: KRAS is chain A; residue 12 is the G12D site and residue 104 is K104Q.
pymol_script = f'''load {pdb_path.as_posix()}, KRAS_G12D

remove solvent
hide everything
show cartoon, chain A
color slate, chain A

# Highlight the G12D site
select G12D, chain A and resi 12
show sticks, G12D
color red, G12D
label name CA and G12D, "G12D: %s12" % resn

# Highlight the second mutation present in 6WS4
select K104Q, chain A and resi 104
show sticks, K104Q
color orange, K104Q
label name CA and K104Q, "K104Q: %s104" % resn

# Show the bound GDP ligand
select GDP_ligand, chain A and resn GDP
show sticks, GDP_ligand
color yellow, GDP_ligand

set cartoon_transparency, 0.25, chain A
set stick_radius, 0.18
set label_color, white
set label_size, 18

# Save a complete-chain view.
orient chain A
png {structure_dir.as_posix()}/{pdb_id}_full_structure.png, dpi=300, ray=1

# Save a close-up of the G12D site and nearby ligand.
zoom G12D, 8
orient G12D
png {structure_dir.as_posix()}/{pdb_id}_G12D_view.png, dpi=300, ray=1
save {structure_dir.as_posix()}/{pdb_id}_G12D_view.pse
'''

pymol_script_path.write_text(pymol_script, encoding="utf-8")

print(f"PyMOL脚本已生成: {pymol_script_path}")
print("在 PyMOL 命令行执行:")
print(f"@{pymol_script_path.as_posix()}")
PyMOL脚本已生成: structure_data/6WS4_G12D_visualization.pml
在 PyMOL 命令行执行:
@structure_data/6WS4_G12D_visualization.pml
In [21]:
from pathlib import Path
from IPython.display import display, Markdown
import py3Dmol

pdb_text = Path("structure_data/6WS4.pdb").read_text(encoding="utf-8")

# 完整结构:链 A 用 cartoon 显示,G12D、K104Q 和 GDP 保留醒目标记。
full_view = py3Dmol.view(width=700, height=520)
full_view.addModel(pdb_text, "pdb")
full_view.setStyle({"chain": "A"}, {"cartoon": {"color": "slate"}})
full_view.addStyle({"chain": "A", "resi": "12"}, {"stick": {"color": "red", "radius": 0.22}})
full_view.addStyle({"chain": "A", "resi": "104"}, {"stick": {"color": "orange", "radius": 0.22}})
full_view.addStyle({"chain": "A", "resn": "GDP"}, {"stick": {"color": "yellow", "radius": 0.20}})
full_view.addLabel("G12D", {"fontColor": "red", "backgroundColor": "white"}, {"chain": "A", "resi": "12"})
full_view.zoomTo({"chain": "A"})
full_view.setBackgroundColor("white")

display(Markdown("### 6WS4 完整结构(链 A)"))
display(full_view)

# 特写视图:聚焦 G12D 位点,同时显示附近的 K104Q 和 GDP。
site_view = py3Dmol.view(width=700, height=520)
site_view.addModel(pdb_text, "pdb")
site_view.setStyle({"chain": "A"}, {"cartoon": {"color": "lightgray", "opacity": 0.55}})
site_view.addStyle({"chain": "A", "resi": "12"}, {"stick": {"color": "red", "radius": 0.28}})
site_view.addStyle({"chain": "A", "resi": "104"}, {"stick": {"color": "orange", "radius": 0.24}})
site_view.addStyle({"chain": "A", "resn": "GDP"}, {"stick": {"color": "yellow", "radius": 0.22}})
site_view.addLabel("G12D", {"fontColor": "red", "backgroundColor": "white"}, {"chain": "A", "resi": "12"})
site_view.zoomTo({"chain": "A", "resi": "12"})
site_view.setBackgroundColor("white")

display(Markdown("### G12D 突变位点特写"))
display(site_view)

6WS4 完整结构(链 A)¶

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

<py3Dmol.view at 0x7205932440a0>

G12D 突变位点特写¶

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

<py3Dmol.view at 0x720593247a60>