4. 去除盐和溶剂分子(Desalting)——给分子“卸妆”
嗯,聊到这一步,我想大家应该已经习惯了 SMILES 里那些“不干净”的东西。说实话,从公开数据库(比如 ChEMBL、PubChem)或者供应商那边拿到的分子结构,很少是“纯”的。它们常常带着盐、溶剂分子、结晶水。这就像你收到一份测序样本,结果里面混着 buffer 和 stabilizer——直接上机跑,结果肯定是一团糟。
为什么要专门讲去盐?因为我吃过这个亏。有一次我跑一个基于指纹的相似性搜索,结果跑出来一堆乱七八糟的分子。查了半天,发现是 SMILES 里带了 NaCl,指纹计算全被那个“Na”带偏了。说白了,盐和溶剂就是噪音。我们必须给分子“卸妆”,把真正的核心结构剥离出来。
核心原则:去盐不是删除所有带电基团。我们需要保留共价键连接的阳离子(如季铵盐),只去掉那些游离的、作为单独组分的离子对或溶剂分子。
4.1 为什么要去盐?——这不是洁癖,是原则
你想想看,如果你要训练一个分子性质预测模型,输入是 CCCO.CC(=O)O(丙醇+醋酸),模型会学到什么?它会学到“这个分子有-OH、有-O-、有-COOH”,但这其实是两个分子。真实活性是基于哪个成分的?不知道。这就引入了不可控的歧义。
去盐的核心价值有三个:
- 保证结构完整性:只保留药效团,去掉干扰物。
- 提升数据一致性:同样一个药物分子,可能有盐酸盐、硫酸盐、磷酸盐形式。去掉后统一成游离碱或游离酸,方便对比。
- 避免计算误差:盐离子会影响 logP、MW 等描述符的计算,也会让分子图变得复杂无比。
4.2 检测盐和溶剂的“定位”方法
我个人习惯在做任何预处理之前,先跑一遍“成分检测”。SMILES 支持通过 .(点号)连接多个组分。如果 SMILES 里包含两个点号连接的片段,那大概率就有盐或溶剂。
# 示例:含盐的 SMILES
Cn1c(=O)c2c(ncn2C)n(C)c1=O.[Na] # 咖啡因的钠盐?不对,实际上是咖啡因 + Na⁺
O=C(O)c1ccccc1.NC(CO)(CO)CO # 苯甲酸 + 三羟甲基氨基甲烷(Tris buffer)
CC(C)CC.CCCCCC # 异戊烷 + 己烷(纯溶剂混合物)
注意,点号不代表任何化学键,它只是表示“这些分子在同一个体系里共存”。因此,我们可以用正则表达式或者 RDKit 的 SanitizeMol 来拆解这些片段。
小技巧:你可以用 RDKit 的 MolFromSmiles 解析后,调用 rdkit.Chem.rdMolDescriptors.CalcNumRotatableBonds 之类的方法,但如果 SMILES 里有游离盐,直接解析会报错(比如 [Na])。需要先做规范化。
4.3 去盐的常用策略
去盐不是无脑删。我总结了三步策略:
- 拆解组分:用
.切分 SMILES,得到组分列表。 - 识别盐/溶剂:根据预定义规则(小分子离子、常见溶剂、金属离子)标记每个组分。
- 保留主体:通常保留最重的那个有机片段,或者保留所有非盐非溶剂的片段。
为什么强调“通常”?因为有时候药物分子本身就是电解质(如季铵盐),这时候保留阳离子部分反而会丢失结构。我个人习惯用“分子量 + 元素组成 + 官能团”综合判断。
| 类型 | 常见例子 | 处理方法 |
|---|---|---|
| 无机盐 | NaCl, KCl, MgSO₄ | 直接移除 |
| 有机酸盐 | CH₃COO⁻, HCOO⁻ | 根据 pH/电荷判断是否移除 |
| 质子化溶剂 | H₂O, CH₃OH, DMSO | 一般移除(除非是结晶溶剂) |
| 目标分子本身就是盐 | 四甲基铵氯化物 | 保留阳离子部分,删除反离子 |
4.4 实战:用 RDKit 和自定义规则去盐
好,我们来写点代码。我最常用的方法是用 RDKit 的 SaltRemover 和 GetMolFrags。
from rdkit import Chem
from rdkit.Chem import SaltRemover
def desalt_smiles(smiles, remove_defn_file=None):
"""
去盐函数:移除常见无机盐和溶剂
:param smiles: 输入的 SMILES 字符串
:param remove_defn_file: 自定义盐/溶剂定义的 SMARTS 文件路径
:return: 去盐后的 SMILES,如果只剩盐则返回 None
"""
mol = Chem.MolFromSmiles(smiles)
if mol is None:
return None, 'Invalid SMILES'
# 使用 RDKit 内置的 SaltRemover
remover = SaltRemover.SaltRemover(defnData=remove_defn_file)
frags = remover.StripMol(mol, dontRemoveEverything=True)
# StripMol 会返回一个包含所有剩余片段的 Mol 对象
clean_smi = Chem.MolToSmiles(frags, isomericSmiles=True)
return clean_smi, 'Desalted'
# 使用默认的盐定义
print(desalt_smiles('CC(=O)O.[Na]')) # 应该输出 CC(=O)O
print(desalt_smiles('CCO.CCOC')) # 乙醇+乙醚,两个都是溶剂,怎么办?
注意上面第二个例子:CCO.CCOC(乙醇+乙醚),它们都是溶剂。RDKit 默认不会把它们当作“盐”移除。这时候需要自定义规则。
4.5 构建自定义盐/溶剂字典
我曾经历过一次痛苦:处理一批从 PubChem 下载的分子,里面含有大量的 H₂O、CH₃OH、DMSO。RDKit 的默认盐库不覆盖这些。后来我建了一个自定义 SMARTS 文件。
# 自定义溶剂 / 离子定义(保存为 custom_salts.smarts)
# 格式:SMARTS pattern, 类型注释
[OH2],water
[CH3]O,methanol
CS(=O)C,DMSO
[Na+];[Cl-],salt
[K+];[Cl-],salt
[NH4+];[Cl-],salt
然后用这个文件初始化 SaltRemover:
def desalt_custom(smiles):
remover = SaltRemover.SaltRemover(defnData='custom_salts.smarts')
mol = Chem.MolFromSmiles(smiles)
if mol:
clean = remover.StripMol(mol, dontRemoveEverything=True)
return Chem.MolToSmiles(clean)
return smiles
进阶技巧:对于复杂体系,可以先用 Chem.GetMolFrags 获取所有组分,然后用分子量阈值(比如 < 50)筛选出潜在的盐/溶剂。我常用这个逻辑来捕获那些没有被 SMARTS 覆盖的偶然发现。
from rdkit.Chem import Fragments
def desalt_by_frags(smiles, max_mw=80.0):
mol = Chem.MolFromSmiles(smiles)
if mol is None:
return None
frags = Chem.GetMolFrags(mol, asMols=True)
kept = []
for f in frags:
mw = Descriptors.MolWt(f)
# 如果分子量大于阈值,或者包含氨基酸残基,保留
if mw > max_mw or has_organic_core(f):
kept.append(f)
if not kept:
return None
# 合并保留的片段,通常只有一个主体
combined = Chem.CombineMols(*kept) if len(kept) > 1 else kept[0]
return Chem.MolToSmiles(combined)
4.6 避坑指南
我曾经在一个项目中,直接把所有含金属的片段都删了。结果发现有一类分子是金属有机配合物(比如钌催化剂),它们的中心金属是活性必需部分。所以,去盐不能一刀切。
- 不要删除共价结合的金属:比如
c1ccc([Ru]c2ccccc2)cc1,这个 Ru 是配位在环上的,不是游离盐。 - 注意异构体标记:去盐后,如果主体结构有手性中心,SMILES 末尾的
@或@@不能丢掉。 - 晶体水 vs 结晶溶剂:晶体水(
.[H]O[H])有时候是维持晶型必需的,但在计算模型里通常是噪音。除非你研究晶体结构预测,否则删了没问题。 - 检查点号数量:一个 SMILES 里点号过多(比如超过 5 个),很可能是聚合反应混合物,不适合用简单去盐处理。
我的经验:我一般会在去盐之后输出一份报告,列出被移除的片段类型和 SMILES,方便回头校对。如果发现重要成分被误删,可以及时调整规则。
4.7 整体流程可视化
下面这张图总结了我平时处理 SMILES 去盐的完整逻辑。你看,从输入到输出,每一步都在做“筛子”。
这个流程看着长,其实写成一个函数也就十几行。关键就在于那个“匹配盐/溶剂字典”的判断节点。字典的好坏,直接决定了去盐的精度。
4.8 总结一些实用建议
去盐这件事,说难不难,说容易也容易踩坑。我最后再说几点:
- 先跑一遍统计:看看你的数据集里有多少 SMILES 包含点号。如果超过 30%,去盐就是绕不过的预处理步骤。
- 不要迷信默认字典:RDKit 内置的盐库覆盖了常见的无机离子,但溶剂和水需要自己补充。我建议把你自己数据集里出现的、不想要的片段手动加进去。
- 保留日志:每次去盐后,对比一下去盐前后的分子数。如果损失率超过 5%,可能需要检查是否误删了有效分子。
最后的一个坑:有些数据库会在 SMILES 末尾用 | 或 / 标记盐信息(比如 PubChem 的 CC1=C(C=C(C=C1)Cl)C(=O)O.Cl)。我建议在去盐之前先统一做一次 SMILES 标准化(比如统一成 . 连接),否则正则规则会漏掉。