lammps/tools/drude/polarizer.py

746 lines
28 KiB
Python
Executable File

#!/usr/bin/env python
# polarizer.py - add Drude oscillators to LAMMPS data file.
# Agilio Padua <agilio.padua@univ-bpclermont.fr>
# Alain Dequidt <alain.dequidt@univ-bpclermont.fr>
# version 2017/02/08
import sys
import argparse
import random
from copy import deepcopy
usage = """Add Drude oscillators to LAMMPS data file.
Format of file containing specification of Drude oscillators:
# type dm/u dq/e k/(kJ/molA2) alpha/A3 thole
C3H 1.0 0.0 4184.0 2.051 2.6
...
* dm is the mass to place on the Drude particle (taken from its core),
* dq is the charge to place on the Drude particle (taken from its core),
* k is the harmonic force constant of the bond between core and Drude,
* alpha is the polarizability,
* thole is a parameter of the Thole damping function.
A Drude particle is created for each atom in the LAMMPS data file
that corresponds to an atom type given in the Drude file.
Since LAMMPS uses numbers for atom types in the data file, a comment
after each line in the Masses section has to be introduced to allow
identification of the atom types within the force field database:
Masses
1 12.011 # C3H
2 12.011 # CTO
...
This script will add new atom types, new bond types, new atoms and
new bonds to the data file.
It will also generate some commands to be included in the LAMMPS input script,
which are related to the topology and force field, namely fix drude,
pair_style and pair_coeff commands. For information on thermostating please
read the documentation of the USER-DRUDE package.
This tool can also be used to revert a Drude-polarized data file to a
non-polarizable one.
"""
# keywords of header and main sections (from data.py in Pizza.py)
hkeywords = ["atoms", "ellipsoids", "lines", "triangles", "bodies",
"bonds", "angles", "dihedrals", "impropers",
"atom types", "bond types", "angle types", "dihedral types",
"improper types", "xlo xhi", "ylo yhi", "zlo zhi", "xy xz yz"]
skeywords = [["Masses", "atom types"],
["Pair Coeffs", "atom types"],
["Bond Coeffs", "bond types"], ["Angle Coeffs", "angle types"],
["Dihedral Coeffs", "dihedral types"],
["Improper Coeffs", "improper types"],
["BondBond Coeffs", "angle types"],
["BondAngle Coeffs", "angle types"],
["MiddleBondTorsion Coeffs", "dihedral types"],
["EndBondTorsion Coeffs", "dihedral types"],
["AngleTorsion Coeffs", "dihedral types"],
["AngleAngleTorsion Coeffs", "dihedral types"],
["BondBond13 Coeffs", "dihedral types"],
["AngleAngle Coeffs", "improper types"],
["Atoms", "atoms"], ["Velocities", "atoms"],
["Ellipsoids", "ellipsoids"],
["Lines", "lines"], ["Triangles", "triangles"],
["Bodies", "bodies"],
["Bonds", "bonds"],
["Angles", "angles"], ["Dihedrals", "dihedrals"],
["Impropers", "impropers"], ["Molecules", "atoms"]]
def massline(att):
return "{0:4d} {1:8.3f} # {2}\n".format(att['id'], att['m'], att['type'])
def bdtline(bdt):
return "{0:4d} {1:12.6f} {2:12.6f} {3}\n".format(bdt['id'], bdt['k'],
bdt['r0'], bdt['note'])
def atomline(at):
return "{0:7d} {1:7d} {2:4d} {3:8.4f} {4:13.6e} {5:13.6e} {6:13.6e} "\
" {7}\n".format(at['n'], at['mol'], at['id'], at['q'],
at['x'], at['y'], at['z'], at['note'])
def bondline(bd):
return "{0:7d} {1:4d} {2:7d} {3:7d} {4}\n".format(bd['n'], bd['id'],
bd['i'], bd['j'], bd['note'])
def velline(at):
return "{0:7d} {1:13.6e} {2:13.6e} {3:13.6e} \n".format(at['n'],
at['vx'], at['vy'], at['vz'])
# --------------------------------------
class Data(object):
def __init__(self, datafile):
'''read LAMMPS data file (from data.py in Pizza.py)'''
# for extract method
self.atomtypes = []
self.bondtypes = []
self.atoms = []
self.bonds = []
self.idmap = {}
self.nselect = 1
f = open(datafile, "r")
self.title = f.readline()
self.names = {}
headers = {}
while 1:
line = f.readline().strip()
if '#' in line:
line = line[:line.index('#')].strip()
if len(line) == 0:
continue
found = 0
for keyword in hkeywords:
if keyword in line:
found = 1
words = line.split()
if keyword == "xlo xhi" or keyword == "ylo yhi" or \
keyword == "zlo zhi":
headers[keyword] = (float(words[0]), float(words[1]))
elif keyword == "xy xz yz":
headers[keyword] = \
(float(words[0]), float(words[1]), float(words[2]))
else:
headers[keyword] = int(words[0])
if not found:
break
sections = {}
while 1:
if len(line) > 0:
found = 0
for pair in skeywords:
keyword, length = pair[0], pair[1]
if keyword == line:
found = 1
if length not in headers:
raise RuntimeError("data section {} "\
"has no matching header value".format(line))
f.readline()
list_ = []
for _ in range(headers[length]):
list_.append(f.readline())
sections[keyword] = list_
if not found:
raise RuntimeError("invalid section {} in data"\
" file".format(line))
#f.readline()
line = f.readline()
if not line:
break
if '#' in line:
line = line[:line.index('#')]
line = line.strip()
f.close()
self.headers = headers
self.sections = sections
def write(self, filename):
'''write out a LAMMPS data file (from data.py in Pizza.py)'''
with open(filename, "w") as f:
f.write(self.title + '\n')
for keyword in hkeywords:
if keyword in self.headers:
if keyword == "xlo xhi" or keyword == "ylo yhi" or \
keyword == "zlo zhi":
f.write("{0:f} {1:f} {2}\n".format(
self.headers[keyword][0],
self.headers[keyword][1], keyword))
elif keyword == "xy xz yz":
f.write("{0:f} {1:f} {2:f} {3}\n".format(
self.headers[keyword][0],
self.headers[keyword][1],
self.headers[keyword][2], keyword))
else:
f.write("{0:d} {1}\n".format(self.headers[keyword],
keyword))
for pair in skeywords:
keyword = pair[0]
if keyword in self.sections:
f.write("\n{}\n\n".format(keyword))
for line in self.sections[keyword]:
f.write(line)
def extract_nonpol(self):
"""extract atom and bond info from nonpolarizable data"""
# extract atom IDs
missinglabels = False
for line in self.sections['Masses']:
tok = line.split()
if len(tok) < 4:
print("error: missing type for atom ID " + tok[0] +
" in Masses section")
missinglabels = True
continue
atomtype = {}
atomtype['id'] = int(tok[0])
atomtype['m'] = float(tok[1])
atomtype['type'] = tok[3]
self.atomtypes.append(atomtype)
if missinglabels:
sys.exit(0)
# extract atom registers
for line in self.sections['Atoms']:
tok = line.split()
atom = {}
atom['n'] = int(tok[0])
atom['mol'] = int(tok[1])
atom['id'] = int(tok[2])
atom['q'] = float(tok[3])
atom['x'] = float(tok[4])
atom['y'] = float(tok[5])
atom['z'] = float(tok[6])
#atom['note'] = ''.join([s + ' ' for s in tok[7:]]).strip()
atom['note'] = ' '.join(tok[7:])
self.atoms.append(atom)
self.idmap[atom['n']] = atom
if 'Velocities' in self.sections:
for line in self.sections['Velocities']:
tok = line.split()
atom = self.idmap[int(tok[0])]
atom['vx'] = float(tok[1])
atom['vy'] = float(tok[2])
atom['vz'] = float(tok[3])
def polarize(self, drude):
"""add Drude particles"""
if 'Pair Coeffs' in self.sections:
raise RuntimeError("cannot polarize a data with Pair Coeffs")
self.extract_nonpol()
natom = self.headers['atoms']
nbond = self.headers['bonds']
nattype = self.headers['atom types']
nbdtype = self.headers['bond types']
# create new atom types (IDs) for Drude particles and modify cores
newattypes = []
for att in self.atomtypes:
att['dflag'] = 'n'
for ddt in drude.types:
if ddt['type'] == att['type']:
nattype += 1
newid = {}
newid['id'] = ddt['id'] = nattype
newid['m'] = ddt['dm']
att['m'] -= ddt['dm']
# label drude particles and cores
att['dflag'] = 'c'
newid['dflag'] = 'd'
newid['type'] = att['type'] + ' DP'
att['type'] += ' DC'
ddt['type'] += ' DC'
newattypes.append(newid)
break
self.headers['atom types'] += len(newattypes)
self.sections['Masses'] = []
for att in self.atomtypes + newattypes:
self.sections['Masses'].append(massline(att))
# create new bond types for core-drude bonds
newbdtypes = []
for att in self.atomtypes:
for ddt in drude.types:
if ddt['type'] == att['type']:
nbdtype += 1
newbdtype = {}
newbdtype['id'] = ddt['bdid'] = nbdtype
newbdtype['k'] = ddt['k']
newbdtype['r0'] = 0.0
newbdtype['note'] = '# ' + ddt['type'] + '-DP'
newbdtypes.append(newbdtype)
break
self.headers['bond types'] += len(newbdtypes)
for bdt in newbdtypes:
self.sections['Bond Coeffs'].append(bdtline(bdt))
# create new atoms for Drude particles and new bonds with their cores
random.seed(123)
newatoms = []
newbonds = []
for atom in self.atoms:
atom['dflag'] = '' # [c]ore, [d]rude, [n]on-polarizable
atom['dd'] = 0 # partner drude or core
for att in self.atomtypes:
if att['id'] == atom['id']:
break
for ddt in drude.types:
if ddt['type'] == att['type']:
natom += 1
newatom = deepcopy(atom)
newatom['n'] = natom
self.idmap[natom] = newatom
newatom['id'] = ddt['id']
newatom['q'] = ddt['dq']
newatom['note'] = atom['note']
if '#' not in newatom['note']:
newatom['note'] += ' #'
newatom['note'] += ' DP'
newatom['dflag'] = 'd'
newatom['dd'] = atom['n']
# avoid superposition of cores and Drudes
newatom['x'] += 0.1 * (random.random() - 0.5)
newatom['y'] += 0.1 * (random.random() - 0.5)
newatom['z'] += 0.1 * (random.random() - 0.5)
if 'Velocities' in self.sections:
newatom['vx'] = atom['vx']
newatom['vy'] = atom['vy']
newatom['vz'] = atom['vz']
newatoms.append(newatom)
atom['q'] -= ddt['dq']
atom['dflag'] = 'c'
atom['dd'] = natom
if '#' not in atom['note']:
atom['note'] += ' #'
atom['note'] += ' DC'
nbond += 1
newbond = {}
newbond['n'] = nbond
newbond['id'] = ddt['bdid']
newbond['i'] = atom['n']
newbond['j'] = newatom['n']
newbond['note'] = '# ' + ddt['type'] + '-DP'
newbonds.append(newbond)
break
self.headers['atoms'] += len(newatoms)
self.headers['bonds'] += len(newbonds)
self.sections['Atoms'] = []
for atom in self.atoms + newatoms:
self.sections['Atoms'].append(atomline(atom))
for bond in newbonds:
self.sections['Bonds'].append(bondline(bond))
if 'Velocities' in self.sections:
self.sections['Velocities'] = []
for atom in self.atoms + newatoms:
self.sections['Velocities'].append(velline(atom))
# update list of atom IDs
for att in newattypes:
self.atomtypes.append(att)
def extract_pol(self, drude):
"""extract atom, drude, bonds info from polarizable data"""
# extract atom IDs
for line in self.sections['Masses']:
tok = line.split()
atomtype = {}
atomtype['id'] = int(tok[0])
atomtype['m'] = float(tok[1])
if len(tok) >= 4:
atomtype['type'] = tok[3]
atomtype['dflag'] = 'n'
if tok[-1] == "DC":
atomtype['dflag'] = 'c'
elif tok[-1] == "DP":
atomtype['dflag'] = 'd'
print(atomtype['dflag'])
else:
raise RuntimeError("comments in Masses section required "\
"to identify cores (DC) and Drudes (DP)")
self.atomtypes.append(atomtype)
# extract bond type data
for line in self.sections['Bond Coeffs']:
tok = line.split()
bondtype = {}
bondtype['id'] = int(tok[0])
bondtype['k'] = float(tok[1])
bondtype['r0'] = float(tok[2])
bondtype['note'] = ''.join([s + ' ' for s in tok[3:]]).strip()
self.bondtypes.append(bondtype)
# extract atom registers
for line in self.sections['Atoms']:
tok = line.split()
atom = {}
atom['n'] = int(tok[0])
atom['mol'] = int(tok[1])
atom['id'] = int(tok[2])
atom['q'] = float(tok[3])
atom['x'] = float(tok[4])
atom['y'] = float(tok[5])
atom['z'] = float(tok[6])
# atom['note'] = ''.join([s + ' ' for s in tok[7:-1]]).strip()
if tok[-1] == 'DC':
atom['note'] = ' '.join(tok[7:-1])
else:
atom['note'] = ' '.join(tok[7:])
self.atoms.append(atom)
self.idmap[atom['n']] = atom
if 'Velocities' in self.sections:
for line in self.sections['Velocities']:
tok = line.split()
atom = self.idmap[int(tok[0])]
atom['vx'] = float(tok[1])
atom['vy'] = float(tok[2])
atom['vz'] = float(tok[3])
# extract bond data
for line in self.sections['Bonds']:
tok = line.split()
bond = {}
bond['n'] = int(tok[0])
bond['id'] = int(tok[1])
bond['i'] = int(tok[2])
bond['j'] = int(tok[3])
bond['note'] = ''.join([s + ' ' for s in tok[4:]]).strip()
self.bonds.append(bond)
if 'Velocities' in self.sections:
for line in self.sections['Velocities']:
tok = line.split()
atom = self.idmap[int(tok[0])]
atom['vx'] = float(tok[1])
atom['vy'] = float(tok[2])
atom['vz'] = float(tok[3])
def depolarize(self, drude):
"""remove Drude particles"""
self.extract_pol(drude)
atom_tp_map = {}
bond_tp_map = {}
atom_map = {}
bond_map = {}
q = {}
atom_tp = {}
m = {}
for att in self.atomtypes:
if att['dflag'] != 'd':
atom_tp_map[att['id']] = len(atom_tp_map) + 1
m[att['id']] = att['m']
print(atom_tp_map)
for atom in self.atoms:
if atom['id'] in atom_tp_map:
atom_map[atom['n']] = len(atom_map) + 1
q[atom['n']] = atom['q']
atom_tp[atom['n']] = atom['id']
for bond in self.bonds:
if bond['i'] in atom_map and bond['j'] in atom_map:
bond_map[bond['n']] = len(bond_map) + 1
if bond['id'] not in bond_tp_map:
bond_tp_map[bond['id']] = len(bond_tp_map) + 1
else:
if bond['i'] in atom_map:
q[bond['i']] += q[bond['j']]
if atom_tp[bond['j']] in m:
m[atom_tp[bond['i']]] += m.pop(atom_tp[bond['j']])
else:
q[bond['j']] += q[bond['i']]
if atom_tp[bond['i']] in m:
m[atom_tp[bond['j']]] += m.pop(atom_tp[bond['i']])
size = len(self.atomtypes)
for iatom_tp in reversed(range(size)):
att = self.atomtypes[iatom_tp]
if att['id'] not in atom_tp_map:
del self.atomtypes[iatom_tp]
else:
att['m'] = m[att['id']]
att['id'] = atom_tp_map[att['id']]
size = len(self.bondtypes)
for ibond_tp in reversed(range(size)):
bdt = self.bondtypes[ibond_tp]
if bdt['id'] not in bond_tp_map:
del self.bondtypes[ibond_tp]
else:
bdt['id'] = bond_tp_map[bdt['id']]
size = len(self.atoms)
for iatom in reversed(range(size)):
atom = self.atoms[iatom]
if atom['n'] not in atom_map:
del self.atoms[iatom]
else:
atom['q'] = q[atom['n']]
atom['n'] = atom_map[atom['n']]
atom['id'] = atom_tp_map[atom['id']]
size = len(self.bonds)
for ibond in reversed(range(size)):
bond = self.bonds[ibond]
if bond['n'] not in bond_map:
del self.bonds[ibond]
else:
bond['n'] = bond_map[bond['n']]
bond['id'] = bond_tp_map[bond['id']]
bond['i'] = atom_map[bond['i']]
bond['j'] = atom_map[bond['j']]
self.sections['Atoms'] = []
for atom in self.atoms:
self.sections['Atoms'].append(atomline(atom))
self.headers['atoms'] = len(self.atoms)
self.sections['Masses'] = []
for att in self.atomtypes:
self.sections['Masses'].append(massline(att))
self.headers['atom types'] = len(self.atomtypes)
self.sections['Bonds'] = []
for bond in self.bonds:
self.sections['Bonds'].append(bondline(bond))
self.headers['bonds'] = len(self.bonds)
self.sections['Bond Coeffs'] = []
for bdt in self.bondtypes:
self.sections['Bond Coeffs'].append(bdtline(bdt))
self.headers['bond types'] = len(self.bondtypes)
if 'Velocities' in self.sections:
self.sections['Velocities'] = []
for atom in self.atoms:
self.sections['Velocities'].append(velline(atom))
def lmpscript(self, drude, outfile, thole = 2.6, cutoff = 12.0):
"""print lines for input script, including pair_style thole"""
pairfile = "pair-drude.lmp"
dfound = False
for att in self.atomtypes:
if att['dflag'] == 'd':
dfound = True
break
if not dfound:
print("# No polarizable atoms found.")
return
print("# Commands to include in the LAMMPS input script\n")
print("# adapt the pair_style command as needed")
print("pair_style hybrid/overlay ... coul/long/cs {0:.1f} "\
"thole {1:.3f} {0:.1f}\n".format(cutoff, thole))
print("# data file with Drude oscillators added")
print("read_data {0}\n".format(outfile))
print("# pair interactions with Drude particles written to file")
print("# Thole damping recommended if more than 1 Drude per molecule")
print("include {0}\n".format(pairfile))
with open(pairfile, "w") as f:
f.write("# interactions involving Drude particles\n")
f.write("pair_coeff * {0:3d}* coul/long/cs\n".format(att['id']))
f.write("# Thole damping if more than 1 Drude per molecule\n")
# Thole parameters for I,J pairs
ifound = False
for atti in self.atomtypes:
itype = atti['type'].split()[0]
for ddt in drude.types:
dtype = ddt['type'].split()[0]
if dtype == itype:
alphai = ddt['alpha']
tholei = ddt['thole']
ifound = True
break
jfound = False
for attj in self.atomtypes:
if attj['id'] < atti['id']:
continue
jtype = attj['type'].split()[0]
for ddt in drude.types:
dtype = ddt['type'].split()[0]
if dtype == jtype:
alphaj = ddt['alpha']
tholej = ddt['thole']
jfound = True
break
if ifound and jfound:
alphaij = (alphai * alphaj)**0.5
tholeij = (tholei + tholej) / 2.0
if tholeij == thole:
f.write("pair_coeff {0:4} {1:4} thole "\
"{2:7.3f}\n".format(atti['id'], attj['id'],
alphaij))
else:
f.write("pair_coeff {0:4} {1:4} thole {2:7.3f} "\
"{3:7.3f}\n".format(atti['id'],attj['id'],
alphaij, tholeij))
jfound = False
ifound = False
print("# atom groups convenient for thermostats (see package "
"documentation), etc.")
gatoms = gcores = gdrudes = ""
for att in self.atomtypes:
if att['dflag'] != 'd':
gatoms += " {0}".format(att['id'])
if att['dflag'] == 'c':
gcores += " {0}".format(att['id'])
if att['dflag'] == 'd':
gdrudes += " {0}".format(att['id'])
print("group ATOMS type" + gatoms)
print("group CORES type" + gcores)
print("group DRUDES type" + gdrudes)
print("")
print("# flag for each atom type: [C]ore, [D]rude, [N]on-polarizable")
drudetypes = ""
for att in self.atomtypes:
drudetypes += " {0}".format(att['dflag'].upper())
print("fix DRUDE all drude" + drudetypes)
print("")
print("# ATTENTION!")
print("# * read_data may need 'extra/special/per/atom' keyword, "
"LAMMPS will exit with a message.")
print("# * If using fix shake the group-ID must not include "
"Drude particles.")
print("# Use group ATOMS for example.")
print("# * Give all I<=J pair interactions, no mixing.")
print("# * Pair style coul/long/cs from CORESHELL package is used "\
"for interactions")
print("# of Drude particles. Alternatively pair lj/cut/thole/long "\
"could be used,")
print("# avoiding hybrid/overlay and allowing mixing. See doc "\
"pages.")
# --------------------------------------
kcal = 4.184 # kJ
eV = 96.485 # kJ/mol
fpe0 = 0.000719756 # (4 Pi eps0) in e^2/(kJ/mol A)
class Drude(object):
"""specification of drude oscillator types"""
def __init__(self, drudefile, polar = '', positive = False, metal = False):
self.types = []
with open(drudefile, "r") as f:
for line in f:
line = line.strip()
if line.startswith('#') or len(line) == 0:
continue
tok = line.split()
drude = {}
drude['type'] = tok[0]
drude['dm'] = float(tok[1])
dq = float(tok[2])
k = float(tok[3])
drude['alpha'] = alpha = float(tok[4])
drude['thole'] = float(tok[5])
if polar == 'q':
dq = (fpe0 * k * alpha)**0.5
elif polar == 'k':
k = dq*dq / (fpe0 * alpha)
if positive:
drude['dq'] = abs(dq)
else:
drude['dq'] = -abs(dq)
if metal:
drude['k'] = k / (2.0 * eV)
else:
drude['k'] = k / (2.0 * kcal)
self.types.append(drude)
# --------------------------------------
def main():
parser = argparse.ArgumentParser(description = usage,
formatter_class = argparse.RawTextHelpFormatter)
parser.add_argument('-f', '--ffdrude', default = 'drude.dff',
help = 'Drude parameter file (default: drude.dff)')
parser.add_argument('-t', '--thole', type = float, default = 2.6,
help = 'Thole damping parameter (default: 2.6)')
parser.add_argument('-c', '--cutoff', type = float, default = 12.0,
help = 'distance cutoff/A (default: 12.0)')
parser.add_argument('-q', '--qcalc', action = 'store_true',
help = 'Drude charges calculated from polarisability '\
'(default: q value from parameter file)')
parser.add_argument('-k', '--kcalc', action = 'store_true',
help = 'Drude force constants calculated from '\
'polarisability (default: k value from parameter file)')
parser.add_argument('-p', '--positive', action = 'store_true',
help = 'Drude particles have positive charge '\
'(default: negative charge)')
parser.add_argument('-m', '--metal', action = 'store_true',
help = 'LAMMPS metal units (default: real units)')
parser.add_argument('-d', '--depolarize', action = 'store_true',
help = 'remove Drude dipole polarization from '\
'LAMMPS data file')
parser.add_argument('infile', help = 'input LAMMPS data file')
parser.add_argument('outfile', help = 'output LAMMPS data file')
args = parser.parse_args()
if args.qcalc:
polar = 'q'
elif args.kcalc:
polar = 'p'
else:
polar = ''
data = Data(args.infile)
drude = Drude(args.ffdrude, polar, args.positive, args.metal)
if not args.depolarize:
data.polarize(drude)
data.lmpscript(drude, args.outfile, args.thole, args.cutoff)
else:
data.depolarize(drude)
data.write(args.outfile)
if __name__ == '__main__':
main()