查阅起点来自《A Deep Learning Approach to Antibiotic Discovery》
摩根指纹一般指 Extended-connectivity fingerprints (ECFPs) ,可以调用 rdkit 得到,这篇文章描述实现的具体细节,主要参考
Extended-Connectivity Fingerprints。输入算法参数的是一个哈希函数(将列表映射为一个32bit的整数),待编码的分子 smiles,半径 r,最终编码长度 n_bits,下面是算法的大致描述,每个阶段的描述之后是python的实现,代码效率可能比较低(特别是对化学键的处理,暂时想不到好的办法,以后有机会改进),主要是辅助理解。原文细节描述很多,如果不懂下面的描述强烈推荐去读原文。
下面代码注释中提到的图都是来自原文中提到的示例分子,也即下面图片中的分子
from rdkit import Chem
from rdkit.Chem import AllChem
from collections import defaultdict
from rdkit.Chem.Draw import IPythonConsole
IPythonConsole.ipython_useSVG=True
smiles="CCCC(=O)N"
mol=Chem.MolFromSmiles(smiles)
mol
1.初始化
- 对每个原子初始化一个32bit的整数标识符,对于ECFP来说,使用的方法是 Daylight atomic invariants rule
- Daylight atomic invariants rule 将每个原子映射为一个32bit的整数标识符,具体是先得到每个原子的描述列表,包括重原子邻居个数,化学价与邻接氢原子的差值,化学价,原子序数,原子质量,原子电荷数,邻接氢原子数,作者在这里另外加了一个元素是原子是否在环上。用算法参数中的哈希函数将这个描述列表变成一个32bit的整数,最后返回这个整数作为标识符
def myHash(array):
key=2**32
tmp=hash(tuple(array))%key
return tmp-2**31
def myHash(array):
key=2**32
tmp=hash(tuple(array))%key
return tmp-2**31
def getECFPInit(mol,Hash=myHash):
mol = Chem.AddHs(mol)
atom2vec=defaultdict(list)
atoms = mol.GetAtoms()
for i in range(len(atoms)):
atom2vec[atoms[i]].append(sum(atom.GetAtomicNum()==1 for atom in atoms[i].GetNeighbors()))
atom2vec={key:value for key,value in atom2vec.items() if key.GetAtomicNum()!=1}
for atom in atom2vec.keys():
atom2vec[atom].append(atom.GetDegree()-atom2vec[atom][0])
atom2vec[atom].append(atom.GetTotalValence()-atom2vec[atom][0])
atom2vec[atom].append(atom.GetAtomicNum())
atom2vec[atom].append(atom.GetMass())
atom2vec[atom].append(atom.GetFormalCharge())
atom2vec[atom].append(atom2vec[atom][0])
atom2vec[atom].append(int(atom.IsInRing()))
atomIdx2int={key.GetIdx():Hash(value[1:]) for key,value in atom2vec.items()}
return atomIdx2int
2.迭代
- 对每个原子初始化一个化学键的集合,这个集合用于每次迭代后的去重,如果两个化学键集合完全一致,就删除本轮得到的其中一个标识符
- 每个分子对应一个指纹列表,它收集每轮迭代后所有分子的标识符,初始即为第一步初始化得到的所有标识符去重后的结构
- 对每个原子而言不断增大半径变成更大的结构片段,收集局部结构信息,如下图。收集局部信息的具体方式是:对当前的结构扩大一步键的范围,得到(键标识,新连接的原子标识符),其中键标识是单键,双键,三键,芳香键分别为1,2,3,4。将得到的元组列表利用哈希函数映射成32bit的整数,之后去重
import copy
def GetMorganFingerprint(mol,r=2,Hash=myHash,useFeatures=False):
bondType2int={
rdkit.Chem.rdchem.BondType.SINGLE:1,
rdkit.Chem.rdchem.BondType.DOUBLE:2,
rdkit.Chem.rdchem.BondType.TRIPLE:3,
rdkit.Chem.rdchem.BondType.AROMATIC:4,
}
if useFeatures:
atomIdx2int=getFCFPInit(mol)
else:
atomIdx2int=getECFPInit(mol)
atomIdx2substruAndBonds=defaultdict(list)
bondNum=mol.GetNumBonds()
for atomIdx in atomIdx2int.keys():
atomIdx2substruAndBonds[atomIdx].append([])
atomIdx2substruAndBonds[atomIdx][0].append(atomIdx)
atomIdx2substruAndBonds[atomIdx].append([])
for i in range(bondNum):
bond=mol.GetBondWithIdx(i)
if (bond.GetBeginAtomIdx()==atomIdx or bond.GetEndAtomIdx()==atomIdx):
atomIdx2substruAndBonds[atomIdx][1].append(bond.GetIdx())
"""
for key,value in atomIdx2substruAndBonds.items():
print(key,value) #可以print出来看一下,key是原子索引,value[0]是子结构包含的原子的索引,value[1]是子结构包含的键索引
"""
fpList=[]
diffSubstru=defaultdict(list)
tmp={atomIdx:tuple([tuple([mol.GetAtomWithIdx(thisAtomidx).GetAtomicNum() for thisAtomidx in substruAndBonds[0]]),
tuple([bondType2int[mol.GetBondWithIdx(thisbondidx).GetBondType()] for thisbondidx in substruAndBonds[1]]
)]) for atomIdx,substruAndBonds in atomIdx2substruAndBonds.items()}
for atomIdx,subStructure in tmp.items():
diffSubstru[subStructure].append(atomIdx)
"""
for key,value in diffSubstru.items():
print(key,value) #可以print出来看一下,key是子结构(前一个元素是原子序数,后一个元素是键的映射),value这个子结构的原子中心,结合下面的分子图理解
#可以看到有 ((6,), (1, 1)) [1,2]
#解释是结构为【((6,), (1, 1))】的有两个原子,这个结构的解释是6号原子即C,和两个单键(单键映射是1),即下面的图中C2(序号是1)和C3(序号是2)两个原子都是连接了两个单键,它们是同样的结构
"""
for subStructure,atomIdxList in diffSubstru.items():
if len(atomIdxList)>1:
minIdenti=atomIdx2int[atomIdxList[0]]
for atomIdx in atomIdxList:
if atomIdx2int[atomIdx]<minIdenti: minIdenti=atomIdx2int[atomIdx]
fpList.append(minIdenti)
else: fpList.append(atomIdx2int[atomIdxList[0]])
diffStructureList=[]
for substructure in atomIdx2substruAndBonds.values(): diffStructureList.append(copy.deepcopy(substructure))
for iteration in range(1,r+1):
for atomIdx,subStructure in atomIdx2substruAndBonds.items():
bondsIdx=subStructure[1]
AatomList=[]
for atomIdx in subStructure[0]:AatomList.append(atomIdx)
for bondIdx in bondsIdx:
bond=mol.GetBondWithIdx(bondIdx)
subStructure[0].append(bond.GetBeginAtom().GetIdx())
subStructure[0].append(bond.GetEndAtom().GetIdx())
subStructure[0]=list(set(subStructure[0]))
AatomList=set(subStructure[0])-set(AatomList)
"""
print(AatomList) #每个中心原子向键的方向延伸,新增了'A'原子,与原文一致
{1}
{0, 2}
{1, 3}
{2, 4, 5}
{3} #N和O都是新增了原文中的4号碳原子,也就是这里的3号原子
{3}
#到这里已经将原文中的"A"原子扩展到了前一步子结构内部,下一步是扩展A原子连接的键
#下面的print对应原文 Figure 7 中初始化之后的几轮,第一轮(New features after first iteration)可以对比一下,与原文一致
#到这里可以看到子结构中的原子数量达到了2,3,3,4,2,2,不算A为原子的话(雀实不应该算)原文与这里得到的结果一样
#还没有扩展延伸键,因此键的数量还是1,2,2,3,1,1。其中的两个2是有子结构,原文图中只画了去重后的一个2
print(subStructure)
[[0, 1], [0]]
[[0, 1, 2], [0, 1]]
[[1, 2, 3], [1, 2]]
[[2, 3, 4, 5], [2, 3, 4]]
[[3, 4], [3]]
[[3, 5], [4]]
"""
oldBondList=[]
for bondIdx in subStructure[1]: oldBondList.append(bondIdx)
for i in range(bondNum):
bond=mol.GetBondWithIdx(i)
beginAtomIdx=bond.GetBeginAtom().GetIdx()
endAtomIdx=bond.GetEndAtom().GetIdx()
if beginAtomIdx in AatomList or endAtomIdx in AatomList:
subStructure[1].append(bond.GetIdx())
subStructure[1]=list(set(subStructure[1]))
"""
#此时原子和键的延伸都已经完成,与原文中结果一致,可以对比原子和键的数量
print(subStructure[0])
print(subStructure[1])
print('---------------下一个子结构-----------------')
print('-----------------------下一轮扩展子结构--------------------------------')
[0, 1]
[0, 1]
---------------下一个子结构-----------------
[0, 1, 2]
[0, 1, 2]
---------------下一个子结构-----------------
[1, 2, 3]
[0, 1, 2, 3, 4]
---------------下一个子结构-----------------
[2, 3, 4, 5]
[1, 2, 3, 4]
---------------下一个子结构-----------------
[3, 4]
[2, 3, 4]
---------------下一个子结构-----------------
[3, 5]
[2, 3, 4]
---------------下一个子结构-----------------
-----------------------下一轮扩展子结构--------------------------------
[0, 1, 2]
[0, 1, 2]
---------------下一个子结构-----------------
"""
newBondIdxList=list(set(subStructure[1])-set(oldBondList))
dataList=[]
for atomIdx in AatomList:
second=atomIdx2int[atomIdx]
targetBond=None
for bondIdx in subStructure[1]:
bond=mol.GetBondWithIdx(bondIdx)
if ((bond.GetBeginAtom().GetIdx()==atomIdx and bond.GetIdx() not in newBondIdxList) or (bond.GetEndAtom().GetIdx()==atomIdx and bond.GetIdx() not in newBondIdxList)):
targetBond=bond
break
first=bondType2int[targetBond.GetBondType()]
dataList.append((first,second))
newIdenti=Hash(dataList)
atomIdx2int[atomIdx]=newIdenti
if subStructure not in diffStructureList:
fpList.append(newIdenti)
diffStructureList.append(copy.deepcopy(subStructure))
fpList=list(set(fpList))
return fpList
3.函数接口
def GetMorganFingerprintAsBitVect(mol,nBits,r=2,Hash=myHash):
fpList=GetMorganFingerprint(mol,r)
res=set()
for identi in fpList:
bit = identi % nBits
res.add(bit)
return res
下面是与rdkit的对比
mol=Chem.MolFromSmiles("CCCC(=O)N")
nbits=2048
for r in range(5):
print("rdkit's:",len(AllChem.GetMorganFingerprint(mol,r).GetNonzeroElements().keys()),len(AllChem.GetMorganFingerprintAsBitVect(mol, r, nBits=nbits).GetOnBits()))
print("my:",len(GetMorganFingerprint(mol,r)),len(GetMorganFingerprintAsBitVect(mol,nbits,r)))
"""
rdkit's: 5 5
my: 5 5
rdkit's: 11 11
my: 11 11
rdkit's: 14 14
my: 14 14
rdkit's: 14 14
my: 14 14
rdkit's: 14 14
my: 14 14
"""
对示例分子而言,原文,rdkit和这里的实现得到的结果一致
mol=Chem.MolFromSmiles('CC(C)Oc1ccc(-c2nc(-c3cccc4c3CC[C@H]4NCCC(=O)O)no2)cc1C#N')
nbits=2048
for r in range(10):
print("rdkit's:",len(AllChem.GetMorganFingerprint(mol,r).GetNonzeroElements().keys()),len(AllChem.GetMorganFingerprintAsBitVect(mol, r, nBits=nbits).GetOnBits()))
print("my:",len(GetMorganFingerprint(mol,r)),len(GetMorganFingerprintAsBitVect(mol,nbits,r)))
"""
rdkit's: 16 15
my: 14 14
rdkit's: 44 43
my: 46 46
rdkit's: 71 68
my: 73 73
rdkit's: 95 91
my: 97 96
rdkit's: 114 110
my: 115 114
rdkit's: 129 125
my: 130 129
rdkit's: 141 137
my: 142 140
rdkit's: 150 146
my: 151 149
rdkit's: 156 152
my: 157 155
rdkit's: 159 155
my: 160 157
"""
其他分子得到的结果可能略有不同,可能是某些细节实现方法有误或与rdkit源码实现有差别。 FCFP与ECFP的区别仅仅在于得到 atomIdx2int 的区别,即初始化原子信息,FCFP的初始化是:
- 该原子是否是氢键供体
- 该原子是否是氢键受体
- 该原子是否负离子化
- 该原子是否正离子化
- 该原子是否是芳香性原子
- 该原子是否是卤素原子
得到 6 个元素为0或1的列表后,通过哈希函数将原子映射为标识符就可以进行迭代,剩下的处理与ECFP完全一致
实现略复杂,遇到了很多坑,可能还有些细节问题,看不懂的话强烈推荐看原文 Extended-Connectivity Fingerprints
|