Source code for ase.io.opls

# fmt: off

import time

import numpy as np

from ase.atom import Atom
from ase.atoms import Atoms
from ase.calculators.lammpsrun import Prism
from ase.data import atomic_masses, chemical_symbols
from ase.io import read
from ase.neighborlist import NeighborList


def twochar(name):
    if len(name) > 1:
        return name[:2]
    else:
        return name + ' '


class BondData:
    def __init__(self, name_value_hash):
        self.nvh = name_value_hash

    def name_value(self, aname, bname):
        name1 = twochar(aname) + '-' + twochar(bname)
        name2 = twochar(bname) + '-' + twochar(aname)
        if name1 in self.nvh:
            return name1, self.nvh[name1]
        if name2 in self.nvh:
            return name2, self.nvh[name2]
        return None, None

    def value(self, aname, bname):
        return self.name_value(aname, bname)[1]


class CutoffList(BondData):
    def max(self):
        return max(self.nvh.values())


class AnglesData:
    def __init__(self, name_value_hash):
        self.nvh = name_value_hash

    def name_value(self, aname, bname, cname):
        for name in [
            (twochar(aname) + '-' + twochar(bname) + '-' + twochar(cname)),
                (twochar(cname) + '-' + twochar(bname) + '-' + twochar(aname))]:
            if name in self.nvh:
                return name, self.nvh[name]
        return None, None


class DihedralsData:
    def __init__(self, name_value_hash):
        self.nvh = name_value_hash

    def name_value(self, aname, bname, cname, dname):
        for name in [
            (twochar(aname) + '-' + twochar(bname) + '-' +
             twochar(cname) + '-' + twochar(dname)),
            (twochar(dname) + '-' + twochar(cname) + '-' +
             twochar(bname) + '-' + twochar(aname))]:
            if name in self.nvh:
                return name, self.nvh[name]
        return None, None


[docs] class OPLSff: """OPLS helper class. Read OPLS structure and force field parameters and write LAMMPS input. """ def __init__(self, fileobj=None, warnings=0): self.warnings = warnings self.data = {} if fileobj is not None: self.read(fileobj) def read(self, fileobj, comments='#'): def read_block(name, symlen, nvalues): """Read a data block. name: name of the block to store in self.data symlen: length of the symbol nvalues: number of values expected """ if name not in self.data: self.data[name] = {} data = self.data[name] def add_line(): line = fileobj.readline().strip() if not len(line): # end of the block return False line = line.split('#')[0] # get rid of comments if len(line) > symlen: symbol = line[:symlen] words = line[symlen:].split() if len(words) >= nvalues: if nvalues == 1: data[symbol] = float(words[0]) else: data[symbol] = [float(word) for word in words[:nvalues]] return True while add_line(): pass read_block('one', 2, 3) read_block('bonds', 5, 2) read_block('angles', 8, 2) read_block('dihedrals', 11, 4) read_block('cutoffs', 5, 1) self.bonds = BondData(self.data['bonds']) self.angles = AnglesData(self.data['angles']) self.dihedrals = DihedralsData(self.data['dihedrals']) self.cutoffs = CutoffList(self.data['cutoffs']) def write_lammps(self, atoms, prefix='lammps'): """Write input for a LAMMPS calculation.""" self.prefix = prefix if hasattr(atoms, 'connectivities'): connectivities = atoms.connectivities else: btypes, blist = self.get_bonds(atoms) atypes, alist = self.get_angles() dtypes, dlist = self.get_dihedrals(alist, atypes) connectivities = { 'bonds': blist, 'bond types': btypes, 'angles': alist, 'angle types': atypes, 'dihedrals': dlist, 'dihedral types': dtypes} self.write_lammps_definitions(atoms, btypes, atypes, dtypes) self.write_lammps_in() self.write_lammps_atoms(atoms, connectivities) def write_lammps_in(self): with open(self.prefix + '_in', 'w') as fileobj: self._write_lammps_in(fileobj) def _write_lammps_in(self, fileobj): fileobj.write("""# LAMMPS relaxation (written by ASE) units metal atom_style full boundary p p p #boundary p p f """) fileobj.write('read_data ' + self.prefix + '_atoms\n') fileobj.write('include ' + self.prefix + '_opls\n') fileobj.write(""" kspace_style pppm 1e-5 #kspace_modify slab 3.0 neighbor 1.0 bin neigh_modify delay 0 every 1 check yes thermo 1000 thermo_style custom step temp press cpu pxx pyy pzz pxy pxz pyz ke pe etotal vol lx ly lz atoms dump 1 all xyz 1000 dump_relax.xyz dump_modify 1 sort id restart 100000 test_relax min_style fire minimize 1.0e-14 1.0e-5 100000 100000 """) # noqa: E501 def write_lammps_atoms(self, atoms, connectivities): """Write atoms input for LAMMPS""" with open(self.prefix + '_atoms', 'w') as fileobj: self._write_lammps_atoms(fileobj, atoms, connectivities) def _write_lammps_atoms(self, fileobj, atoms, connectivities): # header fileobj.write(fileobj.name + ' (by ' + str(self.__class__) + ')\n\n') fileobj.write(str(len(atoms)) + ' atoms\n') fileobj.write(str(len(atoms.types)) + ' atom types\n') blist = connectivities['bonds'] if len(blist): btypes = connectivities['bond types'] fileobj.write(str(len(blist)) + ' bonds\n') fileobj.write(str(len(btypes)) + ' bond types\n') alist = connectivities['angles'] if len(alist): atypes = connectivities['angle types'] fileobj.write(str(len(alist)) + ' angles\n') fileobj.write(str(len(atypes)) + ' angle types\n') dlist = connectivities['dihedrals'] if len(dlist): dtypes = connectivities['dihedral types'] fileobj.write(str(len(dlist)) + ' dihedrals\n') fileobj.write(str(len(dtypes)) + ' dihedral types\n') # cell p = Prism(atoms.get_cell()) xhi, yhi, zhi, xy, xz, yz = p.get_lammps_prism() fileobj.write(f'\n0.0 {xhi} xlo xhi\n') fileobj.write(f'0.0 {yhi} ylo yhi\n') fileobj.write(f'0.0 {zhi} zlo zhi\n') if p.is_skewed(): fileobj.write(f"{xy} {xz} {yz} xy xz yz\n") # atoms fileobj.write('\nAtoms\n\n') tag = atoms.get_tags() if atoms.has('molid'): molid = atoms.get_array('molid') else: molid = [1] * len(atoms) for i, r in enumerate( p.vector_to_lammps(atoms.get_positions())): atype = atoms.types[tag[i]] if len(atype) < 2: atype = atype + ' ' q = self.data['one'][atype][2] fileobj.write('%6d %3d %3d %s %s %s %s' % ((i + 1, molid[i], tag[i] + 1, q) + tuple(r))) fileobj.write(' # ' + atoms.types[tag[i]] + '\n') # velocities velocities = atoms.get_velocities() if velocities is not None: velocities = p.vector_to_lammps(atoms.get_velocities()) fileobj.write('\nVelocities\n\n') for i, v in enumerate(velocities): fileobj.write('%6d %g %g %g\n' % (i + 1, v[0], v[1], v[2])) # masses fileobj.write('\nMasses\n\n') for i, typ in enumerate(atoms.types): cs = atoms.split_symbol(typ)[0] fileobj.write('%6d %g # %s -> %s\n' % (i + 1, atomic_masses[chemical_symbols.index(cs)], typ, cs)) # bonds if blist: fileobj.write('\nBonds\n\n') for ib, bvals in enumerate(blist): fileobj.write('%8d %6d %6d %6d ' % (ib + 1, bvals[0] + 1, bvals[1] + 1, bvals[2] + 1)) if bvals[0] in btypes: fileobj.write('# ' + btypes[bvals[0]]) fileobj.write('\n') # angles if alist: fileobj.write('\nAngles\n\n') for ia, avals in enumerate(alist): fileobj.write('%8d %6d %6d %6d %6d ' % (ia + 1, avals[0] + 1, avals[1] + 1, avals[2] + 1, avals[3] + 1)) if avals[0] in atypes: fileobj.write('# ' + atypes[avals[0]]) fileobj.write('\n') # dihedrals if dlist: fileobj.write('\nDihedrals\n\n') for i, dvals in enumerate(dlist): fileobj.write('%8d %6d %6d %6d %6d %6d ' % (i + 1, dvals[0] + 1, dvals[1] + 1, dvals[2] + 1, dvals[3] + 1, dvals[4] + 1)) if dvals[0] in dtypes: fileobj.write('# ' + dtypes[dvals[0]]) fileobj.write('\n') def update_neighbor_list(self, atoms): cut = 0.5 * max(self.data['cutoffs'].values()) self.nl = NeighborList([cut] * len(atoms), skin=0, bothways=True, self_interaction=False) self.nl.update(atoms) self.atoms = atoms def get_bonds(self, atoms): """Find bonds and return them and their types""" cutoffs = CutoffList(self.data['cutoffs']) self.update_neighbor_list(atoms) types = atoms.get_types() tags = atoms.get_tags() cell = atoms.get_cell() bond_list = [] bond_types = [] for i, atom in enumerate(atoms): iname = types[tags[i]] indices, offsets = self.nl.get_neighbors(i) for j, offset in zip(indices, offsets): if j <= i: continue # do not double count jname = types[tags[j]] cut = cutoffs.value(iname, jname) if cut is None: if self.warnings > 1: print(f'Warning: cutoff {iname}-{jname} not found') continue # don't have it dist = np.linalg.norm(atom.position - atoms[j].position - np.dot(offset, cell)) if dist > cut: continue # too far away name, _val = self.bonds.name_value(iname, jname) if name is None: if self.warnings: print(f'Warning: potential {iname}-{jname} not found') continue # don't have it if name not in bond_types: bond_types.append(name) bond_list.append([bond_types.index(name), i, j]) return bond_types, bond_list def get_angles(self, atoms=None): cutoffs = CutoffList(self.data['cutoffs']) if atoms is not None: self.update_neighbor_list(atoms) else: atoms = self.atoms types = atoms.get_types() tags = atoms.get_tags() cell = atoms.get_cell() ang_list = [] ang_types = [] # center atom *-i-* for i, atom in enumerate(atoms): iname = types[tags[i]] indicesi, offsetsi = self.nl.get_neighbors(i) # search for first neighbor j-i-* for j, offsetj in zip(indicesi, offsetsi): jname = types[tags[j]] cut = cutoffs.value(iname, jname) if cut is None: continue # don't have it dist = np.linalg.norm(atom.position - atoms[j].position - np.dot(offsetj, cell)) if dist > cut: continue # too far away # search for second neighbor j-i-k for k, offsetk in zip(indicesi, offsetsi): if k <= j: continue # avoid double count kname = types[tags[k]] cut = cutoffs.value(iname, kname) if cut is None: continue # don't have it dist = np.linalg.norm(atom.position - np.dot(offsetk, cell) - atoms[k].position) if dist > cut: continue # too far away name, _val = self.angles.name_value(jname, iname, kname) if name is None: if self.warnings > 1: print( f'Warning: angles {jname}-{iname}-{kname} not ' 'found' ) continue # don't have it if name not in ang_types: ang_types.append(name) ang_list.append([ang_types.index(name), j, i, k]) return ang_types, ang_list def get_dihedrals(self, ang_types, ang_list): 'Dihedrals derived from angles.' cutoffs = CutoffList(self.data['cutoffs']) atoms = self.atoms types = atoms.get_types() tags = atoms.get_tags() cell = atoms.get_cell() dih_list = [] dih_types = [] def append(name, i, j, k, L): if name not in dih_types: dih_types.append(name) index = dih_types.index(name) if (([index, i, j, k, L] not in dih_list) and ([index, L, k, j, i] not in dih_list)): dih_list.append([index, i, j, k, L]) for angle in ang_types: L, i, j, k = angle iname = types[tags[i]] jname = types[tags[j]] kname = types[tags[k]] # search for l-i-j-k indicesi, offsetsi = self.nl.get_neighbors(i) for L, offsetl in zip(indicesi, offsetsi): if L == j: continue # avoid double count lname = types[tags[L]] cut = cutoffs.value(iname, lname) if cut is None: continue # don't have it dist = np.linalg.norm(atoms[i].position - atoms[L].position - np.dot(offsetl, cell)) if dist > cut: continue # too far away name, _val = self.dihedrals.name_value(lname, iname, jname, kname) if name is None: continue # don't have it append(name, L, i, j, k) # search for i-j-k-l indicesk, offsetsk = self.nl.get_neighbors(k) for L, offsetl in zip(indicesk, offsetsk): if L == j: continue # avoid double count lname = types[tags[L]] cut = cutoffs.value(kname, lname) if cut is None: continue # don't have it dist = np.linalg.norm(atoms[k].position - atoms[L].position - np.dot(offsetl, cell)) if dist > cut: continue # too far away name, _val = self.dihedrals.name_value(iname, jname, kname, lname) if name is None: continue # don't have it append(name, i, j, k, L) return dih_types, dih_list def write_lammps_definitions(self, atoms, btypes, atypes, dtypes): """Write force field definitions for LAMMPS.""" with open(self.prefix + '_opls', 'w') as fd: self._write_lammps_definitions(fd, atoms, btypes, atypes, dtypes) def _write_lammps_definitions(self, fileobj, atoms, btypes, atypes, dtypes): fileobj.write('# OPLS potential\n') fileobj.write('# write_lammps' + str(time.asctime(time.localtime(time.time())))) # bonds if len(btypes): fileobj.write('\n# bonds\n') fileobj.write('bond_style harmonic\n') for ib, btype in enumerate(btypes): fileobj.write('bond_coeff %6d' % (ib + 1)) for value in self.bonds.nvh[btype]: fileobj.write(' ' + str(value)) fileobj.write(' # ' + btype + '\n') # angles if len(atypes): fileobj.write('\n# angles\n') fileobj.write('angle_style harmonic\n') for ia, atype in enumerate(atypes): fileobj.write('angle_coeff %6d' % (ia + 1)) for value in self.angles.nvh[atype]: fileobj.write(' ' + str(value)) fileobj.write(' # ' + atype + '\n') # dihedrals if len(dtypes): fileobj.write('\n# dihedrals\n') fileobj.write('dihedral_style opls\n') for i, dtype in enumerate(dtypes): fileobj.write('dihedral_coeff %6d' % (i + 1)) for value in self.dihedrals.nvh[dtype]: fileobj.write(' ' + str(value)) fileobj.write(' # ' + dtype + '\n') # Lennard Jones settings fileobj.write('\n# L-J parameters\n') fileobj.write('pair_style lj/cut/coul/long 10.0 7.4' + ' # consider changing these parameters\n') fileobj.write('special_bonds lj/coul 0.0 0.0 0.5\n') data = self.data['one'] for ia, atype in enumerate(atoms.types): if len(atype) < 2: atype = atype + ' ' fileobj.write('pair_coeff ' + str(ia + 1) + ' ' + str(ia + 1)) for value in data[atype][:2]: fileobj.write(' ' + str(value)) fileobj.write(' # ' + atype + '\n') fileobj.write('pair_modify shift yes mix geometric\n') # Charges fileobj.write('\n# charges\n') for ia, atype in enumerate(atoms.types): if len(atype) < 2: atype = atype + ' ' fileobj.write('set type ' + str(ia + 1)) fileobj.write(' charge ' + str(data[atype][2])) fileobj.write(' # ' + atype + '\n')
[docs] class OPLSStructure(Atoms): """Class for representing OPLSSStructure. Parameters ---------- filename: str Extended xyz filename """ default_map = { 'BR': 'Br', 'Be': 'Be', 'C0': 'Ca', 'Li': 'Li', 'Mg': 'Mg', 'Al': 'Al', 'Ar': 'Ar'} def __init__(self, filename=None, *args, **kwargs): Atoms.__init__(self, *args, **kwargs) if filename: self.read_extended_xyz(filename) else: self.types = [] for atom in self: if atom.symbol not in self.types: self.types.append(atom.symbol) atom.tag = self.types.index(atom.symbol) def append(self, atom): """Append atom to end.""" self.extend(Atoms([atom])) def read_extended_xyz(self, fileobj, map={}): """Read extended xyz file with labeled atoms.""" atoms = read(fileobj) self.set_cell(atoms.get_cell()) self.set_pbc(atoms.get_pbc()) types = [] types_map = {} for atom, type in zip(atoms, atoms.get_array('type')): if type not in types: types_map[type] = len(types) types.append(type) atom.tag = types_map[type] self.append(atom) self.types = types # copy extra array info for name, array in atoms.arrays.items(): if name not in self.arrays: self.new_array(name, array) def split_symbol(self, string, translate=default_map): if string in translate: return translate[string], string if len(string) < 2: return string, None return string[0], string[1] def get_types(self): return self.types def colored(self, elements): res = Atoms() res.set_cell(self.get_cell()) for atom in self: elem = self.types[atom.tag] if elem in elements: elem = elements[elem] res.append(Atom(elem, atom.position)) return res def update_from_lammps_dump(self, fileobj, check=True): atoms = read(fileobj, format='lammps-dump') if len(atoms) != len(self): raise RuntimeError('Structure in ' + str(fileobj) + ' has wrong length: %d != %d' % (len(atoms), len(self))) if check: for a, b in zip(self, atoms): # check that the atom types match if a.tag + 1 != b.number: raise RuntimeError('Atoms index %d are of different ' 'type (%d != %d)' % (a.index, a.tag + 1, b.number)) self.set_cell(atoms.get_cell()) self.set_positions(atoms.get_positions()) if atoms.get_velocities() is not None: self.set_velocities(atoms.get_velocities()) # XXX what about energy and forces ??? def read_connectivities(self, fileobj, update_types=False): """Read positions, connectivities, etc. update_types: update atom types from the masses """ lines = fileobj.readlines() lines.pop(0) def next_entry(): line = lines.pop(0).strip() if len(line) > 0: lines.insert(0, line) def next_key(): while len(lines): line = lines.pop(0).strip() if len(line) > 0: lines.pop(0) return line return None next_entry() header = {} while True: line = lines.pop(0).strip() if len(line): w = line.split() if len(w) == 2: header[w[1]] = int(w[0]) else: header[w[1] + ' ' + w[2]] = int(w[0]) else: break while not lines.pop(0).startswith('Atoms'): pass lines.pop(0) natoms = len(self) positions = np.empty((natoms, 3)) for i in range(natoms): w = lines.pop(0).split() assert int(w[0]) == (i + 1) positions[i] = np.array([float(w[4 + c]) for c in range(3)]) # print(w, positions[i]) key = next_key() velocities = None if key == 'Velocities': velocities = np.empty((natoms, 3)) for i in range(natoms): w = lines.pop(0).split() assert int(w[0]) == (i + 1) velocities[i] = np.array([float(w[1 + c]) for c in range(3)]) key = next_key() if key == 'Masses': ntypes = len(self.types) masses = np.empty(ntypes) for i in range(ntypes): w = lines.pop(0).split() assert int(w[0]) == (i + 1) masses[i] = float(w[1]) if update_types: # get the elements from the masses # this ensures that we have the right elements # even when reading from a lammps dump file def newtype(element, types): if len(element) > 1: # can not extend, we are restricted to # two characters return element count = 0 for type in types: if type[0] == element: count += 1 label = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ' return (element + label[count]) symbolmap = {} typemap = {} types = [] ams = atomic_masses[:] ams[np.isnan(ams)] = 0 for i, mass in enumerate(masses): m2 = (ams - mass)**2 symbolmap[self.types[i]] = chemical_symbols[m2.argmin()] typemap[self.types[i]] = newtype( chemical_symbols[m2.argmin()], types) types.append(typemap[self.types[i]]) for atom in self: atom.symbol = symbolmap[atom.symbol] self.types = types key = next_key() def read_list(key_string, length, debug=False): if key != key_string: return [], key lst = [] while len(lines): w = lines.pop(0).split() if len(w) > length: lst.append([(int(w[1 + c]) - 1) for c in range(length)]) else: return lst, next_key() return lst, None bonds, key = read_list('Bonds', 3) angles, key = read_list('Angles', 4) dihedrals, key = read_list('Dihedrals', 5, True) self.connectivities = { 'bonds': bonds, 'angles': angles, 'dihedrals': dihedrals } if 'bonds' in header: assert len(bonds) == header['bonds'] self.connectivities['bond types'] = list( range(header['bond types'])) if 'angles' in header: assert len(angles) == header['angles'] self.connectivities['angle types'] = list( range(header['angle types'])) if 'dihedrals' in header: assert len(dihedrals) == header['dihedrals'] self.connectivities['dihedral types'] = list(range( header['dihedral types']))