ClayFF力场应用于SiO2材料的小脚本-Gromacs
最近要在Gromacs的模拟中用到ClayFF力场,主要是用在界面相互作用的固相材料上面,在模拟中这部分材料的原子大部分都是会被固定的,只有表面部分的羟基氢会被设定为放开,从而保证表面的相互作用,在实际应用的过程中,为了将参数文件合理的分配给每个原子,我没能找到现成的方案,无奈之下选择自己写个简单的脚本来实现这个功能。
Clayff.py脚本全内容
以下是我最终写完的脚本的整体内容,真写出来才发现这个也不是那么容易的,我这个脚本的局限性较大,目前来看只能应用与Si,O,H三种元素,而且成键信息仅包含ho-oh的,不过calyff本来也没有其它的成键信息了,所以这里还好,还有个问题就是有关O原子的分类,ClayFF包含了多种氧原子,我这里仅分辨了oh和ob两种,更多的我也不知道怎么识别,不过我看了一下,不同种类的氧原子也仅仅是电荷的差异,这个在我后面写的重新设定电荷的函数中被优化了。以下为代码的内容:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 import redef read_nonbonded_file (file_name ): clayff_nonbonded = {} with open (file_name, "r" ) as file: lines = file.readlines() pattern = re.compile (r'^\s*(\S+)\s+(\d+)\s+([\d.]+)\s+([\d.-]+)\s+(\S+)\s+([\d.E+-]+)\s+([\d.E+-]+)' ) for line in lines: match = pattern.match (line) if match : name = match .group(1 ) at_num = int (match .group(2 )) mass = float (match .group(3 )) charge = float (match .group(4 )) ptype = match .group(5 ) sigma = float (match .group(6 )) epsilon = float (match .group(7 )) clayff_nonbonded[name] = [at_num, mass, charge, ptype, sigma, epsilon] return clayff_nonbondeddef read_pdb_file (file_name ): atoms = [] h_atoms= 0 with open (file_name, "r" ) as file: for line in file: if line.startswith(("ATOM" , "HETATM" )): index = int (line[6 :11 ].strip()) atom_name = line[12 :16 ].strip() residue_name = line[17 :20 ].strip() if not residue_name: residue_name = "MOL" chain_id = line[21 ].strip() residue_seq = int (line[22 :26 ].strip()) x = float (line[30 :38 ].strip()) y = float (line[38 :46 ].strip()) z = float (line[46 :54 ].strip()) if atom_name == "H" : h_atoms+=1 atoms.append({ "index" : index, "name" : atom_name, "residue" : residue_name, "chain" : chain_id, "residue_seq" : residue_seq, "coords" : (x, y, z) }) elif line.startswith("CONECT" ): indices = list (map (int , line.split()[1 :])) atom_index = indices[0 ] bonded_atoms = indices[1 :] atoms[atom_index - 1 ]["connect" ] = bonded_atoms print ("H原子的数量为:" + str (h_atoms)) return atomsdef set_atom_type (atoms ): h_indices = [] o_indices = [] for atom in atoms: if atom["name" ] == "H" : h_indices.append(atom["index" ]) elif atom["name" ] == "O" : o_indices.append(atom["index" ]) for atom in atoms: if atom["name" ] == "SI" or atom["name" ] == "Si" : atom["type" ] = "st" elif atom["name" ] == "H" : atom["type" ] = "ho" if "connect" in atom: if len (atom["connect" ]) > 1 : print (str (atom) + "===WARNING=== H原子存在两个以上键" ) elif atoms[atom["connect" ][0 ] - 1 ]["name" ] != "O" : print (str (atom) + "===WARNING=== H原子没有与O连接" ) else : print (str (atom) + "===WARNING=== H原子不存在任何键" ) elif atom["name" ] == "O" : if "connect" in atom: if len (atom["connect" ]) > 2 : print (str (atom) + "===WARNING=== O原子存在三个以上键" ) intersection = set (atom["connect" ]) & set (h_indices) if intersection: atom["type" ] = "oh" else : atom["type" ] = "ob" else : atom["type" ] = "ob" print (str (atom) + "===WARNING=== O原子不存在任何键" ) return atomsdef write_itp_file (file_name, atoms, bonds, moleculetype="Clay" ): with open (file_name, "w" ) as file: file.write("[ moleculetype ]\n" ) file.write("; name nrexcl\n" ) file.write(moleculetype+" 3\n\n" ) file.write("[ atoms ]\n" ) file.write("; Index type residue resname atom cgnr charge mass\n" ) total_charge = 0 for atom in atoms: index_n = atom["index" ] type_info = atom["type" ] residue = "1" resname = atom["residue" ] atom_name = atom["name" ] cgnr = "1" charge = atom["charge" ] total_charge += charge mass = atom["mass" ] file.write(f"{index_n:<8 } {type_info:<6 } {residue:<8 } {resname:<8 } {atom_name:<10 } {cgnr:<6 } {charge:<10 } {mass:<10 } \n" ) file.write("; Total charge " + str (total_charge) + "\n" ) if bonds: file.write("[ bonds ]\n" ) file.write("; i j funct length force.c.\n" ) for bond in bonds: i_value = bond["i" ] j_value = bond["j" ] func = bond["func" ] length = bond["length" ] force = bond["force" ] file.write(f"{i_value:<6 } {j_value:<6 } {func:<3 } {length:<6 } {force:<10 } \n" )def adjust_charge (atoms, clayff_nonbonded ): total_charge_without_ob = 0 ob_count = 0 for atom in atoms: if "type" not in atom: print (atom) if atom["type" ] != "ob" : charge = clayff_nonbonded[atom["type" ]][2 ] total_charge_without_ob += charge else : ob_count +=1 if ob_count != 0 : ob_adjust_charge = (0 - total_charge_without_ob) / ob_count ob_adjust_charge = round (ob_adjust_charge, 8 ) print ("将ob类型的氧原子电荷调整为:" + str (ob_adjust_charge)) else : ob_adjust_charge = 0.00 print ("不存在桥接氧,没有调整电荷" ) for atom in atoms: atom["mass" ] = clayff_nonbonded[atom["type" ]][1 ] if atom["type" ] != "ob" : atom["charge" ] = clayff_nonbonded[atom["type" ]][2 ] else : atom["charge" ] = ob_adjust_charge return atomsdef set_bonds (atoms ): bonds = [] for atom in atoms: if atom["type" ] == "ho" : connect = atom["connect" ] o_flag = 0 for connect_index in connect: if atoms[connect_index - 1 ]["type" ] == "oh" : o_flag += 1 if o_flag > 2 : print (str (atom)+"该H原子连接了两个以上的氧原子!该原子第二个连接没有设置bonds信息" ) break bonds.append({ "i" :atom["index" ], "j" :connect_index, "func" :1 , "length" :0.1 , "force" :463532.808 , }) print (str (len (bonds)) + "个-OH" ) return bondsdef main (): clayff_nonbonded = read_nonbonded_file("ffnonbonded.itp" ) atoms = read_pdb_file("Layer.pdb" ) atoms_type = set_atom_type(atoms) atoms_type_charge = adjust_charge(atoms_type, clayff_nonbonded) bonds = set_bonds(atoms_type_charge) write_itp_file("clay.itp" , atoms_type_charge, bonds)if __name__ == '__main__' : main()
该代码的使用就很简单,主要修改三个地方,在main()函数中,修改clayff的ffnonbonded.itp名字,然后修改pdb文件为你自己的文件,然后是修改你想要输出的itp文件命名,分别在220,222,229行,其中要注意的是这个pdb文件尽量别选择用maerial studio(MS)生成的pdb文件,不知道为何。MS生成的pdb文件有些成键会错误,需要在MS建模完成后输出mol2文件,然后再由Gview转称pdb,这样就不会出现问题。
此外有关ffnonbonded.itp的文件我是源自Github上这个人的数据:
https://github.com/thomasunderwood/ClayFF/blob/master/ClayFF.ff/ffnonbonded.itp
他应该是参考的之前一篇文章报道,讲ClayFF的参数转化为了Gromacs可以应用的数据单位。
代码详解
为了便于理解以及便于读者在我这个脚本上面修改以用于自己的体系,这里对整体代码按照函数的顺序进行一个简单的讲解
read_nonbonded_file(file_name)
这个函数的意义就是读取ClayFF的ffnonbonded.itp文件,并将中的数据应用re这个库来按照原子类型存储其相关信息。这里返回的是一个字典clayff_nonbonded,字典的key就是原子类型,每个key对应的是一个列表,列表中有6个数据,分别为at.num,mass,charge,ptype,sigma,epsilon。这样后续可以用字典加索引的形式方便快捷的提取每个原子类型的各项数据。
read_pdb_file(file_name)
这个函数用于读取pdb文件,将其中的原子信息按顺序存入一个atoms列表中,这样存储的好处就是列表的索引号+1就是每个原子的索引号(因为python列表的索引号从0开始,而pdb原子的索引号从1开始)。然后原子信息则是字典的形式保存的,如下:
1 2 3 4 5 6 7 8 atoms.append({ "index" : index, "name" : atom_name, "residue" : residue_name, "chain" : chain_id, "residue_seq" : residue_seq, "coords" : (x, y, z) })
也就是说在atoms这个列表里面会存放原子数量个的字典,每个字典就保存有该原子索引,名称,残基名,坐标等各项信息。
此外这个函数还会继续读取bonds信息,这样写的原因是pdb文件讲成键信息放在了各项原子信息之后,存储的格式为{CONNECT,成键原子索引号,被成键原子索引号1,被成键原子索引号2…},所以根据成键原子索引号去atoms列表中找到对应的原子,然后讲与它连接的被成键原子索引号以列表的形式存入该原子的字典中。
set_atom_type(atoms)
这个函数就是用来人为的设定原子类型了,这儿我觉得应该是要注意根据自己体系进行调整的,我这里仅对Si,H,O的原子进行了设定,其中Si和H都是直接按照原子类型来确定的,而对于O原子则多加了一个如果它跟H原子连接的话,则设定为oh,否则就设定为ob。
然后我还加了一些对成键情况的判定,即对一些异常的成键情况进行提醒,比如氢原子成键数量大于等于2了,或者氧原子成键数量为0之类的,这也是为啥我能发现MS的pdb文件格式不对。
adjust_charge(atoms, clayff_nonbonded)
该函数即用于调整电荷,这个是我需要自己写这个脚本的另外一个主要原因了,因为当你建模之后会发现,该体系的电荷如果严格根据ClayFF里面的电荷信息来的话是不为0的,所以为了实现一个中性的体系我决定对整个体系中所有的桥接氧(ob)的电荷进行调整,所以我写了这个函数,用于重新计算电荷。
该函数的原理就是先读取所有原子的type,然后根据clayff_nonbonded中各个原子类型的电荷求出所有非ob原子类型的总电荷,然后用该电荷除以ob原子的数量,再为所有ob原子重新设置电荷。
set_bonds(atoms)
这个函数是为了写入[ bonds ]后的数据所准备的,主要是为了根据ClayFF设置一些基本的参数,它需要读取之前的atoms里面的每个atom的"connect"的列表,然后如果是H原子的话就会将其数据转化成一个新的,保存有bond相关信息的字典,存入刀bonds这个列表中,最后返回的就是bonds这个列表。
write_itp_file(file_name, atoms, bonds, moleculetype=“Clay”)
这个函数就是为了输出最终的itp文件,需要将前面处理得到的保存有各项数据的atoms列表,bonds泪飙传入,然后按照itp文件的格式输出,注意,这里输出是以[ moleculetype ]开始的,前面的[ atomstype ]这里并没有包含,相关数据可以直接#include ffnonbonded.itp即可。