def _process_mass_line(self, line): words = line.split() try: mass = float(words[1]) except ValueError: raise ParameterError('Could not convert mass to float [%s]' % words[1]) except IndexError: raise ParameterError('Error parsing MASS line. Not enough tokens') if words[0] in self.atom_types: self.atom_types[words[0]].mass = mass else: atype = AtomType(words[0], len(self.atom_types)+1, mass, AtomicNum[element_by_mass(mass)]) self.atom_types[words[0]] = atype
def _process_mass_line(self, line): words = line.split() try: mass = float(words[1]) except ValueError: raise ParameterError('Could not convert mass to float [%s]' % words[1]) except IndexError: raise ParameterError('Error parsing MASS line. Not enough tokens') if words[0] in self.atom_types: self.atom_types[words[0]].mass = mass elif words[0] in ('EP', 'LP'): atype = AtomType(words[0], len(self.atom_types)+1, mass, 0) self.atom_types[words[0]] = atype else: atype = AtomType(words[0], len(self.atom_types)+1, mass, AtomicNum[element_by_mass(mass)]) self.atom_types[words[0]] = atype
def read_topology_file(self, tfile): """ Reads _only_ the atom type definitions from a topology file. This is unnecessary for versions 36 and later of the CHARMM force field. Parameters ---------- tfile : str Name of the CHARMM topology file to read """ conv = CharmmParameterSet._convert if isinstance(tfile, str): own_handle = True f = iter(CharmmFile(tfile)) else: own_handle = False f = tfile hpatch = tpatch = None # default Head and Tail patches residues = dict() patches = dict() hpatches = dict() tpatches = dict() line = next(f) try: while line: line = line.strip() if line[:4] == "MASS": words = line.split() try: idx = conv(words[1], int, "atom type") name = words[2].upper() mass = conv(words[3], float, "atom mass") except IndexError: raise CharmmError("Could not parse MASS section of %s" % tfile) # The parameter file might or might not have an element name try: elem = words[4].upper() if len(elem) == 2: elem = elem[0] + elem[1].lower() atomic_number = AtomicNum[elem] except (IndexError, KeyError): # Figure it out from the mass atomic_number = AtomicNum[element_by_mass(mass)] atype = AtomType(name=name, number=idx, mass=mass, atomic_number=atomic_number) self.atom_types_str[atype.name] = atype self.atom_types_int[atype.number] = atype self.atom_types_tuple[(atype.name, atype.number)] = atype elif line[:4] == "DECL": pass # Not really sure what this means elif line[:4] == "DEFA": words = line.split() if len(words) < 5: warnings.warn("DEFA line has %d tokens; expected 5" % len(words)) else: it = iter(words[1:5]) for tok, val in zip(it, it): if val.upper() == "NONE": val = None if tok.upper().startswith("FIRS"): hpatch = val elif tok.upper() == "LAST": tpatch = val else: warnings.warn("DEFA patch %s unknown" % val) elif line[:4].upper() in ("RESI", "PRES"): restype = line[:4].upper() # Get the residue definition words = line.split() resname = words[1].upper() # Assign default patches hpatches[resname] = hpatch tpatches[resname] = tpatch try: charge = float(words[2]) except (IndexError, ValueError): warnings.warn("No charge for %s" % resname) if restype == "RESI": res = ResidueTemplate(resname) elif restype == "PRES": res = PatchTemplate(resname) else: assert False, "restype != RESI or PRES" line = next(f) group = [] ictable = [] while line: line = line.lstrip() if line[:5].upper() == "GROUP": if group: res.groups.append(group) group = [] elif line[:4].upper() == "ATOM": words = line.split() name = words[1].upper() type = words[2].upper() charge = float(words[3]) atom = Atom(name=name, type=type, charge=charge) group.append(atom) res.add_atom(atom) elif line.strip() and line.split()[0].upper() in ("BOND", "DOUBLE"): it = iter([w.upper() for w in line.split()[1:]]) for a1, a2 in zip(it, it): if a1.startswith("-"): res.head = res[a2] continue if a2.startswith("-"): res.head = res[a1] continue if a1.startswith("+"): res.tail = res[a2] continue if a2.startswith("+"): res.tail = res[a1] continue # Apparently PRES objects do not need to put + # or - in front of atoms that belong to adjacent # residues if restype == "PRES" and (a1 not in res or a2 not in res): continue res.add_bond(a1, a2) elif line[:4].upper() == "CMAP": pass elif line[:5].upper() == "DONOR": pass elif line[:6].upper() == "ACCEPT": pass elif line[:2].upper() == "IC": words = line.split()[1:] ictable.append(([w.upper() for w in words[:4]], [float(w) for w in words[4:]])) elif line[:3].upper() == "END": break elif line[:5].upper() == "PATCH": it = iter(line.split()[1:]) for tok, val in zip(it, it): if val.upper() == "NONE": val = None if tok.upper().startswith("FIRS"): hpatches[resname] = val elif tok.upper().startswith("LAST"): tpatches[resname] = val elif line[:5].upper() == "DELETE": pass elif line[:4].upper() == "IMPR": it = iter([w.upper() for w in line.split()[1:]]) for a1, a2, a3, a4 in zip(it, it, it, it): if a2[0] == "-" or a3[0] == "-" or a4 == "-": res.head = res[a1] elif line[:4].upper() in ("RESI", "PRES", "MASS"): # Back up a line and bail break line = next(f) if group: res.groups.append(group) _fit_IC_table(res, ictable) if restype == "RESI": residues[resname] = res elif restype == "PRES": patches[resname] = res else: assert False, "restype != RESI or PRES" # We parsed a line we need to look at. So don't update the # iterator continue # Get the next line and cycle through line = next(f) except StopIteration: pass # Go through the patches and add the appropriate one for resname, res in iteritems(residues): if hpatches[resname] is not None: try: res.first_patch = patches[hpatches[resname]] except KeyError: warnings.warn("Patch %s not found" % hpatches[resname]) if tpatches[resname] is not None: try: res.last_patch = patches[tpatches[resname]] except KeyError: warnings.warn("Patch %s not found" % tpatches[resname]) # Now update the residues and patches with the ones we parsed here self.residues.update(residues) self.patches.update(patches) if own_handle: f.close()
def read_parameter_file(self, pfile, comments=None): """ Reads all of the parameters from a parameter file. Versions 36 and later of the CHARMM force field files have an ATOMS section defining all of the atom types. Older versions need to load this information from the RTF/TOP files. Parameters ---------- pfile : str or list of lines Name of the CHARMM parameter file to read or list of lines to parse as a file comments : list of str, optional List of comments on each of the pfile lines (if pfile is a list of lines) Notes ----- The atom types must all be loaded by the end of this routine. Either supply a PAR file with atom definitions in them or read in a RTF/TOP file first. Failure to do so will result in a raised RuntimeError. """ conv = CharmmParameterSet._convert if isinstance(pfile, str): own_handle = True f = CharmmFile(pfile) else: own_handle = False f = pfile if not isinstance(f, CharmmFile) and comments is None: comments = _EmptyStringIterator() # What section are we parsing? section = None # The current cmap we are building (these span multiple lines) current_cmap = None current_cmap2 = None current_cmap_data = [] current_cmap_res = 0 nonbonded_types = dict() # Holder parameterset = None for i, line in enumerate(f): line = line.strip() try: comment = f.comment except AttributeError: comment = comments[i] if not line: # This is a blank line continue if parameterset is None and line.strip().startswith("*>>"): parameterset = line.strip()[1:78] continue # Set section if this is a section header if line.startswith("ATOMS"): section = "ATOMS" continue if line.startswith("BONDS"): section = "BONDS" continue if line.startswith("ANGLES"): section = "ANGLES" continue if line.startswith("DIHEDRALS"): section = "DIHEDRALS" continue if line.startswith("IMPROPER"): section = "IMPROPER" continue if line.startswith("CMAP"): section = "CMAP" continue if line.startswith("NONBONDED"): read_first_nonbonded = False section = "NONBONDED" # Get nonbonded keywords words = line.split()[1:] scee = None for i, word in enumerate(words): if word.upper() == "E14FAC": try: scee = float(words[i + 1]) except (ValueError, IndexError): raise CharmmError("Could not parse 1-4 electrostatic scaling " "factor from NONBONDED card") if self._declared_nbrules: # We already specified it -- make sure it's the same # as the one we specified before if abs(self.dihedral_types[0][0].scee - scee) > TINY: raise CharmmError("Inconsistent 1-4 scalings") elif word.upper().startswith("GEOM"): if self._declared_nbrules and self.combining_rule != "geometric": raise CharmmError("Cannot combine parameter files with " "different combining rules") self.combining_rule = "geometric" continue if line.startswith("NBFIX"): section = "NBFIX" continue if line.startswith("HBOND"): section = None continue # It seems like files? sections? can be terminated with 'END' if line.startswith("END"): # should this be case-insensitive? section = None continue # If we have no section, skip if section is None: continue # See if our comments define a penalty for this line pens = _penaltyre.findall(comment) if len(pens) == 1: penalty = float(pens[0]) else: penalty = None # Now handle each section specifically if section == "ATOMS": if not line.startswith("MASS"): continue # Should this happen? words = line.split() try: idx = conv(words[1], int, "atom type") name = words[2].upper() mass = conv(words[3], float, "atom mass") except IndexError: raise CharmmError("Could not parse MASS section.") # The parameter file might or might not have an element name try: elem = words[4].upper() if len(elem) == 2: elem = elem[0] + elem[1].lower() atomic_number = AtomicNum[elem] except (IndexError, KeyError): # Figure it out from the mass atomic_number = AtomicNum[element_by_mass(mass)] atype = AtomType(name=name, number=idx, mass=mass, atomic_number=atomic_number) self.atom_types_str[atype.name] = atype self.atom_types_int[atype.number] = atype self.atom_types_tuple[(atype.name, atype.number)] = atype continue if section == "BONDS": words = line.split() try: type1 = words[0].upper() type2 = words[1].upper() k = conv(words[2], float, "bond force constant") req = conv(words[3], float, "bond equilibrium dist") except IndexError: raise CharmmError("Could not parse bonds.") key = (min(type1, type2), max(type1, type2)) bond_type = BondType(k, req) self.bond_types[(type1, type2)] = bond_type self.bond_types[(type2, type1)] = bond_type bond_type.penalty = penalty continue if section == "ANGLES": words = line.split() try: type1 = words[0].upper() type2 = words[1].upper() type3 = words[2].upper() k = conv(words[3], float, "angle force constant") theteq = conv(words[4], float, "angle equilibrium value") except IndexError: raise CharmmError("Could not parse angles.") angle_type = AngleType(k, theteq) self.angle_types[(type1, type2, type3)] = angle_type self.angle_types[(type3, type2, type1)] = angle_type # See if we have a urey-bradley try: ubk = conv(words[5], float, "Urey-Bradley force constant") ubeq = conv(words[6], float, "Urey-Bradley equil. value") ubtype = BondType(ubk, ubeq) ubtype.penalty = penalty except IndexError: ubtype = NoUreyBradley self.urey_bradley_types[(type1, type2, type3)] = ubtype self.urey_bradley_types[(type3, type2, type1)] = ubtype angle_type.penalty = penalty continue if section == "DIHEDRALS": words = line.split() try: type1 = words[0].upper() type2 = words[1].upper() type3 = words[2].upper() type4 = words[3].upper() k = conv(words[4], float, "dihedral force constant") n = conv(words[5], float, "dihedral periodicity") phase = conv(words[6], float, "dihedral phase") except IndexError: raise CharmmError("Could not parse dihedrals.") key = (type1, type2, type3, type4) # See if this is a second (or more) term of the dihedral group # that's already present. dihedral = DihedralType(k, n, phase) dihedral.penalty = penalty if key in self.dihedral_types: # See if the existing dihedral type list has a term with # the same periodicity -- If so, replace it replaced = False for i, dtype in enumerate(self.dihedral_types[key]): if dtype.per == dihedral.per: # Replace. Warn if they are different if dtype != dihedral: warnings.warn("Replacing dihedral %r with %r" % (dtype, dihedral)) self.dihedral_types[key][i] = dihedral replaced = True break if not replaced: self.dihedral_types[key].append(dihedral) # Now do the other order replaced = False key = (type4, type3, type2, type1) for i, dtype in enumerate(self.dihedral_types[key]): if dtype.per == dihedral.per: self.dihedral_types[key][i] = dihedral replaced = True break if not replaced: self.dihedral_types[key].append(dihedral) else: # key not present dtl = DihedralTypeList() dtl.append(dihedral) self.dihedral_types[(type1, type2, type3, type4)] = dtl self.dihedral_types[(type4, type3, type2, type1)] = dtl continue if section == "IMPROPER": words = line.split() try: type1 = words[0].upper() type2 = words[1].upper() type3 = words[2].upper() type4 = words[3].upper() k = conv(words[4], float, "improper force constant") theteq = conv(words[5], float, "improper equil. value") except IndexError: raise CharmmError("Could not parse dihedrals.") # If we have a 7th column, that is the real psi0 (and the 6th # is just a dummy 0) try: tmp = conv(words[6], float, "improper equil. value") theteq = tmp except IndexError: pass # Do nothing # Improper types seem not to have the central atom defined in # the first place, so just have the key a fully sorted list. We # still depend on the PSF having properly ordered improper atoms key = tuple(sorted([type1, type2, type3, type4])) improp = ImproperType(k, theteq) self.improper_types[key] = improp improp.penalty = penalty continue if section == "CMAP": # This is the most complicated part, since cmap parameters span # many lines. We won't do much error catching here. words = line.split() try: holder = [float(w) for w in words] current_cmap_data.extend(holder) except ValueError: # We assume this is a definition of a new CMAP, so # terminate the last CMAP if applicable if current_cmap is not None: # We have a map to terminate ty = CmapType(current_cmap_res, current_cmap_data) self.cmap_types[current_cmap] = ty self.cmap_types[current_cmap2] = ty try: type1 = words[0].upper() type2 = words[1].upper() type3 = words[2].upper() type4 = words[3].upper() type5 = words[4].upper() type6 = words[5].upper() type7 = words[6].upper() type8 = words[7].upper() res = conv(words[8], int, "CMAP resolution") except IndexError: raise CharmmError("Could not parse CMAP data.") # order the torsions independently k1 = [type1, type2, type3, type4, type5, type6, type7, type8] k2 = [type8, type7, type6, type5, type4, type3, type2, type1] current_cmap = tuple(min(k1, k2)) current_cmap2 = tuple(max(k1, k2)) current_cmap_res = res current_cmap_data = [] continue if section == "NONBONDED": # Now get the nonbonded values words = line.split() try: atype = words[0].upper() # 1st column is ignored epsilon = conv(words[2], float, "vdW epsilon term") rmin = conv(words[3], float, "vdW Rmin/2 term") except (IndexError, CharmmError): # If we haven't read our first nonbonded term yet, we may # just be parsing the settings that should be used. So # soldier on if read_first_nonbonded: raise for i, word in enumerate(words): if word.upper() == "E14FAC": try: scee = float(words[i + 1]) except (ValueError, IndexError): raise CharmmError("Could not parse electrostatic " "scaling constant") if self._declared_nbrules: # We already specified it -- make sure it's the # same as the one we specified before _, dt0 = next(iteritems(self.dihedral_types)) diff = abs(dt0[0].scee - scee) if diff > TINY: raise CharmmError("Inconsistent 1-4 " "scalings") else: scee = 1 / scee for key, dtl in iteritems(self.dihedral_types): for dt in dtl: dt.scee = scee elif word.upper().startswith("GEOM"): if self._declared_nbrules and self.combining_rule != "geometric": raise CharmmError("Cannot combine parameter files with " "different combining rules") self.combining_rule = "geometric" continue else: # OK, we've read our first nonbonded section for sure now read_first_nonbonded = True self._declared_nbrules = True # See if we have 1-4 parameters try: # 4th column is ignored eps14 = conv(words[5], float, "1-4 vdW epsilon term") rmin14 = conv(words[6], float, "1-4 vdW Rmin/2 term") except IndexError: eps14 = rmin14 = None nonbonded_types[atype] = [epsilon, rmin, eps14, rmin14] continue if section == "NBFIX": words = line.split() try: at1 = words[0].upper() at2 = words[1].upper() emin = abs(conv(words[2], float, "NBFIX Emin")) rmin = conv(words[3], float, "NBFIX Rmin") try: emin14 = abs(conv(words[4], float, "NBFIX Emin 1-4")) rmin14 = conv(words[5], float, "NBFIX Rmin 1-4") except IndexError: emin14 = rmin14 = None try: self.atom_types_str[at1].add_nbfix(at2, rmin, emin, rmin14, emin14) self.atom_types_str[at2].add_nbfix(at1, rmin, emin, rmin14, emin14) except KeyError: # Some stream files define NBFIX terms with an atom that # is defined in another toppar file that does not # necessarily have to be loaded. As a result, not every # NBFIX found here will necessarily need to be applied. # If we can't find a particular atom type, don't bother # adding that nbfix and press on pass except IndexError: raise CharmmError("Could not parse NBFIX terms.") self.nbfix_types[(min(at1, at2), max(at1, at2))] = (emin, rmin) # If we had any CMAP terms, then the last one will not have been added # yet. Add it here if current_cmap is not None: ty = CmapType(current_cmap_res, current_cmap_data) self.cmap_types[current_cmap] = ty self.cmap_types[current_cmap2] = ty # Now we're done. Load the nonbonded types into the relevant AtomType # instances. In order for this to work, all keys in nonbonded_types # must be in the self.atom_types_str dict. Raise a RuntimeError if this # is not satisfied try: for key in nonbonded_types: self.atom_types_str[key].set_lj_params(*nonbonded_types[key]) except KeyError: raise RuntimeError("Atom type %s not present in AtomType list" % key) if parameterset is not None: self.parametersets.append(parameterset) if own_handle: f.close()