pub fn run_reactants(
reaction: &Reaction,
reactants: &[(MolBuilder, MolProps)],
max_products: usize,
atom_mapping: bool,
) -> Vec<Outcome>Expand description
对一组反应物跑反应,返回所有结果组。
每个反应物模板配一个互不相同的输入分子;分子数与模板数不等时返回空
(那一档是 run_on_substrate 的形状:多个片段落在同一个分子上)。
每个匹配组合产出一组结果 —— 底物上有几处能反应就有几组,内容可能重复 (对称位点)。去重是调用方的事:要按什么去重取决于用途,规范 SMILES 多重集只是其中一种。
§递入顺序不影响出不出产物
位置不是化学。 谁先谁后是调用方敲键盘的顺序,不是分子的性质,所以 它不该决定这条反应跑不跑得起来。
实现上仍然先试恒等分配(第 i 个模板配第 i 个分子)—— 顺序本来就对得上 时开销与只试这一种完全相同;恒等分配一个产物都给不出,才去找别的一一对应, 按字典序取第一个能出产物的。所以:
- 顺序对得上:行为与耗时都不变
- 顺序不对:照样出产物,而不是交白卷
- 返回空只剩一个意思:这批分子上没有反应位点
这一条是量出来的,不是想出来的。USPTO-50k 正向语料里,按记录自带的分子 顺序直接调用,约 689 条交白卷;抽样 4000 条逐条核过,其中 59 条全部 只是顺序对不上 —— 换个顺序就出产物,没有一条是真的匹配不上。 而调用方拿到的是同一个空列表,分不出这两件事。
回退那条路要多算最多 n²−n 次子结构搜索(n 是反应物模板数,现实中 1–3), 而且只在本来就要返回空的时候才走 —— 拿它换的是一个静默的错答案。
开了 atom_mapping 时,哪个分子担了哪个角色可以从映射号读回来:
Outcome::reactants 里的副本按输入顺序排,号是按底物原子发的。
§哪些原子会进产物:只有从保留下来的原子走得到的
产物 = 模板产物侧建出来的原子,加上从它们出发在底物里能走到的部分。走不到 的原子不进产物,不报错。这条规则有两个看得见的后果,都是刻意的:
一、模板删掉一个原子时,只挂在它身上的东西跟着走。 叔丁酯水解的模板写
C-C-[O:1]-[C:2]=[O:3],删掉的 C-C 是叔丁基的一个甲基加季碳;季碳上另外
两个甲基没有别的路连回保留部分,于是一并消失 —— 一次少掉 4 个重原子而不是 2 个。
真实反应语料里这一档数以千计,不是边角情形。
二、完全不连通的旁观组分原样交回来,不丢。 底物写成 内酰胺.HCl 或
[Na+].[O-]CC(=O)OCC[O-].[K+] 时,反离子与任何匹配到的原子都不连通,遍历
走不到它们。但走不到不等于该丢 —— 丢了产物的重原子数就少于底物,是引擎
自己在破坏质量守恒,而且不报错。逆合成正是把模板作用到任意分子上,盐是常态。
所以这些组分会被原样搬进产物图,按连通分量切开之后各自成为一个产物分子。 引擎不替调用方决定归属 —— “这个反离子该跟哪一半走“没有普遍答案,模板里 也没有这条信息;交回去,由调用方定。
与上一条的分界是“这个组分里有没有原子被模板匹配到“:有,留下还是删掉 是模板的表态;没有,模板压根没提到它。
§atom_mapping
开启后,Outcome::reactants 填上带映射号的反应物副本,产物侧的对应原子
打上同一个号 —— 两侧合起来就是一条完整的原子映射反应。关闭时两侧都不带号,
reactants 留空。
发号的规则:
- 只给两侧都在的原子发。 被反应删掉的、产物侧新建的都不发 —— 一个在 另一侧找不到的号,读的人只能理解成“这个原子凭空消失/出现“,而那正是号 要表达的反面。
- 号是新发的,不沿用模板里的。 模板的
[C:1]连的是两个模板而非底物, 而且模板只覆盖分子的一小块,搬运过来的部分本来就没有号可沿用。反应物 副本上原有的号会先清掉,免得留下在产物侧找不到的悬空号。 - 顺序:按
(第几个反应物, 原子下标)升序,从 1 连续发。同一份输入 永远得到同一套号;换一种原子编号写同一个分子,号会跟着变 —— 映射号本就 是贴在某一种画法上的标签,不是分子的不变量。 - 一个号在同一侧只出现一次。产物是按连通分量切出来的,每个底物原子只进 一个产物,所以这一条自然成立。
§写出带号的产物之前要先净化
与“产物不做净化“那一条配套:隐式氢数是派生量,图改完就过期了。模板删掉 一个邻居之后,那个原子该补几个氢要重算,而重算在净化里。
平时看不出来 —— 裸写形式(N、C)把氢数交给读的人按价规则去推,推出来
的正是重算后的值。可带映射号的原子必须写进方括号,而方括号里的氢数是
显式的,写出去的就是那个过期的缓存值。于是同一个产物,开不开映射号会写出
氢数不同的两串。
所以带号写出之前先 sanitize。实测语料上:不净化
直接写,31 万个 outcome 里有 176 个两串对不上;先净化再写,0 个。
§调用前要先感知双键顺反
反应物应当先跑
perceive_bond_stereo ——
净化那 12 步里没有它(感知要用对称等价类,那在净化的上一层,调不到)。
漏了这一步不会报错,会静默丢几何:方向键(/ \)依附在某根单键上,
反应把那根键删掉,几何就跟着没了 —— 哪怕双键本身根本没被碰过。产物照样
合法、原子数照样对,只有顺反悄悄少了。感知一次之后信息记在双键自己身上,
只要参照原子还在就活得下来。
omgkit_chem::sanitize(&mut mol).unwrap();
omgkit_io::stereo::perceive_bond_stereo(&mut mol); // ← 别漏
let props = MolProps::compute(&mol);
let out = run_reactants(rxn, &[(mol, props)], 0, false);debug 构建下漏了会被 debug_assert 当场拦住;release 下不做这个检查。