Coverage for /builds/ase/ase/ase/io/opls.py: 53.80%
500 statements
« prev ^ index » next coverage.py v7.5.3, created at 2025-08-02 00:12 +0000
« prev ^ index » next coverage.py v7.5.3, created at 2025-08-02 00:12 +0000
1# fmt: off
3import time
5import numpy as np
7from ase.atom import Atom
8from ase.atoms import Atoms
9from ase.calculators.lammpsrun import Prism
10from ase.data import atomic_masses, chemical_symbols
11from ase.io import read
12from ase.neighborlist import NeighborList
15def twochar(name):
16 if len(name) > 1:
17 return name[:2]
18 else:
19 return name + ' '
22class BondData:
23 def __init__(self, name_value_hash):
24 self.nvh = name_value_hash
26 def name_value(self, aname, bname):
27 name1 = twochar(aname) + '-' + twochar(bname)
28 name2 = twochar(bname) + '-' + twochar(aname)
29 if name1 in self.nvh:
30 return name1, self.nvh[name1]
31 if name2 in self.nvh:
32 return name2, self.nvh[name2]
33 return None, None
35 def value(self, aname, bname):
36 return self.name_value(aname, bname)[1]
39class CutoffList(BondData):
40 def max(self):
41 return max(self.nvh.values())
44class AnglesData:
45 def __init__(self, name_value_hash):
46 self.nvh = name_value_hash
48 def name_value(self, aname, bname, cname):
49 for name in [
50 (twochar(aname) + '-' + twochar(bname) + '-' + twochar(cname)),
51 (twochar(cname) + '-' + twochar(bname) + '-' + twochar(aname))]:
52 if name in self.nvh:
53 return name, self.nvh[name]
54 return None, None
57class DihedralsData:
58 def __init__(self, name_value_hash):
59 self.nvh = name_value_hash
61 def name_value(self, aname, bname, cname, dname):
62 for name in [
63 (twochar(aname) + '-' + twochar(bname) + '-' +
64 twochar(cname) + '-' + twochar(dname)),
65 (twochar(dname) + '-' + twochar(cname) + '-' +
66 twochar(bname) + '-' + twochar(aname))]:
67 if name in self.nvh:
68 return name, self.nvh[name]
69 return None, None
72class OPLSff:
73 def __init__(self, fileobj=None, warnings=0):
74 self.warnings = warnings
75 self.data = {}
76 if fileobj is not None:
77 self.read(fileobj)
79 def read(self, fileobj, comments='#'):
81 def read_block(name, symlen, nvalues):
82 """Read a data block.
84 name: name of the block to store in self.data
85 symlen: length of the symbol
86 nvalues: number of values expected
87 """
89 if name not in self.data:
90 self.data[name] = {}
91 data = self.data[name]
93 def add_line():
94 line = fileobj.readline().strip()
95 if not len(line): # end of the block
96 return False
97 line = line.split('#')[0] # get rid of comments
98 if len(line) > symlen:
99 symbol = line[:symlen]
100 words = line[symlen:].split()
101 if len(words) >= nvalues:
102 if nvalues == 1:
103 data[symbol] = float(words[0])
104 else:
105 data[symbol] = [float(word)
106 for word in words[:nvalues]]
107 return True
109 while add_line():
110 pass
112 read_block('one', 2, 3)
113 read_block('bonds', 5, 2)
114 read_block('angles', 8, 2)
115 read_block('dihedrals', 11, 4)
116 read_block('cutoffs', 5, 1)
118 self.bonds = BondData(self.data['bonds'])
119 self.angles = AnglesData(self.data['angles'])
120 self.dihedrals = DihedralsData(self.data['dihedrals'])
121 self.cutoffs = CutoffList(self.data['cutoffs'])
123 def write_lammps(self, atoms, prefix='lammps'):
124 """Write input for a LAMMPS calculation."""
125 self.prefix = prefix
127 if hasattr(atoms, 'connectivities'):
128 connectivities = atoms.connectivities
129 else:
130 btypes, blist = self.get_bonds(atoms)
131 atypes, alist = self.get_angles()
132 dtypes, dlist = self.get_dihedrals(alist, atypes)
133 connectivities = {
134 'bonds': blist,
135 'bond types': btypes,
136 'angles': alist,
137 'angle types': atypes,
138 'dihedrals': dlist,
139 'dihedral types': dtypes}
141 self.write_lammps_definitions(atoms, btypes, atypes, dtypes)
142 self.write_lammps_in()
144 self.write_lammps_atoms(atoms, connectivities)
146 def write_lammps_in(self):
147 with open(self.prefix + '_in', 'w') as fileobj:
148 self._write_lammps_in(fileobj)
150 def _write_lammps_in(self, fileobj):
151 fileobj.write("""# LAMMPS relaxation (written by ASE)
153units metal
154atom_style full
155boundary p p p
156#boundary p p f
158""")
159 fileobj.write('read_data ' + self.prefix + '_atoms\n')
160 fileobj.write('include ' + self.prefix + '_opls\n')
161 fileobj.write("""
162kspace_style pppm 1e-5
163#kspace_modify slab 3.0
165neighbor 1.0 bin
166neigh_modify delay 0 every 1 check yes
168thermo 1000
169thermo_style custom step temp press cpu pxx pyy pzz pxy pxz pyz ke pe etotal vol lx ly lz atoms
171dump 1 all xyz 1000 dump_relax.xyz
172dump_modify 1 sort id
174restart 100000 test_relax
176min_style fire
177minimize 1.0e-14 1.0e-5 100000 100000
178""") # noqa: E501
180 def write_lammps_atoms(self, atoms, connectivities):
181 """Write atoms input for LAMMPS"""
182 with open(self.prefix + '_atoms', 'w') as fileobj:
183 self._write_lammps_atoms(fileobj, atoms, connectivities)
185 def _write_lammps_atoms(self, fileobj, atoms, connectivities):
186 # header
187 fileobj.write(fileobj.name + ' (by ' + str(self.__class__) + ')\n\n')
188 fileobj.write(str(len(atoms)) + ' atoms\n')
189 fileobj.write(str(len(atoms.types)) + ' atom types\n')
190 blist = connectivities['bonds']
191 if len(blist):
192 btypes = connectivities['bond types']
193 fileobj.write(str(len(blist)) + ' bonds\n')
194 fileobj.write(str(len(btypes)) + ' bond types\n')
195 alist = connectivities['angles']
196 if len(alist):
197 atypes = connectivities['angle types']
198 fileobj.write(str(len(alist)) + ' angles\n')
199 fileobj.write(str(len(atypes)) + ' angle types\n')
200 dlist = connectivities['dihedrals']
201 if len(dlist):
202 dtypes = connectivities['dihedral types']
203 fileobj.write(str(len(dlist)) + ' dihedrals\n')
204 fileobj.write(str(len(dtypes)) + ' dihedral types\n')
206 # cell
207 p = Prism(atoms.get_cell())
208 xhi, yhi, zhi, xy, xz, yz = p.get_lammps_prism()
209 fileobj.write(f'\n0.0 {xhi} xlo xhi\n')
210 fileobj.write(f'0.0 {yhi} ylo yhi\n')
211 fileobj.write(f'0.0 {zhi} zlo zhi\n')
213 if p.is_skewed():
214 fileobj.write(f"{xy} {xz} {yz} xy xz yz\n")
216 # atoms
217 fileobj.write('\nAtoms\n\n')
218 tag = atoms.get_tags()
219 if atoms.has('molid'):
220 molid = atoms.get_array('molid')
221 else:
222 molid = [1] * len(atoms)
223 for i, r in enumerate(
224 p.vector_to_lammps(atoms.get_positions())):
225 atype = atoms.types[tag[i]]
226 if len(atype) < 2:
227 atype = atype + ' '
228 q = self.data['one'][atype][2]
229 fileobj.write('%6d %3d %3d %s %s %s %s' % ((i + 1, molid[i],
230 tag[i] + 1,
231 q) + tuple(r)))
232 fileobj.write(' # ' + atoms.types[tag[i]] + '\n')
234 # velocities
235 velocities = atoms.get_velocities()
236 if velocities is not None:
237 velocities = p.vector_to_lammps(atoms.get_velocities())
238 fileobj.write('\nVelocities\n\n')
239 for i, v in enumerate(velocities):
240 fileobj.write('%6d %g %g %g\n' %
241 (i + 1, v[0], v[1], v[2]))
243 # masses
244 fileobj.write('\nMasses\n\n')
245 for i, typ in enumerate(atoms.types):
246 cs = atoms.split_symbol(typ)[0]
247 fileobj.write('%6d %g # %s -> %s\n' %
248 (i + 1,
249 atomic_masses[chemical_symbols.index(cs)],
250 typ, cs))
252 # bonds
253 if blist:
254 fileobj.write('\nBonds\n\n')
255 for ib, bvals in enumerate(blist):
256 fileobj.write('%8d %6d %6d %6d ' %
257 (ib + 1, bvals[0] + 1, bvals[1] + 1,
258 bvals[2] + 1))
259 if bvals[0] in btypes:
260 fileobj.write('# ' + btypes[bvals[0]])
261 fileobj.write('\n')
263 # angles
264 if alist:
265 fileobj.write('\nAngles\n\n')
266 for ia, avals in enumerate(alist):
267 fileobj.write('%8d %6d %6d %6d %6d ' %
268 (ia + 1, avals[0] + 1,
269 avals[1] + 1, avals[2] + 1, avals[3] + 1))
270 if avals[0] in atypes:
271 fileobj.write('# ' + atypes[avals[0]])
272 fileobj.write('\n')
274 # dihedrals
275 if dlist:
276 fileobj.write('\nDihedrals\n\n')
277 for i, dvals in enumerate(dlist):
278 fileobj.write('%8d %6d %6d %6d %6d %6d ' %
279 (i + 1, dvals[0] + 1,
280 dvals[1] + 1, dvals[2] + 1,
281 dvals[3] + 1, dvals[4] + 1))
282 if dvals[0] in dtypes:
283 fileobj.write('# ' + dtypes[dvals[0]])
284 fileobj.write('\n')
286 def update_neighbor_list(self, atoms):
287 cut = 0.5 * max(self.data['cutoffs'].values())
288 self.nl = NeighborList([cut] * len(atoms), skin=0,
289 bothways=True, self_interaction=False)
290 self.nl.update(atoms)
291 self.atoms = atoms
293 def get_bonds(self, atoms):
294 """Find bonds and return them and their types"""
295 cutoffs = CutoffList(self.data['cutoffs'])
296 self.update_neighbor_list(atoms)
298 types = atoms.get_types()
299 tags = atoms.get_tags()
300 cell = atoms.get_cell()
301 bond_list = []
302 bond_types = []
303 for i, atom in enumerate(atoms):
304 iname = types[tags[i]]
305 indices, offsets = self.nl.get_neighbors(i)
306 for j, offset in zip(indices, offsets):
307 if j <= i:
308 continue # do not double count
309 jname = types[tags[j]]
310 cut = cutoffs.value(iname, jname)
311 if cut is None:
312 if self.warnings > 1:
313 print(f'Warning: cutoff {iname}-{jname} not found')
314 continue # don't have it
315 dist = np.linalg.norm(atom.position - atoms[j].position -
316 np.dot(offset, cell))
317 if dist > cut:
318 continue # too far away
319 name, _val = self.bonds.name_value(iname, jname)
320 if name is None:
321 if self.warnings:
322 print(f'Warning: potential {iname}-{jname} not found')
323 continue # don't have it
324 if name not in bond_types:
325 bond_types.append(name)
326 bond_list.append([bond_types.index(name), i, j])
327 return bond_types, bond_list
329 def get_angles(self, atoms=None):
330 cutoffs = CutoffList(self.data['cutoffs'])
331 if atoms is not None:
332 self.update_neighbor_list(atoms)
333 else:
334 atoms = self.atoms
336 types = atoms.get_types()
337 tags = atoms.get_tags()
338 cell = atoms.get_cell()
339 ang_list = []
340 ang_types = []
342 # center atom *-i-*
343 for i, atom in enumerate(atoms):
344 iname = types[tags[i]]
345 indicesi, offsetsi = self.nl.get_neighbors(i)
347 # search for first neighbor j-i-*
348 for j, offsetj in zip(indicesi, offsetsi):
349 jname = types[tags[j]]
350 cut = cutoffs.value(iname, jname)
351 if cut is None:
352 continue # don't have it
353 dist = np.linalg.norm(atom.position - atoms[j].position -
354 np.dot(offsetj, cell))
355 if dist > cut:
356 continue # too far away
358 # search for second neighbor j-i-k
359 for k, offsetk in zip(indicesi, offsetsi):
360 if k <= j:
361 continue # avoid double count
362 kname = types[tags[k]]
363 cut = cutoffs.value(iname, kname)
364 if cut is None:
365 continue # don't have it
366 dist = np.linalg.norm(atom.position -
367 np.dot(offsetk, cell) -
368 atoms[k].position)
369 if dist > cut:
370 continue # too far away
371 name, _val = self.angles.name_value(jname, iname,
372 kname)
373 if name is None:
374 if self.warnings > 1:
375 print(
376 f'Warning: angles {jname}-{iname}-{kname} not '
377 'found'
378 )
379 continue # don't have it
380 if name not in ang_types:
381 ang_types.append(name)
382 ang_list.append([ang_types.index(name), j, i, k])
384 return ang_types, ang_list
386 def get_dihedrals(self, ang_types, ang_list):
387 'Dihedrals derived from angles.'
389 cutoffs = CutoffList(self.data['cutoffs'])
391 atoms = self.atoms
392 types = atoms.get_types()
393 tags = atoms.get_tags()
394 cell = atoms.get_cell()
396 dih_list = []
397 dih_types = []
399 def append(name, i, j, k, L):
400 if name not in dih_types:
401 dih_types.append(name)
402 index = dih_types.index(name)
403 if (([index, i, j, k, L] not in dih_list) and
404 ([index, L, k, j, i] not in dih_list)):
405 dih_list.append([index, i, j, k, L])
407 for angle in ang_types:
408 L, i, j, k = angle
409 iname = types[tags[i]]
410 jname = types[tags[j]]
411 kname = types[tags[k]]
413 # search for l-i-j-k
414 indicesi, offsetsi = self.nl.get_neighbors(i)
415 for L, offsetl in zip(indicesi, offsetsi):
416 if L == j:
417 continue # avoid double count
418 lname = types[tags[L]]
419 cut = cutoffs.value(iname, lname)
420 if cut is None:
421 continue # don't have it
422 dist = np.linalg.norm(atoms[i].position - atoms[L].position -
423 np.dot(offsetl, cell))
424 if dist > cut:
425 continue # too far away
426 name, _val = self.dihedrals.name_value(lname, iname,
427 jname, kname)
428 if name is None:
429 continue # don't have it
430 append(name, L, i, j, k)
432 # search for i-j-k-l
433 indicesk, offsetsk = self.nl.get_neighbors(k)
434 for L, offsetl in zip(indicesk, offsetsk):
435 if L == j:
436 continue # avoid double count
437 lname = types[tags[L]]
438 cut = cutoffs.value(kname, lname)
439 if cut is None:
440 continue # don't have it
441 dist = np.linalg.norm(atoms[k].position - atoms[L].position -
442 np.dot(offsetl, cell))
443 if dist > cut:
444 continue # too far away
445 name, _val = self.dihedrals.name_value(iname, jname,
446 kname, lname)
447 if name is None:
448 continue # don't have it
449 append(name, i, j, k, L)
451 return dih_types, dih_list
453 def write_lammps_definitions(self, atoms, btypes, atypes, dtypes):
454 """Write force field definitions for LAMMPS."""
455 with open(self.prefix + '_opls', 'w') as fd:
456 self._write_lammps_definitions(fd, atoms, btypes, atypes, dtypes)
458 def _write_lammps_definitions(self, fileobj, atoms, btypes, atypes,
459 dtypes):
460 fileobj.write('# OPLS potential\n')
461 fileobj.write('# write_lammps' +
462 str(time.asctime(time.localtime(time.time()))))
464 # bonds
465 if len(btypes):
466 fileobj.write('\n# bonds\n')
467 fileobj.write('bond_style harmonic\n')
468 for ib, btype in enumerate(btypes):
469 fileobj.write('bond_coeff %6d' % (ib + 1))
470 for value in self.bonds.nvh[btype]:
471 fileobj.write(' ' + str(value))
472 fileobj.write(' # ' + btype + '\n')
474 # angles
475 if len(atypes):
476 fileobj.write('\n# angles\n')
477 fileobj.write('angle_style harmonic\n')
478 for ia, atype in enumerate(atypes):
479 fileobj.write('angle_coeff %6d' % (ia + 1))
480 for value in self.angles.nvh[atype]:
481 fileobj.write(' ' + str(value))
482 fileobj.write(' # ' + atype + '\n')
484 # dihedrals
485 if len(dtypes):
486 fileobj.write('\n# dihedrals\n')
487 fileobj.write('dihedral_style opls\n')
488 for i, dtype in enumerate(dtypes):
489 fileobj.write('dihedral_coeff %6d' % (i + 1))
490 for value in self.dihedrals.nvh[dtype]:
491 fileobj.write(' ' + str(value))
492 fileobj.write(' # ' + dtype + '\n')
494 # Lennard Jones settings
495 fileobj.write('\n# L-J parameters\n')
496 fileobj.write('pair_style lj/cut/coul/long 10.0 7.4' +
497 ' # consider changing these parameters\n')
498 fileobj.write('special_bonds lj/coul 0.0 0.0 0.5\n')
499 data = self.data['one']
500 for ia, atype in enumerate(atoms.types):
501 if len(atype) < 2:
502 atype = atype + ' '
503 fileobj.write('pair_coeff ' + str(ia + 1) + ' ' + str(ia + 1))
504 for value in data[atype][:2]:
505 fileobj.write(' ' + str(value))
506 fileobj.write(' # ' + atype + '\n')
507 fileobj.write('pair_modify shift yes mix geometric\n')
509 # Charges
510 fileobj.write('\n# charges\n')
511 for ia, atype in enumerate(atoms.types):
512 if len(atype) < 2:
513 atype = atype + ' '
514 fileobj.write('set type ' + str(ia + 1))
515 fileobj.write(' charge ' + str(data[atype][2]))
516 fileobj.write(' # ' + atype + '\n')
519class OPLSStructure(Atoms):
520 default_map = {
521 'BR': 'Br',
522 'Be': 'Be',
523 'C0': 'Ca',
524 'Li': 'Li',
525 'Mg': 'Mg',
526 'Al': 'Al',
527 'Ar': 'Ar'}
529 def __init__(self, filename=None, *args, **kwargs):
530 Atoms.__init__(self, *args, **kwargs)
531 if filename:
532 self.read_extended_xyz(filename)
533 else:
534 self.types = []
535 for atom in self:
536 if atom.symbol not in self.types:
537 self.types.append(atom.symbol)
538 atom.tag = self.types.index(atom.symbol)
540 def append(self, atom):
541 """Append atom to end."""
542 self.extend(Atoms([atom]))
544 def read_extended_xyz(self, fileobj, map={}):
545 """Read extended xyz file with labeled atoms."""
546 atoms = read(fileobj)
547 self.set_cell(atoms.get_cell())
548 self.set_pbc(atoms.get_pbc())
550 types = []
551 types_map = {}
552 for atom, type in zip(atoms, atoms.get_array('type')):
553 if type not in types:
554 types_map[type] = len(types)
555 types.append(type)
556 atom.tag = types_map[type]
557 self.append(atom)
558 self.types = types
560 # copy extra array info
561 for name, array in atoms.arrays.items():
562 if name not in self.arrays:
563 self.new_array(name, array)
565 def split_symbol(self, string, translate=default_map):
567 if string in translate:
568 return translate[string], string
569 if len(string) < 2:
570 return string, None
571 return string[0], string[1]
573 def get_types(self):
574 return self.types
576 def colored(self, elements):
577 res = Atoms()
578 res.set_cell(self.get_cell())
579 for atom in self:
580 elem = self.types[atom.tag]
581 if elem in elements:
582 elem = elements[elem]
583 res.append(Atom(elem, atom.position))
584 return res
586 def update_from_lammps_dump(self, fileobj, check=True):
587 atoms = read(fileobj, format='lammps-dump')
589 if len(atoms) != len(self):
590 raise RuntimeError('Structure in ' + str(fileobj) +
591 ' has wrong length: %d != %d' %
592 (len(atoms), len(self)))
594 if check:
595 for a, b in zip(self, atoms):
596 # check that the atom types match
597 if a.tag + 1 != b.number:
598 raise RuntimeError('Atoms index %d are of different '
599 'type (%d != %d)'
600 % (a.index, a.tag + 1, b.number))
602 self.set_cell(atoms.get_cell())
603 self.set_positions(atoms.get_positions())
604 if atoms.get_velocities() is not None:
605 self.set_velocities(atoms.get_velocities())
606 # XXX what about energy and forces ???
608 def read_connectivities(self, fileobj, update_types=False):
609 """Read positions, connectivities, etc.
611 update_types: update atom types from the masses
612 """
613 lines = fileobj.readlines()
614 lines.pop(0)
616 def next_entry():
617 line = lines.pop(0).strip()
618 if len(line) > 0:
619 lines.insert(0, line)
621 def next_key():
622 while len(lines):
623 line = lines.pop(0).strip()
624 if len(line) > 0:
625 lines.pop(0)
626 return line
627 return None
629 next_entry()
630 header = {}
631 while True:
632 line = lines.pop(0).strip()
633 if len(line):
634 w = line.split()
635 if len(w) == 2:
636 header[w[1]] = int(w[0])
637 else:
638 header[w[1] + ' ' + w[2]] = int(w[0])
639 else:
640 break
642 while not lines.pop(0).startswith('Atoms'):
643 pass
644 lines.pop(0)
646 natoms = len(self)
647 positions = np.empty((natoms, 3))
648 for i in range(natoms):
649 w = lines.pop(0).split()
650 assert int(w[0]) == (i + 1)
651 positions[i] = np.array([float(w[4 + c]) for c in range(3)])
652 # print(w, positions[i])
654 key = next_key()
656 velocities = None
657 if key == 'Velocities':
658 velocities = np.empty((natoms, 3))
659 for i in range(natoms):
660 w = lines.pop(0).split()
661 assert int(w[0]) == (i + 1)
662 velocities[i] = np.array([float(w[1 + c]) for c in range(3)])
663 key = next_key()
665 if key == 'Masses':
666 ntypes = len(self.types)
667 masses = np.empty(ntypes)
668 for i in range(ntypes):
669 w = lines.pop(0).split()
670 assert int(w[0]) == (i + 1)
671 masses[i] = float(w[1])
673 if update_types:
674 # get the elements from the masses
675 # this ensures that we have the right elements
676 # even when reading from a lammps dump file
677 def newtype(element, types):
678 if len(element) > 1:
679 # can not extend, we are restricted to
680 # two characters
681 return element
682 count = 0
683 for type in types:
684 if type[0] == element:
685 count += 1
686 label = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ'
687 return (element + label[count])
689 symbolmap = {}
690 typemap = {}
691 types = []
692 ams = atomic_masses[:]
693 ams[np.isnan(ams)] = 0
694 for i, mass in enumerate(masses):
695 m2 = (ams - mass)**2
696 symbolmap[self.types[i]] = chemical_symbols[m2.argmin()]
697 typemap[self.types[i]] = newtype(
698 chemical_symbols[m2.argmin()], types)
699 types.append(typemap[self.types[i]])
700 for atom in self:
701 atom.symbol = symbolmap[atom.symbol]
702 self.types = types
704 key = next_key()
706 def read_list(key_string, length, debug=False):
707 if key != key_string:
708 return [], key
710 lst = []
711 while len(lines):
712 w = lines.pop(0).split()
713 if len(w) > length:
714 lst.append([(int(w[1 + c]) - 1) for c in range(length)])
715 else:
716 return lst, next_key()
717 return lst, None
719 bonds, key = read_list('Bonds', 3)
720 angles, key = read_list('Angles', 4)
721 dihedrals, key = read_list('Dihedrals', 5, True)
723 self.connectivities = {
724 'bonds': bonds,
725 'angles': angles,
726 'dihedrals': dihedrals
727 }
729 if 'bonds' in header:
730 assert len(bonds) == header['bonds']
731 self.connectivities['bond types'] = list(
732 range(header['bond types']))
733 if 'angles' in header:
734 assert len(angles) == header['angles']
735 self.connectivities['angle types'] = list(
736 range(header['angle types']))
737 if 'dihedrals' in header:
738 assert len(dihedrals) == header['dihedrals']
739 self.connectivities['dihedral types'] = list(range(
740 header['dihedral types']))