lammpsparser 0.0.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,536 @@
1
+ # coding: utf-8
2
+ # Copyright (c) Max-Planck-Institut für Eisenforschung GmbH - Computational Materials Design (CM) Department
3
+ # Distributed under the terms of "New BSD License", see the LICENSE file.
4
+
5
+ from __future__ import annotations
6
+
7
+ import decimal as dec
8
+ import posixpath
9
+ import warnings
10
+ from typing import Any, Dict, List, Optional, Tuple, Union
11
+
12
+ import numpy as np
13
+ from ase.atoms import Atoms
14
+ from ase.data import atomic_masses, atomic_numbers
15
+
16
+ from lammpsparser.units import UnitConverter
17
+
18
+ try:
19
+ from ase.calculators.lammps import Prism
20
+ except ImportError:
21
+ try:
22
+ from ase.calculators.lammpsrun import Prism
23
+ except ImportError:
24
+ from ase.calculators.lammpsrun import prism as Prism
25
+
26
+ __author__ = "Joerg Neugebauer, Sudarsan Surendralal, Yury Lysogorskiy, Jan Janssen, Markus Tautschnig"
27
+ __copyright__ = (
28
+ "Copyright 2021, Max-Planck-Institut für Eisenforschung GmbH - "
29
+ "Computational Materials Design (CM) Department"
30
+ )
31
+ __version__ = "1.0"
32
+ __maintainer__ = "Sudarsan Surendralal"
33
+ __email__ = "surendralal@mpie.de"
34
+ __status__ = "production"
35
+ __date__ = "Sep 1, 2017"
36
+
37
+
38
+ class UnfoldingPrism(Prism):
39
+ """
40
+ Create a lammps-style triclinic prism object from a cell
41
+
42
+ The main purpose of the prism-object is to create suitable
43
+ string representations of prism limits and atom positions
44
+ within the prism.
45
+ When creating the object, the digits parameter (default set to 10)
46
+ specify the precision to use.
47
+ lammps is picky about stuff being within semi-open intervals,
48
+ e.g. for atom positions (when using create_atom in the in-file),
49
+ x must be within [xlo, xhi).
50
+
51
+ Args:
52
+ cell:
53
+ pbc:
54
+ digits:
55
+ """
56
+
57
+ def __init__(
58
+ self,
59
+ cell: np.ndarray,
60
+ pbc: Union[bool, tuple[bool, bool, bool]] = (True, True, True),
61
+ digits: int = 10,
62
+ ):
63
+ # Temporary fix. Since the arguments for the constructor have changed, try to see if it is compatible with
64
+ # the latest ase. If not, revert to the old __init__ parameters.
65
+ if isinstance(pbc, bool):
66
+ pbc = (pbc, pbc, pbc)
67
+ try:
68
+ super(UnfoldingPrism, self).__init__(
69
+ cell, pbc=np.array(pbc), tolerance=float("1e-{}".format(digits))
70
+ )
71
+ except TypeError:
72
+ super(UnfoldingPrism, self).__init__(cell, pbc=np.array(pbc), digits=digits)
73
+ a, b, c = cell
74
+ an, bn, cn = [np.linalg.norm(v) for v in cell]
75
+
76
+ alpha = np.arccos(np.dot(b, c) / (bn * cn))
77
+ beta = np.arccos(np.dot(a, c) / (an * cn))
78
+ gamma = np.arccos(np.dot(a, b) / (an * bn))
79
+
80
+ xhi = an
81
+ xyp = np.cos(gamma) * bn
82
+ yhi = np.sin(gamma) * bn
83
+ xzp = np.cos(beta) * cn
84
+ yzp = (bn * cn * np.cos(alpha) - xyp * xzp) / yhi
85
+ zhi = np.sqrt(cn**2 - xzp**2 - yzp**2)
86
+
87
+ # Set precision
88
+ self.car_prec = dec.Decimal("10.0") ** int(
89
+ np.floor(np.log10(max((xhi, yhi, zhi)))) - digits
90
+ )
91
+ self.dir_prec = dec.Decimal("10.0") ** (-digits)
92
+ self.acc = float(self.car_prec)
93
+ self.eps = np.finfo(xhi).eps
94
+
95
+ # For rotating positions from ase to lammps
96
+ apre = np.array(((xhi, 0, 0), (xyp, yhi, 0), (xzp, yzp, zhi)))
97
+ # np.linalg.inv(cell) ?= np.array([np.cross(b, c), np.cross(c, a), np.cross(a, b)]).T / np.linalg.det(cell)
98
+ self.R = np.dot(np.linalg.inv(cell), apre)
99
+
100
+ def fold(
101
+ vec: np.ndarray, pvec: np.ndarray, i: int
102
+ ) -> Tuple[List[float], float]:
103
+ p = pvec[i]
104
+ x = vec[i] + 0.5 * p
105
+ n = (np.mod(x, p) - x) / p
106
+ return [float(self.f2qdec(vec_a)) for vec_a in (vec + n * pvec)], n
107
+
108
+ apre[1, :], n1 = fold(apre[1, :], apre[0, :], 0)
109
+ if np.abs(apre[1, 0] / apre[0, 0]) > 0.5:
110
+ apre[1, 0] -= np.sign(n1) * apre[0, 0]
111
+ n1 -= np.sign(n1)
112
+
113
+ apre[2, :], n2 = fold(apre[2, :], apre[1, :], 1)
114
+ if np.abs(apre[2, 1] / apre[1, 1]) > 0.5:
115
+ apre[2, 1] -= np.sign(n2) * apre[1, 1]
116
+ n2 -= np.sign(n2)
117
+
118
+ apre[2, :], n3 = fold(apre[2, :], apre[0, :], 0)
119
+ if np.abs(apre[2, 0] / apre[0, 0]) > 0.5:
120
+ apre[2, 0] -= np.sign(n3) * apre[0, 0]
121
+ n3 -= np.sign(n3)
122
+ self.ns = [n1, n2, n3]
123
+
124
+ d_a = apre[0, 0] / 2 - apre[1, 0]
125
+ if np.abs(d_a) < self.acc:
126
+ if d_a < 0:
127
+ print("debug: apply shift")
128
+ apre[1, 0] += 2 * d_a
129
+ apre[2, 0] += 2 * d_a
130
+
131
+ self.A = apre
132
+
133
+ if self.is_skewed() and (not (pbc[0] and pbc[1] and pbc[2])):
134
+ warnings.warn(
135
+ "Skewed lammps cells should have PBC == True in all directions!"
136
+ )
137
+
138
+ def unfold_cell(self, cell: np.ndarray) -> np.ndarray:
139
+ """
140
+ Unfold LAMMPS cell to original
141
+
142
+ Let C be the pyiron_atomistics cell and A be the Lammps cell, then define (in init) the rotation matrix between them as
143
+ R := C^inv.A
144
+ And recall that rotation matrices have the property
145
+ R^T == R^inv
146
+ Then left multiply the definition of R by C, and right multiply by R.T to get
147
+ C.R.R^T = C.C^inv.A.R^T
148
+ Then
149
+ C = A.R^T
150
+
151
+ After that, account for the folding process.
152
+
153
+ Args:
154
+ cell: LAMMPS cell,
155
+
156
+ Returns:
157
+ unfolded cell
158
+ """
159
+ # Rotation
160
+ ucell = np.dot(cell, self.R.T)
161
+ # Folding
162
+ a = ucell[0]
163
+ bp = ucell[1]
164
+ cpp = ucell[2]
165
+ n1, n2, n3 = self.ns
166
+ b = bp - n1 * a
167
+ c = cpp - n2 * bp - n3 * a
168
+ return np.array([a, b, c])
169
+
170
+ def pos_to_lammps(self, position: np.ndarray) -> Tuple[float, float, float]:
171
+ """
172
+ Rotate an ase-cell position to the lammps cell orientation
173
+
174
+ Args:
175
+ position:
176
+
177
+ Returns:
178
+ tuple of float.
179
+ """
180
+ return tuple([x for x in np.dot(position, self.R)])
181
+
182
+ def f2qdec(self, f: float) -> dec.Decimal:
183
+ return dec.Decimal(str(f)).quantize(self.car_prec, dec.ROUND_DOWN)
184
+
185
+ def f2s(self, f: float) -> str:
186
+ return str(dec.Decimal(str(f)).quantize(self.car_prec, dec.ROUND_HALF_EVEN))
187
+
188
+ def get_lammps_prism_str(self) -> Tuple[str, ...]:
189
+ """Return a tuple of strings"""
190
+ p = self.get_lammps_prism()
191
+ return tuple([self.f2s(x) for x in p])
192
+
193
+
194
+ class LammpsStructure:
195
+ """
196
+
197
+ Args:
198
+ input_file_name:
199
+ """
200
+
201
+ def __init__(
202
+ self,
203
+ bond_dict: Optional[Dict] = None,
204
+ units: str = "metal",
205
+ atom_type: str = "atomic",
206
+ ):
207
+ self._string_input: str = ""
208
+ self._structure: Optional[Atoms] = None
209
+ self._potential: Optional[Any] = None
210
+ self._el_eam_lst: List[str] = []
211
+ self.atom_type: Optional[str] = None
212
+ self.cutoff_radius: Optional[float] = None
213
+ self.digits: int = 10
214
+ self._bond_dict: Optional[Dict] = bond_dict
215
+ self._force_skewed: bool = False
216
+ self._units: str = units
217
+ self._atom_type: str = atom_type
218
+
219
+ @property
220
+ def potential(self) -> Any:
221
+ return self._potential
222
+
223
+ @potential.setter
224
+ def potential(self, val: Any):
225
+ self._potential = val
226
+
227
+ @property
228
+ def structure(self) -> Optional[Atoms]:
229
+ """
230
+
231
+ Returns:
232
+
233
+ """
234
+ return self._structure
235
+
236
+ @structure.setter
237
+ def structure(self, structure: Atoms):
238
+ """
239
+
240
+ Args:
241
+ structure:
242
+
243
+ Returns:
244
+
245
+ """
246
+ self._structure = structure
247
+ if self._atom_type == "charge":
248
+ input_str = self.structure_charge()
249
+ else: # self.atom_type == 'atomic'
250
+ input_str = self.structure_atomic()
251
+ self._string_input = input_str + self._get_velocities_input_string()
252
+
253
+ def _get_velocities_input_string(self) -> str:
254
+ input_str = ""
255
+ if self._structure is not None and not np.all(
256
+ np.isclose(
257
+ self._structure.get_velocities(),
258
+ np.array([[0.0, 0.0, 0.0]] * len(self._structure)),
259
+ )
260
+ ):
261
+ uc = UnitConverter(self._units)
262
+ self._structure.set_velocities(
263
+ self._structure.get_velocities() * uc.pyiron_to_lammps("velocity")
264
+ )
265
+ vels = self.rotate_velocities(self._structure)
266
+ input_str += "Velocities\n\n"
267
+ if len(self._structure.positions[0]) == 3:
268
+ format_str = "{0:d} {1:f} {2:f} {3:f}\n"
269
+ for id_atom, (x, y, z) in enumerate(vels, start=1):
270
+ input_str += format_str.format(id_atom, x, y, z)
271
+ elif len(self._structure.positions[0]) == 2:
272
+ format_str = "{0:d} {1:f} {2:f}\n"
273
+ for id_atom, (x, y) in enumerate(vels, start=1):
274
+ input_str += format_str.format(id_atom, x, y)
275
+ return input_str
276
+
277
+ @property
278
+ def el_eam_lst(self) -> List[str]:
279
+ """
280
+
281
+ Returns:
282
+
283
+ """
284
+ return self._el_eam_lst
285
+
286
+ @el_eam_lst.setter
287
+ def el_eam_lst(self, el_eam_lst: List[str]):
288
+ """
289
+
290
+ Args:
291
+ el_eam_lst:
292
+
293
+ Returns:
294
+
295
+ """
296
+ self._el_eam_lst = el_eam_lst
297
+
298
+ @staticmethod
299
+ def get_lammps_id_dict(el_eam_lst: List[str]) -> Dict[str, int]:
300
+ if len(el_eam_lst) == 0:
301
+ raise ValueError("el_eam_list is empty. Can not determine order of species")
302
+ return {el: idx + 1 for idx, el in enumerate(el_eam_lst)}
303
+
304
+ @staticmethod
305
+ def lammps_header(
306
+ structure: Atoms,
307
+ cell_dimensions: str,
308
+ species_lammps_id_dict: Dict[str, int],
309
+ nbonds: Optional[int] = None,
310
+ nangles: Optional[int] = None,
311
+ nbond_types: Optional[int] = None,
312
+ nangle_types: Optional[int] = None,
313
+ ) -> str:
314
+ atomtypes = (
315
+ "Start File for LAMMPS \n"
316
+ + "{0:d} atoms".format(len(structure))
317
+ + " \n"
318
+ + "{0} atom types".format(len(species_lammps_id_dict.keys()))
319
+ + " \n"
320
+ ) # '{0} atom types'.format(structure.get_number_of_species()) + ' \n'
321
+ if nbonds is not None:
322
+ atomtypes += "{0:d} bonds\n".format(nbonds)
323
+ if nangles is not None:
324
+ atomtypes += "{0:d} angles\n".format(nangles)
325
+ if nbond_types is not None:
326
+ atomtypes += "{0:d} bond types\n".format(nbond_types)
327
+ if nangle_types is not None:
328
+ atomtypes += "{0:d} angle types\n".format(nangle_types)
329
+
330
+ masses = "Masses\n\n"
331
+ for el, idx in species_lammps_id_dict.items():
332
+ mass = atomic_masses[atomic_numbers[el]]
333
+ masses += "{0:3d} {1:f} # ({2}) \n".format(idx, mass, el)
334
+
335
+ return atomtypes + "\n" + cell_dimensions + "\n" + masses + "\n"
336
+
337
+ def simulation_cell(self) -> str:
338
+ """
339
+
340
+ Returns:
341
+
342
+ """
343
+ if self._structure is None:
344
+ raise ValueError("Structure not set")
345
+ self.prism = UnfoldingPrism(self._structure.cell, digits=15)
346
+ xhi, yhi, zhi, xy, xz, yz = self.prism.get_lammps_prism_str()
347
+ # Please, be carefull and not round xhi, yhi,..., otherwise you will get too skew cell from LAMMPS.
348
+ # These values are already checked in UnfoldingPrism to fullfill LAMMPS skewness criteria
349
+ simulation_cell = (
350
+ "0. {} xlo xhi\n".format(xhi)
351
+ + "0. {} ylo yhi\n".format(yhi)
352
+ + "0. {} zlo zhi\n".format(zhi)
353
+ )
354
+
355
+ if is_skewed(self.structure) or self._force_skewed:
356
+ simulation_cell += "{0} {1} {2} xy xz yz\n".format(xy, xz, yz)
357
+
358
+ return simulation_cell
359
+
360
+ def structure_atomic(self) -> str:
361
+ """
362
+ Write routine to create atom structure static file that can be loaded by LAMMPS
363
+
364
+ Returns:
365
+
366
+ """
367
+ if self._structure is None:
368
+ raise ValueError("Structure not set")
369
+ species_lammps_id_dict = self.get_lammps_id_dict(self.el_eam_lst)
370
+ atoms = "Atoms\n\n"
371
+ coords = self.rotate_positions(self._structure)
372
+
373
+ el_lst = self._structure.get_chemical_symbols()
374
+ for id_atom, (el, coord) in enumerate(zip(el_lst, coords)):
375
+ dim = len(self._structure.positions[0])
376
+ c = np.zeros(3)
377
+ c[:dim] = coord
378
+ atoms += (
379
+ "{0:d} {1:d} {2:.15f} {3:.15f} {4:.15f}".format(
380
+ id_atom + 1, species_lammps_id_dict[el], c[0], c[1], c[2]
381
+ )
382
+ + "\n"
383
+ )
384
+ return (
385
+ self.lammps_header(
386
+ structure=self.structure,
387
+ cell_dimensions=self.simulation_cell(),
388
+ species_lammps_id_dict=species_lammps_id_dict,
389
+ )
390
+ + atoms
391
+ + "\n"
392
+ )
393
+
394
+ def structure_charge(self):
395
+ """
396
+ Create atom structure including the atom charges.
397
+
398
+ By convention the LAMMPS atom type numbers are chose alphabetically for the chemical species.
399
+
400
+ Returns: LAMMPS readable structure.
401
+
402
+ """
403
+ species_lammps_id_dict = self.get_lammps_id_dict(self.el_eam_lst)
404
+ atoms = "Atoms\n\n"
405
+ coords = self.rotate_positions(self._structure)
406
+ el_charge_lst = self._structure.get_initial_charges()
407
+ el_lst = self._structure.get_chemical_symbols()
408
+ for id_atom, (el, coord) in enumerate(zip(el_lst, coords)):
409
+ dim = len(self._structure.positions[0])
410
+ c = np.zeros(3)
411
+ c[:dim] = coord
412
+ atoms += (
413
+ "{0:d} {1:d} {2:f} {3:.15f} {4:.15f} {5:.15f}".format(
414
+ id_atom + 1,
415
+ species_lammps_id_dict[el],
416
+ el_charge_lst[id_atom],
417
+ c[0],
418
+ c[1],
419
+ c[2],
420
+ )
421
+ + "\n"
422
+ )
423
+ return (
424
+ self.lammps_header(
425
+ structure=self.structure,
426
+ cell_dimensions=self.simulation_cell(),
427
+ species_lammps_id_dict=species_lammps_id_dict,
428
+ )
429
+ + atoms
430
+ + "\n"
431
+ )
432
+
433
+ def rotate_positions(self, structure: Atoms) -> List[Tuple[float, float, float]]:
434
+ """
435
+ Rotate all atomic positions in given structure according to new Prism cell
436
+
437
+ Args:
438
+ structure: Atoms-like object. Should has .positions attribute
439
+
440
+ Returns:
441
+ (list): List of rotated coordinates
442
+ """
443
+ if self._structure is None:
444
+ raise ValueError("Structure not set")
445
+ prism = UnfoldingPrism(self._structure.cell)
446
+ coords = [prism.pos_to_lammps(position) for position in structure.positions]
447
+ return coords
448
+
449
+ def rotate_velocities(self, structure: Atoms) -> List[Tuple[float, float, float]]:
450
+ """
451
+ Rotate all atomic velocities in given structure according to new Prism cell
452
+
453
+ Args:
454
+ structure: Atoms-like object. Should have .velocities attribute.
455
+
456
+ Returns:
457
+ (list): List of rotated velocities
458
+ """
459
+ if self._structure is None:
460
+ raise ValueError("Structure not set")
461
+ prism = UnfoldingPrism(self._structure.cell)
462
+ vels = [prism.pos_to_lammps(vel) for vel in structure.get_velocities()]
463
+ return vels
464
+
465
+ def write_file(self, file_name: str, cwd: Optional[str] = None):
466
+ """
467
+ Write GenericParameters to input file
468
+
469
+ Args:
470
+ file_name (str): name of the file, either absolute (then cwd must be None) or relative
471
+ cwd (str): path name (default: None)
472
+ """
473
+ if cwd is not None:
474
+ file_name = posixpath.join(cwd, file_name)
475
+
476
+ with open(file_name, "w") as f:
477
+ for line in self._string_input:
478
+ f.write(line)
479
+
480
+
481
+ def is_skewed(structure: Atoms, tolerance: float = 1.0e-8) -> bool:
482
+ """
483
+ Check whether the simulation box is skewed/sheared. The algorithm compares the box volume
484
+ and the product of the box length in each direction. If these numbers do not match, the box
485
+ is considered to be skewed and the function returns True
486
+
487
+ Args:
488
+ tolerance (float): Relative tolerance above which the structure is considered as skewed
489
+
490
+ Returns:
491
+ (bool): Whether the box is skewed or not.
492
+ """
493
+ volume = structure.get_volume()
494
+ prod = np.linalg.norm(structure.cell, axis=-1).prod()
495
+ if volume > 0:
496
+ if abs(volume - prod) / volume < tolerance:
497
+ return False
498
+ return True
499
+
500
+
501
+ def write_lammps_datafile(
502
+ structure: Atoms,
503
+ potential_elements: Union[np.ndarray, list],
504
+ bond_dict: Optional[Dict] = None,
505
+ units: str = "metal",
506
+ file_name: str = "lammps.data",
507
+ working_directory: Optional[str] = None,
508
+ atom_type: str = "atomic",
509
+ ) -> None:
510
+ lammps_str = LammpsStructure(bond_dict=bond_dict, units=units, atom_type=atom_type)
511
+ lammps_str.el_eam_lst = potential_elements
512
+ lammps_str.structure = structure
513
+ lammps_str.write_file(file_name=file_name, cwd=working_directory)
514
+
515
+
516
+ def structure_to_lammps(structure: Atoms) -> Atoms:
517
+ """
518
+ Converts a structure to the Lammps coordinate frame
519
+
520
+ Args:
521
+ structure (pyiron.atomistics.structure.atoms.Atoms): Structure to convert.
522
+
523
+ Returns:
524
+ pyiron.atomistics.structure.atoms.Atoms: Structure with the LAMMPS coordinate frame.
525
+ """
526
+ prism = UnfoldingPrism(structure.cell)
527
+ lammps_structure = structure.copy()
528
+ lammps_structure.set_cell(prism.A)
529
+ lammps_structure.positions = np.matmul(structure.positions, prism.R)
530
+ if not np.all(
531
+ np.isclose(
532
+ structure.get_velocities(), np.array([[0.0, 0.0, 0.0]] * len(structure))
533
+ )
534
+ ):
535
+ lammps_structure.set_velocities(np.matmul(structure.get_velocities(), prism.R))
536
+ return lammps_structure