24 import _ost_mol_mm
as mm
26 from ost
import settings, mol, geom
27 import os, subprocess, math
31 def _GetInteraction(functype, atoms, params):
32 """Get an mm.Interaction with the given func-type and params for the given
33 atoms (name and types extracted from there)."""
35 interaction.SetNames([a.name
for a
in atoms])
36 interaction.SetTypes([a.GetStringProp(
'type')
for a
in atoms])
37 interaction.SetParam(params)
40 def _MakeComponentBuildingBlock(eh, ff_dict):
41 """Take EntityHandle eh (from ParseModifiedPDB) and ff_dict (from
42 ParseAmberForceField) and return BuildingBlock."""
45 angle_scale = math.pi/180.
47 bond_k_scale = 418.4*2.
53 atype = a.GetStringProp(
'type')
54 if not atype
in atype_dict:
55 atype_dict[atype] = [a.handle]
57 atype_dict[atype].append(a.handle)
60 for atype, mass
in ff_dict[
'MASS']:
61 for a
in atype_dict[atype]: a.SetMass(mass)
66 bb.AddAtom(a.name, a.GetStringProp(
'type'), a.GetCharge(), a.GetMass())
74 bond.SetNames([a1.name, a2.name])
75 bond.SetTypes([a1.GetStringProp(
'type'), a2.GetStringProp(
'type')])
78 for atype1, atype2, d0, k
in ff_dict[
'BOND']:
79 for a1
in atype_dict[atype1]:
80 for a2
in atype_dict[atype2]:
82 if not mol.BondExists(a1, a2):
continue
83 if [a1, a2]
in added_bonds
or [a2, a1]
in added_bonds:
continue
84 added_bonds.append([a1,a2])
85 params = [d0*dist_scale, k*bond_k_scale]
86 bond = _GetInteraction(mm.FuncType.HarmonicBond, [a1, a2], params)
87 bb.AddBond(bond, replace_existing=
True)
91 for atype1, atype2, atype3, a0, k
in ff_dict[
'ANGL']:
93 for a2
in atype_dict[atype2]:
94 for a1
in atype_dict[atype1]:
95 if not mol.BondExists(a1, a2):
continue
96 for a3
in atype_dict[atype3]:
98 if not mol.BondExists(a2, a3):
continue
100 if [a1, a2, a3]
in added_angles
or [a3, a2, a1]
in added_angles:
102 added_angles.append([a1, a2, a3])
103 angle = _GetInteraction(mm.FuncType.HarmonicAngle, [a1, a2, a3],
104 [a0*angle_scale, k*angle_k_scale*2])
108 for atype1, atype2, atype3, atype4, idiv, period, phase, k
in ff_dict[
'DIHE']:
111 for a1
in atype_dict[atype1]:
112 for a2
in atype_dict[atype2]:
113 if not mol.BondExists(a1, a2):
continue
114 for a3
in atype_dict[atype3]:
115 if not mol.BondExists(a2, a3):
continue
116 if a1 == a3:
continue
117 for a4
in atype_dict[atype4]:
119 if not mol.BondExists(a3, a4):
continue
120 if a2 == a4:
continue
121 if [a1, a2, a3, a4]
in added_dihedrals
or \
122 [a4, a3, a2, a1]
in added_dihedrals:
continue
123 added_dihedrals.append([a1, a2, a3, a4])
124 dihe = _GetInteraction(mm.FuncType.PeriodicDihedral, [a1, a2, a3, a4],
125 [period, phase*angle_scale, k*angle_k_scale])
130 for atype1, atype2, atype3, atype4, period, phase, k
in ff_dict[
'IMPR']:
132 for ac
in atype_dict[atype3]:
133 for a1
in atype_dict[atype1]:
134 if not mol.BondExists(ac, a1):
continue
135 for a2
in atype_dict[atype2]:
136 if not mol.BondExists(ac, a2):
continue
137 if a1 == a2:
continue
138 for a4
in atype_dict[atype4]:
140 if not mol.BondExists(ac, a4):
continue
141 if a2 == a4
or a1 == a4:
continue
142 if [ac, a1, a2, a4]
in added_impropers
or \
143 [ac, a1, a4, a2]
in added_impropers
or \
144 [ac, a2, a1, a4]
in added_impropers
or \
145 [ac, a2, a4, a1]
in added_impropers
or \
146 [ac, a4, a1, a2]
in added_impropers
or \
147 [ac, a4, a2, a1]
in added_impropers:
continue
148 added_impropers.append([ac, a1, a2, a4])
149 impr = _GetInteraction(mm.FuncType.PeriodicImproper, [a1, a2, ac, a4],
150 [period, phase*angle_scale, k*angle_k_scale])
154 def _ParseModifiedPDB(filename):
155 """Read mpdb file produced by antechamber and return tuple of:
156 - EntityHandle with connectivity, atom types (property 'type') and charges
157 - Residue name as extracted from the mpdb file
158 A RuntimeError is raised if the file can contains multiple residues.
160 eh = mol.CreateEntity()
162 edi = eh.EditXCS(mol.BUFFERED_EDIT)
163 chain = edi.InsertChain(
'A')
166 with open(filename,
'r') as in_file:
170 if line.startswith(
'ATOM'):
171 aname = line[12:17].strip()
174 rname = line[17:20].strip()
175 r = edi.AppendResidue(chain, rname)
176 elif rname != line[17:20].strip():
177 raise RuntimeError(
"More than one residue in file " + filename +\
180 charge = float(line[54:66])
181 atype = line[78:80].strip()
182 a = edi.InsertAtom(r, aname,
geom.Vec3())
183 a.SetStringProp(
'type', atype)
185 elif line.startswith(
'CONECT'):
186 ai1 = int(line[6:11])
191 ai2 = int(line[j:j+5])
194 if not s
in bond_list: bond_list.append(s)
200 for indices
in bond_list:
201 indices = list(indices)
202 a1 = r.atoms[indices[0]-1]
203 a2 = r.atoms[indices[1]-1]
209 def _ParseAmberForceField(filename):
210 """Read frcmod file produced by parmchk2 and return dictionary with all
211 entries for masses, bonds, angles, dihedrals, impropers and non-bonded (LJ)
212 interactions. Stored as key/list-of-value pairs:
213 - 'MASS': [atype, mass]
214 - 'BOND': [atype1, atype2, d0, k]
215 - 'ANGL': [atype1, atype2, atype3, a0, k]
216 - 'DIHE': [atype1, atype2, atype3, atype4, idiv, period, phase, k/idiv]
217 - 'IMPR': [atype1, atype2, atype3, atype4, period, phase, k]
218 - 'NONB': [Rvdw, epsilon]
220 keywords = [
'MASS',
'BOND',
'ANGL',
'DIHE',
'IMPR',
'NONB']
221 with open(filename,
'r') as in_file:
226 if not keyword
in keywords:
continue
228 ff_dict[keyword] = []
229 line = in_file.next()
230 while len(line.strip()) > 0:
233 ost.LogWarning(
'The following line in ' + filename +
' (' + keyword +\
234 ' section) needs revision:\n' + line.strip())
236 if keyword ==
'MASS':
237 atype = line[0:2].strip()
240 ff_dict[keyword].append([atype, mass])
241 elif keyword ==
'BOND':
242 atype1 = line[:2].strip()
243 atype2 = line[3:5].strip()
247 ff_dict[keyword].append([atype1, atype2, d0, k])
248 elif keyword ==
'ANGL':
249 atype1 = line[:2].strip()
250 atype2 = line[3:5].strip()
251 atype3 = line[6:8].strip()
255 ff_dict[keyword].append([atype1, atype2, atype3, a0, k])
256 elif keyword ==
'DIHE':
257 atype1 = line[:2].strip()
258 atype2 = line[3:5].strip()
259 atype3 = line[6:8].strip()
260 atype4 = line[9:11].strip()
261 s = line[11:].split()
267 period = abs(float(s[3]))
268 ff_dict[keyword].append([atype1, atype2, atype3, atype4, idiv,
269 period, phase, k/float(idiv)])
270 elif keyword ==
'IMPR':
271 atype1 = line[:2].strip()
272 atype2 = line[3:5].strip()
273 atype3 = line[6:8].strip()
274 atype4 = line[9:11].strip()
275 s = line[11:].split()
279 ff_dict[keyword].append([atype1, atype2, atype3, atype4, period,
281 elif keyword ==
'NONB':
283 atype = line[0:2].strip()
286 epsilon = float(s[1])
287 ff_dict[keyword].append([atype, Rvdw, epsilon])
289 line = in_file.next()
293 def RunAntechamber(res_name, filename, format='ccif', amberhome=None,
295 """Run Antechamber to guess force field parameters for a given residue name.
297 This requires an installation of AmberTools (tested with AmberTools15) with
298 binaries ``antechamber`` and ``parmchk2``.
300 This has the same restrictions as Antechamber itself and we assume the input
301 to be uncharged. Note that Antechamber cannot deal with metal ions and other
302 non-organic elements.
304 The results are stored in a separate folder named `res_name` within
305 `base_out_dir` (if given, otherwise the current working directory). The main
306 output files are ``frcmod`` and ``out.mpdb``. The former contains force field
307 parameters and masses. The latter maps atom names to atom types and defines
308 the partial charges. The same output could be obtained as follows:
310 .. code-block:: console
312 $ antechamber -i <FILENAME> -fi <FORMAT> -bk '<RES_NAME>' -o out.mol2 -fo mol2 -c bcc -pf yes
313 $ parmchk2 -i out.mol2 -f mol2 -o frcmod -a Y
314 $ antechamber -i out.mol2 -fi mol2 -o out.mpdb -fo mpdb -pf yes
316 The force field parameters can be manually modified if needed. It can for
317 instance happen that some parameters cannot be identified. Those lines will
318 be marked with a comment "ATTN, need revision".
320 :param res_name: Residue name for which we desire force field parameters.
321 :type res_name: :class:`str`
322 :param filename: Path to a file which contains the necessary information for
323 `res_name`. It must include all hydrogens.
324 :type filename: :class:`str`
325 :param format: Format of file given with `filename`. Common formats are 'ccif'
326 for PDB's component dictionary or 'pdb' for a PDB file
327 containing the desired residue with all hydrogens.
328 :type format: :class:`str`
329 :param amberhome: Base path of your AmberTools installation. If not None,
330 we look for ``antechamber`` and ``parmchk2`` within
331 ``AMBERHOME/bin`` additionally to the system's ``PATH``.
332 :type amberhome: :class:`str`
333 :param base_out_dir: Path to a base path, where the output will be stored.
334 If None, the current working directory is used.
335 :type base_out_dir: :class:`str`
338 if amberhome
is None:
341 search_paths = [os.path.join(amberhome,
'bin')]
343 antechamber = settings.Locate(
'antechamber', search_paths=search_paths)
344 parmchk2 = settings.Locate(
'parmchk2', search_paths=search_paths)
346 ost.LogError(
"Failed to find Antechamber binaries. Make sure you have "
347 "AmberTools installed!")
352 if base_out_dir
is None:
354 out_dir = os.path.abspath(os.path.join(base_out_dir, res_name))
355 if not os.path.exists(out_dir):
359 except Exception
as ex:
360 ost.LogError(
"Failed to create output directory " + out_dir +
"!")
366 cmds = [antechamber +
" -i " + filename +
" -fi " + format +
" -bk " \
367 + res_name +
" -o out.mol2 -fo mol2 -c bcc -pf yes",
368 parmchk2 +
" -i out.mol2 -f mol2 -o frcmod -a Y",
369 antechamber +
" -i out.mol2 -fi mol2 -o out.mpdb -fo mpdb -pf yes"]
370 all_sout =
"Generating force field parameters for " + res_name +
"\n"
373 all_sout +=
"-"*70 +
"\n" +
"Stdout of: " + cmd +
"\n" +
"-"*70 +
"\n"
374 all_serr +=
"-"*70 +
"\n" +
"Stderr of: " + cmd +
"\n" +
"-"*70 +
"\n"
375 job = subprocess.Popen(cmd.split(
" "), stdout=subprocess.PIPE,
376 stderr=subprocess.PIPE)
377 sout, serr = job.communicate()
380 if job.returncode != 0:
381 ost.LogError(
"Unsuccessful execution of " + cmd +
". Return code: "\
382 + str(job.returncode))
384 with open(
"console.stdout",
"w")
as txt_file:
385 txt_file.write(all_sout)
386 with open(
"console.stderr",
"w")
as txt_file:
387 txt_file.write(all_serr)
388 except Exception
as ex:
389 ost.LogError(
"Failed to excecute antechamber binaries!")
396 frcmod_filename = os.path.join(out_dir,
'frcmod')
397 mpdb_filename = os.path.join(out_dir,
'out.mpdb')
398 if not os.path.exists(frcmod_filename):
399 raise RuntimeError(
"Failed to generate frcmod file with Antechamber!")
400 if not os.path.exists(mpdb_filename):
401 raise RuntimeError(
"Failed to generate out.mpdb file with Antechamber!")
404 """Add data from a frcmod and an mpdb file to a force field.
406 This will add a new :class:`~ost.mol.mm.BuildingBlock` to `force_field` for
407 the residue defined in those files (residue name is extracted from the mpdb
408 file which can only contain a single residue). Charges for each atom are
409 extracted from the mpdb file. According to the frcmod file, an
410 :class:`~ost.mol.mm.Interaction` is added for each bond, angle, dihedral and
411 improper. Atom types with masses and non-bonded interactions are added to
412 `force_field` as needed.
414 :param force_field: A force field object to which the new parameters are
416 :type force_field: :class:`~ost.mol.mm.Forcefield`
417 :param frcmod_filename: Path to ``frcmod`` file as generated by ``parmchk2``.
418 :type frcmod_filename: :class:`str`
419 :param mpdb_filename: Path to mpdb file as generated by ``antechamber``.
420 :type mpdb_filename: :class:`str`
421 :return: The updated force field (same as `force_field`).
422 :rtype: :class:`~ost.mol.mm.Forcefield`
425 if not os.path.exists(frcmod_filename):
426 raise RuntimeError(
"Could not find frcmod file: " + frcmod_filename)
427 if not os.path.exists(mpdb_filename):
428 raise RuntimeError(
"Could not find mpdb file: " + mpdb_filename)
431 eh, res_name = _ParseModifiedPDB(mpdb_filename)
432 except Exception
as ex:
433 ost.LogError(
"Failed to parse mpdb file: " + mpdb_filename)
436 ff_dict = _ParseAmberForceField(frcmod_filename)
437 except Exception
as ex:
438 ost.LogError(
"Failed to parse frcmod file: " + frcmod_filename)
440 ost.LogInfo(
"Adding force field for " + res_name)
442 for aname, mass
in ff_dict[
'MASS']:
443 force_field.AddMass(aname, mass)
445 lj_sigma_scale = 2./10./2**(1./6.)
446 lj_epsilon_scale = 4.184
447 for aname, Rvdw, epsilon
in ff_dict[
'NONB']:
449 if Rvdw == 0
or epsilon == 0:
453 lj.SetParam([Rvdw*lj_sigma_scale, epsilon*lj_epsilon_scale])
454 force_field.AddLJ(lj)
456 bb = _MakeComponentBuildingBlock(eh, ff_dict)
457 force_field.AddBuildingBlock(res_name, bb)
462 """Add data from a directory created with :meth:`Run` to a force field.
463 See :meth:`AddFromFiles` for details.
465 :param force_field: A force field object to which the new parameters are
467 :type force_field: :class:`~ost.mol.mm.Forcefield`
468 :param out_dir: Output directory as created with :meth:`Run`. Must contain
469 files ``frcmod`` and ``out.mpdb``.
470 :type out_dir: :class:`str`
471 :return: The updated force field (same as `force_field`).
472 :rtype: :class:`~ost.mol.mm.Forcefield`
474 frcmod_filename = os.path.join(out_dir,
'frcmod')
475 mpdb_filename = os.path.join(out_dir,
'out.mpdb')
476 return AddFromFiles(force_field, frcmod_filename, mpdb_filename)
478 def AddIon(force_field, res_name, atom_name, atom_mass, atom_charge, lj_sigma,
480 """Add a single atom as an ion to a force field.
482 Since Antechamber cannot deal with ions, you can add simple ones easily with
483 this function. This adds a :class:`~ost.mol.mm.BuildingBlock` to `force_field`
484 for the given residue name containing a single atom. The atom will have a type
485 with the same name as the atom name and the given mass, charge and non-bonded
486 (LJ) interaction parameters.
488 :param force_field: A force field object to which the ion is added.
489 :type force_field: :class:`~ost.mol.mm.Forcefield`
490 :param res_name: Residue name for the ion to be added.
491 :type res_name: :class:`str`
492 :param atom_name: Atom name which is also used as atom type name.
493 :type atom_name: :class:`str`
494 :param atom_mass: Mass of the atom.
495 :type atom_mass: :class:`float`
496 :param atom_charge: Charge of the atom.
497 :type atom_charge: :class:`float`
498 :param lj_sigma: The sigma parameter for the non-bonded LJ interaction.
499 :type lj_sigma: :class:`float` in nm
500 :param lj_epsilon: The sigma parameter for the non-bonded LJ interaction.
501 :type lj_epsilon: :class:`float` in kJ/mol
504 force_field.AddMass(atom_name, atom_mass)
507 bb.AddAtom(atom_name, atom_name, atom_charge, atom_mass)
508 force_field.AddBuildingBlock(res_name, bb)
511 lj.SetTypes([atom_name])
512 lj.SetParam([lj_sigma, lj_epsilon])
513 force_field.AddLJ(lj)
515 __all__ = (
'RunAntechamber',
'AddFromFiles',
'AddFromPath',
'AddIon',)
Three dimensional vector class, using Real precision.