Skip to content

Commit 27e6f14

Browse files
authored
validate and repair for ligand (#41)
* fix: scan bug * version 0.1.5 * fix: add validate ligand tool * fix: add validate ligand tool * feat: add ligand prepare * fix: add forcefield check * feat: add a new tool unigbsa-validate * fix: add protprep for lauching * version 0.1.6
1 parent 9e91b64 commit 27e6f14

6 files changed

Lines changed: 219 additions & 12 deletions

File tree

launching/app.py

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,22 @@ class GBSAModel(
179179
...
180180

181181

182+
def prep_protein(pdbfile):
183+
outfile = pdbfile[:-4] + '_prep.pdb'
184+
args = [
185+
'-i', pdbfile,
186+
'-o', outfile,
187+
'-mode', 'gromacs',
188+
]
189+
print(args)
190+
argstr = ' '.join(args)
191+
cmd = '/bin/bash -c "source ~/.bashrc; conda activate protprep; unisp-prot %s"'%argstr
192+
RC = os.system(cmd)
193+
if RC != 0:
194+
return -2
195+
return outfile
196+
197+
182198
def gbsa_runner(opts: GBSAModel) -> int:
183199
status = 0
184200
try:
@@ -217,6 +233,9 @@ def gbsa_runner(opts: GBSAModel) -> int:
217233
}
218234
}
219235
input_protein = opts.input_protein.get_path()
236+
preped_protein = prep_protein(input_protein)
237+
if preped_protein == -2:
238+
raise Exception('ERROR prep protein file.')
220239
input_dir = os.path.dirname(input_protein)
221240
output_dir = str(opts.output_dir)
222241
if not os.path.exists(output_dir):
@@ -228,9 +247,10 @@ def gbsa_runner(opts: GBSAModel) -> int:
228247

229248
cmd = sh.Command('unigbsa-pipeline')
230249
args = [
231-
'-i', opts.input_protein.get_path(),
232-
'-l', " ".join(opts.input_ligands),
233-
'-c', configfile,
250+
'-i', preped_protein,
251+
'-l', *opts.input_ligands,
252+
'-validate',
253+
'-c', str(configfile),
234254
'-o', csvoutfile
235255
]
236256

setup.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,8 @@ def get_readme(rel_path):
5151
'unigbsa-buildsys = unigbsa.CLI:simulation_builder',
5252
'unigbsa-md = unigbsa.CLI:simulation_run',
5353
'unigbsa-plots = unigbsa.CLI:mmpbsa_plot',
54-
'unigbsa-scan = unigbsa.scanparas.scan:main'
54+
'unigbsa-scan = unigbsa.scanparas.scan:main',
55+
'unigbsa-validate = unigbsa.CLI:ligand_check'
5556
]},
5657
include_package_data=True
5758
)

unigbsa/CLI.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from .utils import generate_index_file, process_pbc
88

99
from .simulation import topology, mdrun
10+
from .simulation.utils import ligand_validate
1011
from .settings import logging
1112
from .version import __version__
1213

@@ -292,3 +293,22 @@ def mmpbsa_plot():
292293
func = funcdic[fname]
293294
logging.info('Analysis file: %s'%fname)
294295
func(datfile, outdir=oup)
296+
297+
298+
def ligand_check():
299+
parser = argparse.ArgumentParser(description='Analysis and plot results for MMPBSA.')
300+
parser.add_argument('-i', help='Ligand file to validate.', required=True)
301+
parser.add_argument('-o', help='Ligand file after validate. default: None', default=None)
302+
parser.add_argument('-v', '--version', action='version', version="{prog}s ({version})".format(prog="%(prog)", version=__version__))
303+
304+
args = parser.parse_args()
305+
ligandfile, outfile = args.i, args.o
306+
if not outfile:
307+
outfile = ligand_validate(ligandfile, outfile)
308+
if outfile == ligandfile:
309+
print(f'{ligandfile} is OK as the input for unigbsa.')
310+
else:
311+
print(f'{ligandfile} is not OK as the input for unigbsa. But unigbsa-validate can repair it.')
312+
else:
313+
outfile = ligand_validate(ligandfile, outfile)
314+
print(f'{outfile} is OK as the input for unigbsa.')

unigbsa/pipeline.py

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from unigbsa.utils import generate_index_file, load_configue_file
1111
from unigbsa.simulation.mdrun import GMXEngine
1212
from unigbsa.simulation.topology import build_topol, build_protein
13+
from unigbsa.simulation.utils import ligand_validate
1314
from unigbsa.settings import logging, DEFAULT_CONFIGURE_FILE, GMXEXE, set_OMP_NUM_THREADS
1415

1516
from tqdm import tqdm
@@ -52,7 +53,7 @@ def traj_pipeline(complexfile, trajfile, topolfile, indexfile, pbsaParas=None, m
5253
print('%6d %4s %18.4f '%(irow['Frames'], irow['mode'], irow['TOTAL']))
5354
return detal_G
5455

55-
def base_pipeline(receptorfile, ligandfiles, paras, nt=1, mmpbsafile=None, outfile='BindingEnergy.csv', verbose=False):
56+
def base_pipeline(receptorfile, ligandfiles, paras, nt=1, mmpbsafile=None, outfile='BindingEnergy.csv', validate=False, verbose=False):
5657
"""
5758
This function takes a receptorfile and ligandfile, and build a complex.pdb and complex.top file
5859
@@ -81,7 +82,8 @@ def base_pipeline(receptorfile, ligandfiles, paras, nt=1, mmpbsafile=None, outfi
8182
if not os.path.exists(ligandName):
8283
os.mkdir(ligandName)
8384
os.chdir(ligandName)
84-
85+
if validate:
86+
ligandfile = ligand_validate(ligandfile, ligandName+'.mol')
8587
grofile = 'complex.pdb'
8688
topfile = 'complex.top'
8789
logging.info('Build ligand topology: %s'%ligandName)
@@ -118,7 +120,7 @@ def base_pipeline(receptorfile, ligandfiles, paras, nt=1, mmpbsafile=None, outfi
118120

119121

120122
def single(arg):
121-
receptor, ligandfile, simParas, ligandfiles, mmpbsafile, nt, pbsaParas, verbose, receptorfile = arg
123+
receptor, ligandfile, simParas, ligandfiles, mmpbsafile, nt, pbsaParas, validate, verbose, receptorfile = arg
122124
d1 = pd.DataFrame({'Frames': 1, 'mode':pbsaParas['modes'], 'complex':0.0,'receptor':0.0,'ligand':0.0,'Internal':0.0,'Van der Waals':0.0,'Electrostatic':0,'Polar Solvation':0.0,'Non-Polar Solvation':0.0,'Gas':0.0,'Solvation':0.0,'TOTAL':0.0}, index=[1])
123125
cwd = os.getcwd()
124126
statu = 'S'
@@ -127,6 +129,8 @@ def single(arg):
127129
if not os.path.exists(ligandName):
128130
os.mkdir(ligandName)
129131
os.chdir(ligandName)
132+
if validate:
133+
ligandfile = ligand_validate(ligandfile, ligandName+'.mol')
130134
grofile = 'complex.pdb'
131135
topfile = 'complex.top'
132136
if len(ligandfiles) == 1:
@@ -179,7 +183,7 @@ def single(arg):
179183
d1['status'] = statu
180184
return d1
181185

182-
def minim_pipeline(receptorfile, ligandfiles, paras, mmpbsafile=None, nt=1, outfile='BindingEnergy.csv', verbose=False):
186+
def minim_pipeline(receptorfile, ligandfiles, paras, mmpbsafile=None, nt=1, outfile='BindingEnergy.csv', validate=False, verbose=False):
183187
"""
184188
It runs the simulation pipeline for each ligand.
185189
@@ -199,7 +203,7 @@ def minim_pipeline(receptorfile, ligandfiles, paras, mmpbsafile=None, nt=1, outf
199203

200204
args = [(receptor, ligandfile, simParas,
201205
ligandfiles, mmpbsafile, 1,
202-
pbsaParas, verbose, receptorfile) for ligandfile in sorted(ligandfiles)]
206+
pbsaParas, validate, verbose, receptorfile) for ligandfile in sorted(ligandfiles)]
203207
if len(args) == 1:
204208
df = single(args[0])
205209
else:
@@ -285,6 +289,7 @@ def main(args=None):
285289
parser.add_argument('-d', dest='ligdir', help='Floder contains many ligand files. file format: .mol or .sdf', default=None)
286290
parser.add_argument('-f', dest='pbsafile', help='gmx_MMPBSA input file. default=None', default=None)
287291
parser.add_argument('-o', dest='outfile', help='Output file.', default='BindingEnergy.csv')
292+
parser.add_argument('-validate', help='Validate the ligand file. default: False', action='store_true', default=False)
288293
parser.add_argument('-nt', dest='thread', help='Set number of thread to run this program.', type=int, default=multiprocessing.cpu_count())
289294
parser.add_argument('--decomp', help='Decompose the free energy. default:False', action='store_true', default=False)
290295
parser.add_argument('--verbose', help='Keep all the files.', action='store_true', default=False)
@@ -323,7 +328,7 @@ def main(args=None):
323328
paras['GBSA']['modes'] = gbtype
324329

325330
if paras['simulation']['mode'] == 'em':
326-
minim_pipeline(receptorfile=receptor, ligandfiles=ligands, paras=paras, outfile=outfile, mmpbsafile=mmpbsafile, verbose=verbose, nt=nt)
331+
minim_pipeline(receptorfile=receptor, ligandfiles=ligands, paras=paras, outfile=outfile, mmpbsafile=mmpbsafile, validate=args.validate, verbose=verbose, nt=nt)
327332
elif paras['simulation']['mode'] == 'md':
328333
md_pipeline(receptorfile=receptor, ligandfiles=ligands, paras=paras, outfile=outfile, mmpbsafile=mmpbsafile, verbose=verbose, nt=nt)
329334
elif paras['simulation']['mode'] == 'input':

unigbsa/simulation/utils.py

Lines changed: 162 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
import os
2-
from unigbsa.settings import GMXEXE
2+
import uuid
3+
import shutil
4+
from pathlib import Path
5+
from unigbsa.settings import GMXEXE, logging
6+
7+
38
def convert_format(inputfile, filetype, outfile=None, outtype='mol'):
49
"""
510
Convert a file of type filetype to mol2 format
@@ -335,3 +340,159 @@ def obtain_net_charge(sdfile):
335340
# Calculate the net charge of the molecule
336341
net_charge = mol.GetTotalCharge()
337342
return net_charge
343+
344+
345+
def check_forcefield(sdfile):
346+
sqm_key = "grms_tol=0.005,qm_theory='AM1',scfconv=1.d-5,ndiis_attempts=700,maxcyc=0"
347+
tmpdir = Path('/tmp') / str(uuid.uuid4())
348+
sdfile = Path(sdfile).absolute()
349+
net_charge = obtain_net_charge(str(sdfile))
350+
tmpdir.mkdir(exist_ok=True)
351+
cwd = os.getcwd()
352+
os.chdir(tmpdir)
353+
cmd = f'export OMP_NUM_THREADS=1;acpype -i {sdfile} -b MOL -a gaff -c bcc -n {net_charge} -k "{sqm_key}" >acpype.log 2>&1 '
354+
rc = os.system(cmd)
355+
os.chdir(cwd)
356+
shutil.rmtree(tmpdir)
357+
if rc != 0:
358+
return False
359+
else:
360+
return True
361+
362+
363+
def check_element(sdfile):
364+
from openbabel import openbabel
365+
ext = Path(sdfile).suffix[1:]
366+
obConversion = openbabel.OBConversion()
367+
obConversion.SetInAndOutFormats(ext, ext)
368+
mol = openbabel.OBMol()
369+
if not obConversion.ReadFile(mol, str(sdfile)):
370+
return -1
371+
372+
acceptable_elements = {6, 7, 8, 16, 15, 1, 9, 17, 35, 53}
373+
for atom in openbabel.OBMolAtomIter(mol):
374+
element = atom.GetAtomicNum()
375+
if element not in acceptable_elements:
376+
return 1
377+
return 0
378+
379+
380+
def get_total_valence_electrons(sdfile):
381+
from openbabel import openbabel
382+
extin = Path(sdfile).suffix[1:]
383+
obConversion = openbabel.OBConversion()
384+
obConversion.SetInAndOutFormats(extin, extin)
385+
mol = openbabel.OBMol()
386+
obConversion.ReadFile(mol, str(sdfile))
387+
total_valence_electrons = 0
388+
for atom in openbabel.OBMolAtomIter(mol):
389+
total_valence_electrons += atom.GetAtomicNum() - atom.GetFormalCharge()
390+
return total_valence_electrons
391+
392+
393+
def get_electronegativity(atomic_number):
394+
electronegativity_table = {
395+
1: 2.20, # Hydrogen (H)
396+
6: 2.55, # Carbon (C)
397+
7: 3.04, # Nitrogen (N)
398+
8: 3.44, # Oxygen (O)
399+
15: 2.19, # Phosphorus (P)
400+
16: 2.58, # Sulfur (S)
401+
9: 3.98, # Fluorine (F)
402+
17: 3.16, # Chlorine (Cl)
403+
35: 2.96, # Bromine (Br)
404+
53: 2.66, # Iodine (I)
405+
}
406+
return electronegativity_table.get(atomic_number, 0)
407+
408+
409+
def adjust_charge_based_on_electronegativity(ob_mol):
410+
from openbabel import openbabel
411+
max_electronegativity = 0
412+
target_atom = None
413+
414+
for atom in openbabel.OBMolAtomIter(ob_mol):
415+
if atom.GetFormalCharge() != 0:
416+
electronegativity = get_electronegativity(atom.GetAtomicNum())
417+
if electronegativity > max_electronegativity:
418+
max_electronegativity = electronegativity
419+
target_atom = atom
420+
421+
if target_atom is not None:
422+
if target_atom.GetFormalCharge() > 0:
423+
target_atom.SetFormalCharge(target_atom.GetFormalCharge() - 1)
424+
elif target_atom.GetFormalCharge() < 0:
425+
target_atom.SetFormalCharge(target_atom.GetFormalCharge() + 1)
426+
427+
428+
def add_hydrogen(sdfile, outfile=None):
429+
from openbabel import openbabel
430+
extin = extout = Path(sdfile).suffix[1:]
431+
obConversion = openbabel.OBConversion()
432+
obConversion.SetInAndOutFormats(extin, extout)
433+
mol = openbabel.OBMol()
434+
obConversion.ReadFile(mol, str(sdfile))
435+
mol.AddHydrogens()
436+
mol_string = obConversion.WriteString(mol)
437+
if outfile:
438+
with open(outfile, 'w') as fw:
439+
fw.write(mol_string)
440+
else:
441+
return mol
442+
443+
444+
def repair_ligand(sdfile, outfile=None):
445+
from openbabel import openbabel
446+
extin = Path(sdfile).suffix[1:]
447+
if outfile is None:
448+
fname = str(uuid.uuid4()) + '.' + extin
449+
outfile = Path('/tmp') / fname
450+
extout = Path(outfile).suffix[1:]
451+
obConversion = openbabel.OBConversion()
452+
obConversion.SetInAndOutFormats(extin, extout)
453+
mol = openbabel.OBMol()
454+
obConversion.ReadFile(mol, str(sdfile))
455+
mol.AddHydrogens()
456+
mol.DeleteHydrogens()
457+
# Correct valence information
458+
pH = 7.4
459+
charge_model = openbabel.OBChargeModel.FindType("mmff94")
460+
charge_model.ComputeCharges(mol, str(pH))
461+
mol.CorrectForPH()
462+
mol.PerceiveBondOrders()
463+
total_valence_electrons = sum([atom.GetAtomicNum() - atom.GetFormalCharge() for atom in openbabel.OBMolAtomIter(mol)])
464+
if total_valence_electrons % 2 == 1:
465+
adjust_charge_based_on_electronegativity(mol)
466+
mol_string = obConversion.WriteString(mol)
467+
new_string = []
468+
for i, line in enumerate(mol_string.split('\n')):
469+
if i >= 4 and len(line) >= 30 and line.startswith(' '):
470+
line = line[:42] + ' 0 0 0 0 0 0 0 0 0'
471+
new_string.append(line)
472+
mol_string = '\n'.join(new_string)
473+
with open(outfile, 'w') as fw:
474+
fw.write(mol_string)
475+
add_hydrogen(outfile, outfile)
476+
return outfile
477+
478+
479+
def ligand_validate(sdfile, outfile=None):
480+
rc = check_element(sdfile)
481+
if rc == -1:
482+
logging.error(f'Failed to load {sdfile}, please check your input {sdfile}.')
483+
exit(256)
484+
elif rc == 1:
485+
logging.error(f'Ligand file only accept C N O S P H F Cl Br I, please check your input {sdfile}.')
486+
exit(257)
487+
total_valence_electrons = get_total_valence_electrons(sdfile)
488+
if total_valence_electrons % 2 == 1 or not check_forcefield(sdfile):
489+
logging.warning(f'The total valence electrons of your ligand is odd({total_valence_electrons}) or forcefield check error, try to repair input ligand.')
490+
outfile = repair_ligand(sdfile, outfile=outfile)
491+
total_valence_electrons = get_total_valence_electrons(outfile)
492+
if total_valence_electrons % 2 == 1 or not check_forcefield(outfile):
493+
logging.error(f'The total valence electrons of your ligand is stil odd({total_valence_electrons}) or forcefield check error after repair ligand, please check your input {sdfile}.')
494+
exit(257)
495+
else:
496+
return outfile
497+
else:
498+
return sdfile

unigbsa/version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
1-
__version__="0.1.5"
1+
__version__="0.1.6"
22

0 commit comments

Comments
 (0)