生物大分子批量仿真开发教程(18):实战二——双抗可开发性与界面筛选流水线
版本声明块
- 工具/软件:Schrödinger Release 2025-4(BioLuminate/PIPER)、MOE 2024.06(Pro:Pro Dock 自本版本起自动选抗体为配体)、NetMHCIIpan-4.0、TepiTool
- 语言/环境:Python 3.10 + pandas + PyYAML;作业提交复用第 16 篇
antibody_pipeline骨架- 本文目标:把第 13/14/15 篇的双抗与可开发性组件,装配成一条 YAML 配置驱动、可换后端、可审计的界面筛选流水线。
一句话结论:双抗筛选比单抗多出"界面"这一维——流水线必须按"格式选择(Knobs-into-holes(KiH)/CrossMab/臂交换)→ CH3 突变枚举 → 复合物组装建模 → PIPER 或 Pro:Pro Dock 对接与重排 → 聚集倾向与免疫原性(NetMHCIIpan-4.0/TepiTool)三域联合排序"推进,用一份带 SHA-256 哈希的 YAML 固定版本、并行、阈值与权重,任何一域单指标都无权淘汰候选。
〇、本篇要解决的认知问题
- 双特异性抗体(bispecific antibody,双抗)的 KiH、CrossMab、DuoBody 格式差异,在计算流水线里如何选、选了影响什么?
- CH3 异源二聚化界面的突变枚举怎么做,组合爆炸怎么剪枝?
- 双抗复合物建模与对接复用前面哪几篇的哪些组件?
- 界面分数、聚集倾向、免疫原性三域指标如何合成一个可辩护的统一排序?
- 为什么这条流水线要用 YAML 配置驱动,配置文件里必须有什么?
一、机制解析
1.1 双抗的三张"错配税单"
单抗流水线(第 17 篇)默认一条重链一条轻链自配自己;双抗把这个假设打碎,产生三张必须建模的税单:
| 税单 | 机制 | 计算后果 | 结构学解法(文献锚点) |
|---|---|---|---|
| 重链-重链错配 | 两条不同特异性重链在 CH3 界面随机配对 | 异源二聚体比例低,界面打分对象不明 | KiH:Knobs-into-holes 大中小互补(Ridgway 1996);CrossMab 亦含 CH3 改造 |
| 轻链错配 | 两条不同轻链随机搭上两臂 | 出现四种组合,只一种是要的 | CrossMab 结构域交换、DuoBody 臂交换(Labrijn 2013) |
| 臂-臂交互 | 两抗原结合域在同一分子内互相干扰 | 表位可达性、聚集热点都变了 | 连接区柔性设计;仿真里做界面接触普查 |
格式选择不是生物学口味问题而是流水线分支:选 KiH-IgG1 就枚举 CH3 突变、装配异源二聚体 Fc;选 CrossMab 还要对轻链-重链配对做约束编码;选 DuoBody 类臂交换则重点建 Fab-arm 交换产物。第 15 篇已给单格式的实现,本篇把它们变成配置里的可选项。
1.2 流水线 DAG 与组件回收
YAML 配置(本篇) ─哈希→ 每一步结果行 │ L0 格式决策 ──► L1 CH3 突变枚举(第 15 篇, 剪枝) │ 臂对序列(第 04 篇 ANARCI 编号) ──► L2 复合物组装建模(第 07 篇建模 + 第 05 篇准备: 二硫/糖基化, 铁律 9) │ L3 对接与重排: maestro→PIPER(BioLuminate, 第 09 篇) / moe→Pro:Pro Dock(第 09 篇) │ └─ 重排: MM-GBSA Residue Scanning(第 15 篇) / Glide 语境见第 10 篇 L4 三域打分: 界面分数 ‖ 聚集/表面(第 13/14 篇) ‖ 免疫原性(NetMHCIIpan-4.0 百分位 + TepiTool consensus) │ L5 联合排序: 分位投票 + 帕累托(Pareto)前沿 → top N 进 MD(第 11 篇) 复核 → 候选清单(第 08 篇入库)两条版本红线:MOE 侧 Pro:Pro Dock 自 2024.06 起对"含注释的抗体-抗原复合物"自动选抗体为配体、抗原为受体,且$CDR_H3自动注释——用旧版脚本喂 2024.06 或反过来,接口假设就对不上(铁律 2);Schrödinger 侧 2026-3 发行说明提示 PIPER 新增输出文件,解析代码按安装版内 Help 的文件说明写死版本。
1.3 联合排序:三域各有各的"一票否决"
免疫原性用每个候选在 HLA consensus 面板上预测肽的百分位排名分布(如预测到的强结合肽个数/千残基),来自 NetMHCIIpan-4.0 与 TepiTool 流水线;聚集与表面域沿用第 13/14 篇矩阵。合成规则:先各自域内取分位,再做帕累托前沿(任一候选在三个域上都不被他人全面压制,才入前沿),前沿内再按可调权重排序——同时任何一域触碰最差 1% 否决线直接出局。这正是铁律 6 的多域版:界面亲和力最好但聚集与免疫原性双差的候选,恰恰是临床阶段最贵的错误。
1.4 为什么必须配置驱动
同一份业务代码,昨天跑单抗、今天跑双抗、明天换后端;如果格式、枚举上限、对接引擎、权重散落在脚本常量里,"配置漂移"会在结果表里留下无法归因的差异。所以:YAML 承载全部决策参数(版本锁、并行度、阈值、权重、预算),文件本身算哈希写进每一行结果(铁律 5 的审计版);换后端只改docking.backend一行。
二、完整代码与逐行剖析
代码一:bispecific.yaml —— 整条流水线的唯一决策源
# bispecific.yaml —— 本文件的 SHA-256 记入结果表每一行(配置漂移即无所遁形)project:bsab-screen-2026-09versions:# 铁律 2:声明并锁定,不是注释而是运行前断言schrodinger:"2025-4"moe:"2024.06"# Pro:Pro Dock 抗体自动选择自本版本起可用pipeline_pkg:"antibody_pipeline==0.4.0"pairing:arms_csv:data/arms.csv# 列: pair_id,hc_vh,lc_vl,hc2_vh,lc2_vl(编号=Kabat,第 04 篇)formats:-name:KiH-IgG1# 重链异构二聚:knobs-into-holes 界面ch3_strategy:knobs-into-holeslight_chain:common-vl# 共用轻链:免掉轻链错配税单-name:CrossMab-KiHch3_strategy:knobs-into-holeslight_chain:domain-swap# CrossMab 结构域交换防轻链错配enumeration:mutation_table:data/ch3_mutations.csv# 列: chain,resnum,wt,mut——位点与残基号来自第 15 篇设计表max_combo_size:3# 单/双/三突变组合:指数爆炸的第一道闸max_combos:200# 枚举总预算:次道闸(边枚举边截断)docking:backend:maestro# maestro→PIPER(BioLuminate); moe→Pro:Pro Dock(第 09 篇两实现)rescoring:mm-gbsa-residue-scan# MM-GBSA Residue Scanning 重排(第 15 篇)surface:# 第 13/14 篇指标族metrics:[camsol,a3d,fvcsp_abs,hydro_patch]veto_quantile:0.99# 任何域触最差 1% 出局(铁律 6)immunogenicity:# 第 14 篇tools:[netmhciipan-4.0,tepi-tool-consensus]# 服务器现状实测 2026-09-04 在线(铁律 8)strong_bind_per_1k:3# 每千残基强结合肽数阈值:超线进否决名单ranking:method:pareto-then-vote# 先帕累托前沿,前沿内加权分位(权重须与实验组共识)weights:{interface:0.4,aggregation:0.35,immunogenicity:0.25}top_n_md:24# 复核名单容量:预算在 budget 里封顶budget:{gpu_hours:500,cpus:64}# 第 19 篇调度器据此限流注意versions是数据不是文档:驱动脚本启动时逐一断言实际环境,对不上就拒绝起跑。rescoring键写的是第 15 篇封装的注册名,具体面板参数以安装版内 Help 为准。
代码二:bsab_pipeline.py —— 配置驱动:枚举→组装→对接→重排
# bsab_pipeline.py —— 读 YAML 驱动双抗筛选主体(对接/表面对外只经 capability 派活)importhashlib,itertools,jsonfrompathlibimportPathimportpandasaspd,yamlfromantibody_pipeline.backendsimportget_backend# 第 16 篇:唯一容身商业细节的层defload_cfg(p="bispecific.yaml"):raw=Path(p).read_bytes()returnyaml.safe_load(raw.decode("utf-8")),hashlib.sha256(raw).hexdigest()[:12]# 返回配置哈希:后面写进每一行结果,供第 20 篇平台做审计链defassert_versions(cfg):env={"schrodinger":Path("SCHRODINGER_VER").read_text().strip()ifPath("SCHRODINGER_VER").exists()else"absent","moe":"absent"}# 实战中向平台查询(第 01 篇体检脚本)fork,wantincfg["versions"].items():# 铁律 2:版本不齐不开工ifkinenvandenv[k]!="absent"andwantnotinenv[k]:raiseRuntimeError(f"版本失配{k}: 需要{want}实际{env[k]}")defenumerate_ch3(muts_csv,max_size,max_combos):m=pd.read_csv(muts_csv)# 残基号沿用第 04 篇固定的 Kabat 编号(铁律 1)per_site={site:sorted(g["mut"])forsite,ginm.groupby(["chain","resnum"])}# 每位点的候选突变列表combos=[]forkinrange(1,max_size+1):# 1..3 点组合:规模 = Π(1+候选数) 的截断forsite_pickinitertools.combinations(sorted(per_site),k):formutsinitertools.product(*(per_site[s]forsinsite_pick)):combos.append({f"{c}{r}{w}"for(c,r),winzip(site_pick,muts)})iflen(combos)>=max_combos:# 边枚举边截断:绝不让组合集先爆炸再剪returncombosreturncombosdefjid_of(pair_id,combo,backend,cfg_sha):h=hashlib.sha256()h.update(json.dumps(sorted(combo)).encode())# 参与哈希的必须是"影响结果的一切"h.update(f"{pair_id}|{backend}|{cfg_sha}".encode())# 换后端/改配置 → 新哈希 → 不覆盖旧结果returnh.hexdigest()[:12]defrun():cfg,csha=load_cfg()assert_versions(cfg)be=get_backend(cfg["docking"]["backend"])# maestro / moe 在这里才分流(第 16 篇)ifnotbe.supports("dock"):raiseValueError(f"后端{be.name}不具备 dock 能力")# capability 护栏,早爆ledger=Path("ledger.tsv").open("a",encoding="utf-8")# 追加式台账:失败也落库(铁律 10)forfmtincfg["formats"]:for_,pinpd.read_csv(cfg["pairing"]["arms_csv"]).iterrows():forcomboinenumerate_ch3(cfg["enumeration"]["mutation_table"],cfg["enumeration"]["max_combo_size"],cfg["enumeration"]["max_combos"]):jid=jid_of(f"{fmt['name']}|{p['pair_id']}",combo,be.name,csha)work=Path("runs/bsab")/jidif(work/".done").exists():continue# 幂等续跑(第 17 篇漏斗范式)work.mkdir(parents=True,exist_ok=True)try:complex_mae=build_complex(p,combo,fmt,work)# 复用第 15 篇装配+第 05 篇准备dock_and_rescore(complex_mae,work,be,cfg)# 复用第 09 篇批量对接封装(work/".done").write_text(jid)ledger.write(f"{jid}\t{p['pair_id']}\t{fmt['name']}\tok\t\t{csha}\n")exceptExceptionase:# 单候选失败绝不断整批:分类记进台账ledger.write(f"{jid}\t{p['pair_id']}\t{fmt['name']}\tfailed\t"f"{str(e)[:160]}\t{csha}\n")ledger.close()if__name__=="__main__":run()build_complex与dock_and_rescore是包内函数:前者按格式的light_chain策略装配两对轻重链并交第 05 篇准备流程补二硫键与 Fc Asn297 糖型(铁律 9),后者把复合物交给第 09 篇的 PIPER/Pro:Pro Dock 批量封装再挂上重排。jid_of把后端名也哈希进去,意味着"换后端重跑"不会覆盖旧后端结果,两者可在结果库里并排对比——这正是练习 3 的伏笔。
代码三:joint_rank.py —— 三域联合排序(帕累托前沿 + 域内投票)
# joint_rank.py —— 界面分数 × 聚集 × 免疫原性:三域合成一个可辩护的排名importpandasaspd,yaml cfg=yaml.safe_load(open("bispecific.yaml",encoding="utf-8"))df=pd.read_csv("ledger.tsv",sep="\t")df=df[df.status=="ok"].set_index("jid")# 失败行不参与评选,但报告页仍要列出agg=pd.read_csv("surface_metrics_bsab.csv").set_index("jid")# 第 13/14 篇导出imm=pd.read_csv("immuno_metrics.csv").set_index("jid")# 第 14 篇:NetMHCIIpan-4.0/TepiToold=df.join(agg[["risk_agg"]]).join(imm[["strong_bind_per_1k","immuno_pct"]])dom=pd.DataFrame({"interface":1-d["docking_score"].rank(pct=True),# 对接/重排分越高→风险越低"aggregation":d["risk_agg"].rank(pct=True),# 已是"越大越差"的方向"immunogenicity":d["immuno_pct"].rank(pct=True),# 强结合肽百分位排名})veto=(dom>=cfg["surface"]["veto_quantile"]).any(axis=1)# 三域同一条最差 1% 否决线strong=d["strong_bind_per_1k"]>cfg["immunogenicity"]["strong_bind_per_1k"]elig=~(veto|strong)# 免疫原性另有绝对阈值线front=[]# 帕累托前沿:不被任何人在三域全面压制foriindom[elig].index:r=dom.loc[i]dominated=((dom.loc[elig][["interface","aggregation","immunogenicity"]].le(r)&dom.loc[elig].ne(r).any(axis=1)).any(axis=1))ifnotdominated:front.append(i)w=pd.Series(cfg["ranking"]["weights"])# 权重是团队共识,不是脚本私货ranked=(dom.loc[front]*w).sum(axis=1).sort_values()# 前沿内加权分位→最终次序out=pd.DataFrame({"jid":ranked.index,"final_rank":range(1,len(ranked)+1),"top_md":ranked.index[:cfg["ranking"]["top_n_md"]]})out.to_csv("candidates_bsab.csv",index=False)print(f"eligible={elig.sum()}pareto={len(front)}-> candidates_bsab.csv")先帕累托后加权的顺序不能反:帕累托先把"任何一域明显更差"的候选剔出竞争,权重只在无支配关系的头部小集合里起作用,把"权重拍脑袋"的伤害面积压到最小。免疫原性域额外加了一条绝对阈值(强结合肽数/千残基),因为它的分位排名在整体都偏低的优质池里区分度不足——阈值数值写进 YAML(strong_bind_per_1k: 3),修改即换哈希,历史结果自动隔离。在线工具提交结果一律先缓存落盘再进表,服务器可用性按实测日期说话(铁律 8)。
三、常见报错与排查
- 对接打分对格式无感。现象:KiH 与 CrossMab 两分支分数几乎同分布。根因:复合物装配时把异源二聚体当对称同源建模,CH3 界面根本没有突变进去。解法:L2 之后加"序列层断言"——突变集合与产物 FASTA 逐位点核对,不符者落库
fail_reason="assembly"。 - Pro:Pro Dock 不再自动选抗体为配体。根因:MOE ≥2024.06 的自动行为前提是"复合物含抗体注释",
$CDR_H3未注释(第 09 篇)或用了 2024.06 之前的脚本假设。解法:补注释步骤并把 MOE 版本写入配置断言。 - 解析 PIPER 输出偶发 KeyError。根因:Schrödinger Release 2026-3 起 PIPER 有新增输出文件,解析器按旧版单文件写死。解法:解析函数按
versions.schrodinger分支,缺字段落库而不是崩溃(铁律 10)。 - 免疫原性批量提交半途失联。现象:外呼 NetMHCIIpan-4.0/TepiTool 的后半批超时。根因:在线服务限速,且服务器状态不可假设长期可用(铁律 8,本文实测 2026-09-04)。解法:提交即缓存、断点续提交、必要时降级为"仅序列 motif 过滤 + 标记未评"。
- 同一候选在不同运行里名次跳动。根因:配置常量散落脚本、或权重在代码里被顺手改过。解法:全部参数收回 YAML,运行时比对
cfg_sha,不一致的批次不得进同一张排名表。
四、动手练习
- 枚举预算:把
max_combos从 200 降到 50、max_combo_size保持 3。判定标准:台账行数恰好 ≤ 50×臂对数×格式数,且枚举在位点组合展开阶段即停止(打点计数验证,不出现 50 倍内存峰值)。 - 后端等价性:同一份
arms.csv、backend分别设 maestro 与 moe 各跑一遍。判定标准:两次jid集合零交集(后端名参与哈希)、结果库中可按pair_id并排比较两域界面分数,Spearman 相关 > 0.6 为合理量级。 - 否决线守恒:构造一条聚集域触 0.99 否决线但界面分数全场第一的假数据。判定标准:不出现在
candidates_bsab.csv,但出现在报告页"否决名单"表中且注明触线域。
五、小结与下一篇预告
本篇把双抗的三张错配税单翻译成流水线分支,用 YAML 单源配置 + 配置哈希实现"可换后端、可审计、可续跑",并以"帕累托前沿 → 前沿内加权"的三域联合排序守住铁律 6。组件回收清单:编号(第 04 篇)、准备(第 05 篇)、建模(第 07 篇)、对接(第 09 篇)、重排(第 10/15 篇)、表面与聚集(第 13/14 篇)、封装(第 16 篇)、漏斗(第 17 篇)。两篇实战跑下来,瓶颈已经清晰暴露在地基上:调度、容错与吞吐。下一篇专讲性能、容错与规模化——作业阵列与并行进程怎么选、失败怎么分类重试、万级变体的机器时怎么估。
本篇认知问题回显(FAQ)
Q1:双抗格式 KiH、CrossMab、DuoBody 在筛选流水线里怎么选?
A:把格式写进 YAML 的 formats 段并映射到装配策略:KiH 走 knobs-into-holes 的 CH3 突变枚举,CrossMab 追加轻链结构域交换约束,臂交换格式重点建交换产物;格式决定枚举范围、复合物装配与界面评估对象,属流水线分支而非事后过滤。
Q2:双抗 CH3 界面突变怎么枚举与剪枝?
A:以链/残基号/野生型/突变型四列 CSV 输入,按位点分组做 1~3 点组合枚举,用 max_combo_size 和 max_combos 两道闸边枚举边截断,并排除 CDR 区与破坏互补电荷对的组合;残基号沿用 ANARCI 固定的 Kabat 编号。
Q3:双抗复合物建模与对接依赖哪些前文组件?
A:建模复用第 07 篇抗体同源建模与第 05 篇结构准备(二硫键与 Fc Asn297 糖基化必做),对接复用第 09 篇的两条批量路线——Schrödinger BioLuminate 的 PIPER 或 MOE 2024.06 的 Pro:Pro Dock,其后重排用 MM-GBSA Residue Scanning。
Q4:聚集倾向与免疫原性如何与界面分数合成统一排序?
A:三域各自转分位风险秩,任何域触最差 1% 否决线出局,免疫原性另设每千残基强结合肽数绝对阈值;存活者先取三域帕累托前沿,前沿内再按 YAML 声明的权重(如 0.4/0.35/0.25)加权排序。
Q5:双抗筛选流水线为什么用 YAML 配置驱动?
A:格式、枚举上限、对接后端、指标阈值、权重、预算全部外置于一份 YAML 并计算其 SHA-256 记入每行结果,换后端只改一行、配置漂移即被哈希揪出,满足版本锁定(铁律 2)与幂等审计(铁律 5)。