Coverage for /builds/ase/ase/ase/calculators/lammpsrun.py : 81.89%

Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1# lammps.py (2011/03/29)
2# An ASE calculator for the LAMMPS classical MD code available from
3# http://lammps.sandia.gov/
4# The environment variable ASE_LAMMPSRUN_COMMAND must be defined to point to the
5# LAMMPS binary.
6#
7# Copyright (C) 2009 - 2011 Joerg Meyer, joerg.meyer@ch.tum.de
8#
9# This library is free software; you can redistribute it and/or
10# modify it under the terms of the GNU Lesser General Public
11# License as published by the Free Software Foundation; either
12# version 2.1 of the License, or (at your option) any later version.
13#
14# This library is distributed in the hope that it will be useful,
15# but WITHOUT ANY WARRANTY; without even the implied warranty of
16# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
17# Lesser General Public License for more details.
18#
19# You should have received a copy of the GNU Lesser General Public
20# License along with this file; if not, write to the Free Software
21# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301
22# USA or see <http://www.gnu.org/licenses/>.
25import os
26import shutil
27import shlex
28from subprocess import Popen, PIPE, TimeoutExpired
29from threading import Thread
30from re import compile as re_compile, IGNORECASE
31from tempfile import mkdtemp, NamedTemporaryFile, mktemp as uns_mktemp
32import inspect
33import warnings
34from typing import Dict, Any
35import numpy as np
37from ase.parallel import paropen
38from ase.calculators.calculator import Calculator
39from ase.calculators.calculator import all_changes
40from ase.data import chemical_symbols
41from ase.data import atomic_masses
42from ase.io.lammpsdata import write_lammps_data
43from ase.io.lammpsrun import read_lammps_dump
44from ase.calculators.lammps import Prism
45from ase.calculators.lammps import write_lammps_in
46from ase.calculators.lammps import CALCULATION_END_MARK
47from ase.calculators.lammps import convert
49__all__ = ["LAMMPS"]
52class LAMMPS(Calculator):
53 """The LAMMPS calculators object
55 files: list
56 List of files typically containing relevant potentials for the
57 calculation
58 parameters: dict
59 Dictionary of settings to be passed into the input file for calculation.
60 specorder: list
61 Within LAAMPS, atoms are identified by an integer value starting from 1.
62 This variable allows the user to define the order of the indices
63 assigned to the atoms in the calculation, with the default
64 if not given being alphabetical
65 keep_tmp_files: bool
66 Retain any temporary files created. Mostly useful for debugging.
67 tmp_dir: str
68 path/dirname (default None -> create automatically).
69 Explicitly control where the calculator object should create
70 its files. Using this option implies 'keep_tmp_files'
71 no_data_file: bool
72 Controls whether an explicit data file will be used for feeding
73 atom coordinates into lammps. Enable it to lessen the pressure on
74 the (tmp) file system. THIS OPTION MIGHT BE UNRELIABLE FOR CERTAIN
75 CORNER CASES (however, if it fails, you will notice...).
76 keep_alive: bool
77 When using LAMMPS as a spawned subprocess, keep the subprocess
78 alive (but idling when unused) along with the calculator object.
79 always_triclinic: bool
80 Force use of a triclinic cell in LAMMPS, even if the cell is
81 a perfect parallelepiped.
83 **Example**
85Provided that the respective potential file is in the working directory, one
86can simply run (note that LAMMPS needs to be compiled to work with EAM
87potentials)
89::
91 from ase import Atom, Atoms
92 from ase.build import bulk
93 from ase.calculators.lammpsrun import LAMMPS
95 parameters = {'pair_style': 'eam/alloy',
96 'pair_coeff': ['* * NiAlH_jea.eam.alloy H Ni']}
98 files = ['NiAlH_jea.eam.alloy']
100 Ni = bulk('Ni', cubic=True)
101 H = Atom('H', position=Ni.cell.diagonal()/2)
102 NiH = Ni + H
104 lammps = LAMMPS(parameters=parameters, files=files)
106 NiH.calc = lammps
107 print("Energy ", NiH.get_potential_energy())
109(Remember you also need to set the environment variable
110``$ASE_LAMMPSRUN_COMMAND``)
112 """
114 name = "lammpsrun"
115 implemented_properties = ["energy", "free_energy", "forces", "stress",
116 "energies"]
118 # parameters to choose options in LAMMPSRUN
119 ase_parameters: Dict[str, Any] = dict(
120 specorder=None,
121 atorder=True,
122 always_triclinic=False,
123 keep_alive=True,
124 keep_tmp_files=False,
125 no_data_file=False,
126 tmp_dir=None,
127 files=[], # usually contains potential parameters
128 verbose=False,
129 write_velocities=False,
130 binary_dump=True, # bool - use binary dump files (full
131 # precision but long long ids are casted to
132 # double)
133 lammps_options="-echo log -screen none -log /dev/stdout",
134 trajectory_out=None, # file object, if is not None the
135 # trajectory will be saved in it
136 )
138 # parameters forwarded to LAMMPS
139 lammps_parameters = dict(
140 boundary=None, # bounadry conditions styles
141 units="metal", # str - Which units used; some potentials
142 # require certain units
143 atom_style="atomic",
144 special_bonds=None,
145 # potential informations
146 pair_style="lj/cut 2.5",
147 pair_coeff=["* * 1 1"],
148 masses=None,
149 pair_modify=None,
150 # variables controlling the output
151 thermo_args=[
152 "step", "temp", "press", "cpu", "pxx", "pyy", "pzz",
153 "pxy", "pxz", "pyz", "ke", "pe", "etotal", "vol", "lx",
154 "ly", "lz", "atoms", ],
155 dump_properties=["id", "type", "x", "y", "z", "vx", "vy",
156 "vz", "fx", "fy", "fz", ],
157 dump_period=1, # period of system snapshot saving (in MD steps)
158 )
160 default_parameters = dict(ase_parameters, **lammps_parameters)
162 # legacy parameter persist, when the 'parameters' dictinary is manipulated
163 # from the outside. All others are rested to the default value
164 legacy_parameters = [
165 "specorder",
166 "dump_period",
167 "always_triclinic",
168 "keep_alive",
169 "keep_tmp_files",
170 "tmp_dir",
171 "parameters",
172 "no_data_file",
173 "files",
174 "write_velocities",
175 "trajectory_out",
176 ]
178 legacy_parameters_map = {"_custom_thermo_args": "thermo_args"}
180 legacy_warn_string = "You are using an "
181 legacy_warn_string += "old syntax to set '{}'.\n"
182 legacy_warn_string += "Please use {}.set().".format(name.upper())
184 def __init__(self, label="lammps", **kwargs):
185 # "Parameters" used to be the dictionary with all parameters forwarded
186 # to lammps. This clashes with the implementation in Calculator to
187 # reload an old one. Trying to catch both cases to not break old
188 # scripts.
189 if "parameters" in kwargs:
190 old_parameters = kwargs["parameters"]
191 if isinstance(old_parameters, dict):
192 warnings.warn(self.legacy_warn_string.format("parameters"))
193 del kwargs["parameters"]
194 else:
195 old_parameters = None
197 Calculator.__init__(self, label=label, **kwargs)
199 if old_parameters and isinstance(old_parameters, dict):
200 self.set(**old_parameters)
202 self.prism = None
203 self.calls = 0
204 self.forces = None
205 # thermo_content contains data "written by" thermo_style.
206 # It is a list of dictionaries, each dict (one for each line
207 # printed by thermo_style) contains a mapping between each
208 # custom_thermo_args-argument and the corresponding
209 # value as printed by lammps. thermo_content will be
210 # re-populated by the read_log method.
211 self.thermo_content = []
213 if self.parameters.tmp_dir is not None:
214 # If tmp_dir is pointing somewhere, don't remove stuff!
215 self.parameters.keep_tmp_files = True
216 self._lmp_handle = None # To handle the lmp process
218 if self.parameters.tmp_dir is None:
219 self.parameters.tmp_dir = mkdtemp(prefix="LAMMPS-")
220 else:
221 self.parameters.tmp_dir = os.path.realpath(self.parameters.tmp_dir)
222 if not os.path.isdir(self.parameters.tmp_dir):
223 os.mkdir(self.parameters.tmp_dir, 0o755)
225 for f in self.parameters.files:
226 shutil.copy(
227 f, os.path.join(self.parameters.tmp_dir, os.path.basename(f))
228 )
230 def get_lammps_command(self):
231 cmd = self.parameters.get('command')
232 if cmd is None:
233 envvar = 'ASE_{}_COMMAND'.format(self.name.upper())
234 cmd = os.environ.get(envvar)
236 if cmd is None:
237 cmd = 'lammps'
239 opts = self.parameters.get('lammps_options')
241 if opts is not None:
242 cmd = '{} {}'.format(cmd, opts)
244 return cmd
246 def __setattr__(self, key, value):
247 """Catch attribute sets to emulate legacy behavior.
249 Old LAMMPSRUN allows to just override the parameters
250 dictionary. "Modern" ase calculators can assume that default
251 parameters are always set, overrides of the
252 'parameters'-dictionary have to be caught and the default
253 parameters need to be added first. A check refuses to set
254 calculator attributes if they are unknown and set outside the
255 '__init__' functions.
256 """
257 # !TODO: remove and break somebody's code (e.g. the test examples)
258 if (
259 key == "parameters"
260 and value is not None
261 and self.parameters is not None
262 ):
263 temp_dict = self.get_default_parameters()
264 if self.parameters:
265 for l_key in self.legacy_parameters:
266 try:
267 temp_dict[l_key] = self.parameters[l_key]
268 except KeyError:
269 pass
270 temp_dict.update(value)
271 value = temp_dict
272 if key in self.legacy_parameters and key != "parameters":
273 warnings.warn(self.legacy_warn_string.format(key))
274 self.set(**{key: value})
275 elif key in self.legacy_parameters_map:
276 warnings.warn(
277 self.legacy_warn_string.format(
278 "{} for {}".format(self.legacy_parameters_map[key], key)
279 )
280 )
281 self.set(**{self.legacy_parameters_map[key]: value})
282 # Catch setting none-default attributes
283 # one test was assigning an useless Attribute, but it still worked
284 # because the assigned object was before manipulation already handed
285 # over to the calculator (10/2018)
286 elif hasattr(self, key) or inspect.stack()[1][3] == "__init__":
287 Calculator.__setattr__(self, key, value)
288 else:
289 raise AttributeError("Setting unknown Attribute '{}'".format(key))
291 def __getattr__(self, key):
292 """Corresponding getattribute-function to emulate legacy behavior.
293 """
294 if key in self.legacy_parameters and key != "parameters":
295 return self.parameters[key]
296 if key in self.legacy_parameters_map:
297 return self.parameters[self.legacy_parameters_map[key]]
298 return object.__getattribute__(self, key)
300 def clean(self, force=False):
302 self._lmp_end()
304 if not self.parameters.keep_tmp_files or force:
305 shutil.rmtree(self.parameters.tmp_dir)
307 def check_state(self, atoms, tol=1.0e-10):
308 # Transforming the unit cell to conform to LAMMPS' convention for
309 # orientation (c.f. https://lammps.sandia.gov/doc/Howto_triclinic.html)
310 # results in some precision loss, so we use bit larger tolerance than
311 # machine precision here. Note that there can also be precision loss
312 # related to how many significant digits are specified for things in
313 # the LAMMPS input file.
314 return Calculator.check_state(self, atoms, tol)
316 def calculate(self, atoms=None, properties=None, system_changes=None):
317 if properties is None:
318 properties = self.implemented_properties
319 if system_changes is None:
320 system_changes = all_changes
321 Calculator.calculate(self, atoms, properties, system_changes)
322 self.run()
324 def _lmp_alive(self):
325 # Return True if this calculator is currently handling a running
326 # lammps process
327 return self._lmp_handle and not isinstance(
328 self._lmp_handle.poll(), int
329 )
331 def _lmp_end(self):
332 # Close lammps input and wait for lammps to end. Return process
333 # return value
334 if self._lmp_alive():
335 # !TODO: handle lammps error codes
336 try:
337 self._lmp_handle.communicate(timeout=5)
338 except TimeoutExpired:
339 self._lmp_handle.kill()
340 self._lmp_handle.communicate()
341 err = self._lmp_handle.poll()
342 assert err is not None
343 return err
345 def set_missing_parameters(self):
346 """Verify that all necessary variables are set.
347 """
348 symbols = self.atoms.get_chemical_symbols()
349 # If unspecified default to atom types in alphabetic order
350 if not self.parameters.specorder:
351 self.parameters.specorder = sorted(set(symbols))
353 # !TODO: handle cases were setting masses actual lead to errors
354 if not self.parameters.masses:
355 self.parameters.masses = []
356 for type_id, specie in enumerate(self.parameters.specorder):
357 mass = atomic_masses[chemical_symbols.index(specie)]
358 self.parameters.masses += [
359 "{0:d} {1:f}".format(type_id + 1, mass)
360 ]
362 # set boundary condtions
363 if not self.parameters.boundary:
364 b_str = " ".join(["fp"[int(x)] for x in self.atoms.get_pbc()])
365 self.parameters.boundary = b_str
367 def run(self, set_atoms=False):
368 # !TODO: split this function
369 """Method which explicitly runs LAMMPS."""
370 pbc = self.atoms.get_pbc()
371 if all(pbc):
372 cell = self.atoms.get_cell()
373 elif not any(pbc):
374 # large enough cell for non-periodic calculation -
375 # LAMMPS shrink-wraps automatically via input command
376 # "periodic s s s"
377 # below
378 cell = 2 * np.max(np.abs(self.atoms.get_positions())) * np.eye(3)
379 else:
380 warnings.warn(
381 "semi-periodic ASE cell detected - translation "
382 + "to proper LAMMPS input cell might fail"
383 )
384 cell = self.atoms.get_cell()
385 self.prism = Prism(cell)
387 self.set_missing_parameters()
388 self.calls += 1
390 # change into subdirectory for LAMMPS calculations
391 tempdir = self.parameters.tmp_dir
393 # setup file names for LAMMPS calculation
394 label = "{0}{1:>06}".format(self.label, self.calls)
395 lammps_in = uns_mktemp(
396 prefix="in_" + label, dir=tempdir
397 )
398 lammps_log = uns_mktemp(
399 prefix="log_" + label, dir=tempdir
400 )
401 lammps_trj_fd = NamedTemporaryFile(
402 prefix="trj_" + label,
403 suffix=(".bin" if self.parameters.binary_dump else ""),
404 dir=tempdir,
405 delete=(not self.parameters.keep_tmp_files),
406 )
407 lammps_trj = lammps_trj_fd.name
408 if self.parameters.no_data_file:
409 lammps_data = None
410 else:
411 lammps_data_fd = NamedTemporaryFile(
412 prefix="data_" + label,
413 dir=tempdir,
414 delete=(not self.parameters.keep_tmp_files),
415 mode='w',
416 encoding='ascii'
417 )
418 write_lammps_data(
419 lammps_data_fd,
420 self.atoms,
421 specorder=self.parameters.specorder,
422 force_skew=self.parameters.always_triclinic,
423 velocities=self.parameters.write_velocities,
424 prismobj=self.prism,
425 units=self.parameters.units,
426 atom_style=self.parameters.atom_style
427 )
428 lammps_data = lammps_data_fd.name
429 lammps_data_fd.flush()
431 # see to it that LAMMPS is started
432 if not self._lmp_alive():
433 command = self.get_lammps_command()
434 # Attempt to (re)start lammps
435 self._lmp_handle = Popen(
436 shlex.split(command, posix=(os.name == "posix")),
437 stdin=PIPE,
438 stdout=PIPE,
439 )
440 lmp_handle = self._lmp_handle
442 # Create thread reading lammps stdout (for reference, if requested,
443 # also create lammps_log, although it is never used)
444 if self.parameters.keep_tmp_files:
445 lammps_log_fd = open(lammps_log, "wb")
446 fd = SpecialTee(lmp_handle.stdout, lammps_log_fd)
447 else:
448 fd = lmp_handle.stdout
449 thr_read_log = Thread(target=self.read_lammps_log, args=(fd,))
450 thr_read_log.start()
452 # write LAMMPS input (for reference, also create the file lammps_in,
453 # although it is never used)
454 if self.parameters.keep_tmp_files:
455 lammps_in_fd = open(lammps_in, "wb")
456 fd = SpecialTee(lmp_handle.stdin, lammps_in_fd)
457 else:
458 fd = lmp_handle.stdin
459 write_lammps_in(
460 lammps_in=fd,
461 parameters=self.parameters,
462 atoms=self.atoms,
463 prismobj=self.prism,
464 lammps_trj=lammps_trj,
465 lammps_data=lammps_data,
466 )
468 if self.parameters.keep_tmp_files:
469 lammps_in_fd.close()
471 # Wait for log output to be read (i.e., for LAMMPS to finish)
472 # and close the log file if there is one
473 thr_read_log.join()
474 if self.parameters.keep_tmp_files:
475 lammps_log_fd.close()
477 if not self.parameters.keep_alive:
478 self._lmp_end()
480 exitcode = lmp_handle.poll()
481 if exitcode and exitcode != 0:
482 raise RuntimeError(
483 "LAMMPS exited in {} with exit code: {}."
484 "".format(tempdir, exitcode)
485 )
487 # A few sanity checks
488 if len(self.thermo_content) == 0:
489 raise RuntimeError("Failed to retrieve any thermo_style-output")
490 if int(self.thermo_content[-1]["atoms"]) != len(self.atoms):
491 # This obviously shouldn't happen, but if prism.fold_...() fails,
492 # it could
493 raise RuntimeError("Atoms have gone missing")
495 trj_atoms = read_lammps_dump(
496 infileobj=lammps_trj,
497 order=self.parameters.atorder,
498 index=-1,
499 prismobj=self.prism,
500 specorder=self.parameters.specorder,
501 )
503 if set_atoms:
504 self.atoms = trj_atoms.copy()
506 self.forces = trj_atoms.get_forces()
507 # !TODO: trj_atoms is only the last snapshot of the system; Is it
508 # desirable to save also the inbetween steps?
509 if self.parameters.trajectory_out is not None:
510 # !TODO: is it advisable to create here temporary atoms-objects
511 self.trajectory_out.write(trj_atoms)
513 tc = self.thermo_content[-1]
514 self.results["energy"] = convert(
515 tc["pe"], "energy", self.parameters["units"], "ASE"
516 )
517 self.results["free_energy"] = self.results["energy"]
518 self.results['forces'] = convert(self.forces.copy(),
519 'force',
520 self.parameters['units'],
521 'ASE')
522 stress = np.array(
523 [-tc[i] for i in ("pxx", "pyy", "pzz", "pyz", "pxz", "pxy")]
524 )
526 # We need to apply the Lammps rotation stuff to the stress:
527 xx, yy, zz, yz, xz, xy = stress
528 stress_tensor = np.array([[xx, xy, xz],
529 [xy, yy, yz],
530 [xz, yz, zz]])
531 R = self.prism.rot_mat
532 stress_atoms = np.dot(R, stress_tensor)
533 stress_atoms = np.dot(stress_atoms, R.T)
534 stress_atoms = stress_atoms[[0, 1, 2, 1, 0, 0],
535 [0, 1, 2, 2, 2, 1]]
536 stress = stress_atoms
538 self.results["stress"] = convert(
539 stress, "pressure", self.parameters["units"], "ASE"
540 )
542 lammps_trj_fd.close()
543 if not self.parameters.no_data_file:
544 lammps_data_fd.close()
546 def __enter__(self):
547 return self
549 def __exit__(self, *args):
550 self._lmp_end()
552 def read_lammps_log(self, lammps_log=None):
553 # !TODO: somehow communicate 'thermo_content' explicitly
554 """Method which reads a LAMMPS output log file."""
556 if lammps_log is None:
557 lammps_log = self.label + ".log"
559 if isinstance(lammps_log, str):
560 fileobj = paropen(lammps_log, "wb")
561 close_log_file = True
562 else:
563 # Expect lammps_in to be a file-like object
564 fileobj = lammps_log
565 close_log_file = False
567 # read_log depends on that the first (three) thermo_style custom args
568 # can be capitalized and matched against the log output. I.e.
569 # don't use e.g. 'ke' or 'cpu' which are labeled KinEng and CPU.
570 mark_re = r"^\s*" + r"\s+".join(
571 [x.capitalize() for x in self.parameters.thermo_args[0:3]]
572 )
573 _custom_thermo_mark = re_compile(mark_re)
575 # !TODO: regex-magic necessary?
576 # Match something which can be converted to a float
577 f_re = r"([+-]?(?:(?:\d+(?:\.\d*)?|\.\d+)(?:e[+-]?\d+)?|nan|inf))"
578 n_args = len(self.parameters["thermo_args"])
579 # Create a re matching exactly N white space separated floatish things
580 _custom_thermo_re = re_compile(
581 r"^\s*" + r"\s+".join([f_re] * n_args) + r"\s*$", flags=IGNORECASE
582 )
584 thermo_content = []
585 line = fileobj.readline().decode("utf-8")
586 while line and line.strip() != CALCULATION_END_MARK:
587 # check error
588 if 'ERROR:' in line:
589 if close_log_file:
590 fileobj.close()
591 raise RuntimeError(f'LAMMPS exits with error message: {line}')
593 # get thermo output
594 if _custom_thermo_mark.match(line):
595 bool_match = True
596 while bool_match:
597 line = fileobj.readline().decode("utf-8")
598 bool_match = _custom_thermo_re.match(line)
599 if bool_match:
600 # create a dictionary between each of the
601 # thermo_style args and it's corresponding value
602 thermo_content.append(
603 dict(
604 zip(
605 self.parameters.thermo_args,
606 map(float, bool_match.groups()),
607 )
608 )
609 )
610 else:
611 line = fileobj.readline().decode("utf-8")
613 if close_log_file:
614 fileobj.close()
616 self.thermo_content = thermo_content
619class SpecialTee:
620 """A special purpose, with limited applicability, tee-like thing.
622 A subset of stuff read from, or written to, orig_fd,
623 is also written to out_fd.
624 It is used by the lammps calculator for creating file-logs of stuff
625 read from, or written to, stdin and stdout, respectively.
626 """
628 def __init__(self, orig_fd, out_fd):
629 self._orig_fd = orig_fd
630 self._out_fd = out_fd
631 self.name = orig_fd.name
633 def write(self, data):
634 self._orig_fd.write(data)
635 self._out_fd.write(data)
636 self.flush()
638 def read(self, *args, **kwargs):
639 data = self._orig_fd.read(*args, **kwargs)
640 self._out_fd.write(data)
641 return data
643 def readline(self, *args, **kwargs):
644 data = self._orig_fd.readline(*args, **kwargs)
645 self._out_fd.write(data)
646 return data
648 def readlines(self, *args, **kwargs):
649 data = self._orig_fd.readlines(*args, **kwargs)
650 self._out_fd.write("".join(data))
651 return data
653 def flush(self):
654 self._orig_fd.flush()
655 self._out_fd.flush()