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.
lammpsparser/output.py ADDED
@@ -0,0 +1,340 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ import warnings
5
+ from typing import Dict, List, Optional, Tuple, Union
6
+
7
+ import numpy as np
8
+ import pandas as pd
9
+ from ase.atoms import Atoms
10
+
11
+ from lammpsparser.output_raw import (
12
+ parse_raw_dump_from_h5md,
13
+ parse_raw_dump_from_text,
14
+ parse_raw_lammps_log,
15
+ )
16
+ from lammpsparser.structure import UnfoldingPrism
17
+ from lammpsparser.units import UnitConverter
18
+
19
+
20
+ def remap_indices_ase(
21
+ lammps_indices: Union[np.ndarray, List],
22
+ potential_elements: Union[np.ndarray, List],
23
+ structure: Atoms,
24
+ ) -> np.ndarray:
25
+ """
26
+ Give the Lammps-dumped indices, re-maps these back onto the structure's indices to preserve the species.
27
+
28
+ The issue is that for an N-element potential, Lammps dumps the chemical index from 1 to N based on the order
29
+ that these species are written in the Lammps input file. But the indices for a given structure are based on the
30
+ order in which chemical species were added to that structure, and run from 0 up to the number of species
31
+ currently in that structure. Therefore we need to be a little careful with mapping.
32
+
33
+ Args:
34
+ lammps_indices (numpy.ndarray/list): The Lammps-dumped integers.
35
+ potential_elements (numpy.ndarray/list):
36
+ structure (pyiron_atomistics.atomistics.structure.Atoms):
37
+
38
+ Returns:
39
+ numpy.ndarray: Those integers mapped onto the structure.
40
+ """
41
+ lammps_symbol_order = np.array(potential_elements)
42
+
43
+ # Create a map between the lammps indices and structure indices to preserve species
44
+ structure_symbol_order = np.unique(structure.get_chemical_symbols())
45
+ map_ = np.array(
46
+ [
47
+ int(np.argwhere(lammps_symbol_order == symbol)[0][0]) + 1
48
+ for symbol in structure_symbol_order
49
+ ]
50
+ )
51
+
52
+ structure_indices = np.array(lammps_indices)
53
+ for i_struct, i_lammps in enumerate(map_):
54
+ np.place(structure_indices, lammps_indices == i_lammps, i_struct)
55
+ # TODO: Vectorize this for-loop for computational efficiency
56
+
57
+ return structure_indices
58
+
59
+
60
+ def parse_lammps_output(
61
+ working_directory: str,
62
+ structure: Atoms,
63
+ potential_elements: Union[np.ndarray, List],
64
+ units: str,
65
+ prism: Optional[UnfoldingPrism] = None,
66
+ dump_h5_file_name: str = "dump.h5",
67
+ dump_out_file_name: str = "dump.out",
68
+ log_lammps_file_name: str = "log.lammps",
69
+ remap_indices_funct: callable = remap_indices_ase,
70
+ ) -> Dict:
71
+ if prism is None:
72
+ prism = UnfoldingPrism(structure.cell)
73
+ dump_dict = _parse_dump(
74
+ dump_h5_full_file_name=os.path.join(working_directory, dump_h5_file_name),
75
+ dump_out_full_file_name=os.path.join(working_directory, dump_out_file_name),
76
+ prism=prism,
77
+ structure=structure,
78
+ potential_elements=potential_elements,
79
+ remap_indices_funct=remap_indices_funct,
80
+ )
81
+
82
+ generic_keys_lst, pressure_dict, df = _parse_log(
83
+ log_lammps_full_file_name=os.path.join(working_directory, log_lammps_file_name),
84
+ prism=prism,
85
+ )
86
+
87
+ convert_units = UnitConverter(units).convert_array_to_pyiron_units
88
+
89
+ hdf_output = {"generic": {}, "lammps": {}}
90
+ hdf_generic = hdf_output["generic"]
91
+ hdf_lammps = hdf_output["lammps"]
92
+
93
+ if "computes" in dump_dict.keys():
94
+ for k, v in dump_dict.pop("computes").items():
95
+ hdf_generic[k] = convert_units(np.array(v), label=k)
96
+
97
+ hdf_generic["steps"] = convert_units(
98
+ np.array(dump_dict.pop("steps"), dtype=int), label="steps"
99
+ )
100
+
101
+ for k, v in dump_dict.items():
102
+ if len(v) > 0:
103
+ try:
104
+ hdf_generic[k] = convert_units(np.array(v), label=k)
105
+ except ValueError:
106
+ hdf_generic[k] = [convert_units(np.array(val), label=k) for val in v]
107
+
108
+ if df is not None and pressure_dict is not None and generic_keys_lst is not None:
109
+ for k, v in df.items():
110
+ v = convert_units(np.array(v), label=k)
111
+ if k in generic_keys_lst:
112
+ hdf_generic[k] = v
113
+ else: # This is a hack for backward comparability
114
+ hdf_lammps[k] = v
115
+
116
+ # Store pressures as numpy arrays
117
+ for key, val in pressure_dict.items():
118
+ hdf_generic[key] = convert_units(val, label=key)
119
+ else:
120
+ warnings.warn("LAMMPS warning: No log.lammps output file found.")
121
+
122
+ return hdf_output
123
+
124
+
125
+ def _parse_dump(
126
+ dump_h5_full_file_name: str,
127
+ dump_out_full_file_name: str,
128
+ prism: UnfoldingPrism,
129
+ structure: Atoms,
130
+ potential_elements: Union[np.ndarray, List],
131
+ remap_indices_funct: callable = remap_indices_ase,
132
+ ) -> Dict:
133
+ if os.path.isfile(dump_h5_full_file_name):
134
+ if not _check_ortho_prism(prism=prism):
135
+ raise RuntimeError(
136
+ "The Lammps output will not be mapped back to pyiron correctly."
137
+ )
138
+ return parse_raw_dump_from_h5md(
139
+ file_name=dump_h5_full_file_name,
140
+ )
141
+ elif os.path.exists(dump_out_full_file_name):
142
+ return _collect_dump_from_text(
143
+ file_name=dump_out_full_file_name,
144
+ prism=prism,
145
+ structure=structure,
146
+ potential_elements=potential_elements,
147
+ remap_indices_funct=remap_indices_funct,
148
+ )
149
+ else:
150
+ raise FileNotFoundError(
151
+ f"Neither {dump_h5_full_file_name} nor {dump_out_full_file_name} exist."
152
+ )
153
+
154
+
155
+ def _collect_dump_from_text(
156
+ file_name: str,
157
+ prism: UnfoldingPrism,
158
+ structure: Atoms,
159
+ potential_elements: Union[np.ndarray, List],
160
+ remap_indices_funct: callable = remap_indices_ase,
161
+ ) -> Dict:
162
+ """
163
+ general purpose routine to extract static from a lammps dump file
164
+ """
165
+ rotation_lammps2orig = prism.R.T
166
+ dump_lammps_dict = parse_raw_dump_from_text(file_name=file_name)
167
+ dump_dict = {}
168
+ for key, val in dump_lammps_dict.items():
169
+ if key in ["cells"]:
170
+ dump_dict[key] = [prism.unfold_cell(cell=cell) for cell in val]
171
+ elif key in ["indices"]:
172
+ dump_dict[key] = [
173
+ remap_indices_funct(
174
+ lammps_indices=indices,
175
+ potential_elements=potential_elements,
176
+ structure=structure,
177
+ )
178
+ for indices in val
179
+ ]
180
+ elif key in [
181
+ "forces",
182
+ "mean_forces",
183
+ "velocities",
184
+ "mean_velocities",
185
+ "mean_unwrapped_positions",
186
+ ]:
187
+ dump_dict[key] = [np.matmul(v, rotation_lammps2orig) for v in val]
188
+ elif key in ["positions", "unwrapped_positions"]:
189
+ dump_dict[key] = [
190
+ np.matmul(np.matmul(v, lammps_cell), rotation_lammps2orig)
191
+ for v, lammps_cell in zip(val, dump_lammps_dict["cells"])
192
+ ]
193
+ else:
194
+ dump_dict[key] = val
195
+ return dump_dict
196
+
197
+
198
+ def _parse_log(
199
+ log_lammps_full_file_name: str, prism: UnfoldingPrism
200
+ ) -> Union[Tuple[List[str], Dict, pd.DataFrame], Tuple[None, None, None]]:
201
+ """
202
+ If it exists, parses the lammps log file and either raises an exception if errors
203
+ occurred or returns data. Just returns a tuple of Nones if there is no file at the
204
+ given location.
205
+
206
+ Args:
207
+ log_lammps_full_file_name (str): The path to the lammps log file.
208
+ prism (pyiron_atomistics.lammps.structure.UnfoldingPrism): For mapping between
209
+ lammps and pyiron structures
210
+
211
+ Returns:
212
+ (list | None): Generic keys
213
+ (dict | None): Pressures
214
+ (pandas.DataFrame | None): A dataframe with the rest of the information
215
+
216
+ Raises:
217
+ (RuntimeError): If there are "ERROR" tags in the log.
218
+ """
219
+ if os.path.exists(log_lammps_full_file_name):
220
+ return _collect_output_log(
221
+ file_name=log_lammps_full_file_name,
222
+ prism=prism,
223
+ )
224
+ else:
225
+ return None, None, None
226
+
227
+
228
+ def _collect_output_log(
229
+ file_name: str, prism: UnfoldingPrism
230
+ ) -> Tuple[List[str], Dict, pd.DataFrame]:
231
+ """
232
+ general purpose routine to extract static from a lammps log file
233
+ """
234
+ df = parse_raw_lammps_log(file_name=file_name)
235
+
236
+ h5_dict = {
237
+ "Step": "steps",
238
+ "Temp": "temperature",
239
+ "PotEng": "energy_pot",
240
+ "TotEng": "energy_tot",
241
+ "Volume": "volume",
242
+ "LogStep": "LogStep",
243
+ }
244
+ if "LogStep" not in df.columns:
245
+ del h5_dict["LogStep"]
246
+
247
+ for key in df.columns[df.columns.str.startswith("f_mean")]:
248
+ h5_dict[key] = key.replace("f_", "")
249
+
250
+ df = df.rename(index=str, columns=h5_dict)
251
+ pressure_dict = dict()
252
+ if all(
253
+ [
254
+ x in df.columns.values
255
+ for x in [
256
+ "Pxx",
257
+ "Pxy",
258
+ "Pxz",
259
+ "Pxy",
260
+ "Pyy",
261
+ "Pyz",
262
+ "Pxz",
263
+ "Pyz",
264
+ "Pzz",
265
+ ]
266
+ ]
267
+ ):
268
+ pressures = (
269
+ np.stack(
270
+ (
271
+ df.Pxx,
272
+ df.Pxy,
273
+ df.Pxz,
274
+ df.Pxy,
275
+ df.Pyy,
276
+ df.Pyz,
277
+ df.Pxz,
278
+ df.Pyz,
279
+ df.Pzz,
280
+ ),
281
+ axis=-1,
282
+ )
283
+ .reshape(-1, 3, 3)
284
+ .astype("float64")
285
+ )
286
+ # Rotate pressures from Lammps frame to pyiron frame if necessary
287
+ if _check_ortho_prism(prism=prism):
288
+ rotation_matrix = prism.R.T
289
+ pressures = rotation_matrix.T @ pressures @ rotation_matrix
290
+
291
+ df = df.drop(
292
+ columns=df.columns[
293
+ ((df.columns.str.len() == 3) & df.columns.str.startswith("P"))
294
+ ]
295
+ )
296
+ pressure_dict["pressures"] = pressures
297
+ else:
298
+ warnings.warn(
299
+ "LAMMPS warning: log.lammps does not contain the required pressure values."
300
+ )
301
+ if "mean_pressure[1]" in df.columns:
302
+ pressures = (
303
+ np.stack(
304
+ tuple(df[f"mean_pressure[{i}]"] for i in [1, 4, 5, 4, 2, 6, 5, 6, 3]),
305
+ axis=-1,
306
+ )
307
+ .reshape(-1, 3, 3)
308
+ .astype("float64")
309
+ )
310
+ if _check_ortho_prism(prism=prism):
311
+ rotation_matrix = prism.R.T
312
+ pressures = rotation_matrix.T @ pressures @ rotation_matrix
313
+ df = df.drop(
314
+ columns=df.columns[
315
+ (
316
+ df.columns.str.startswith("mean_pressure")
317
+ & df.columns.str.endswith("]")
318
+ )
319
+ ]
320
+ )
321
+ pressure_dict["mean_pressures"] = pressures
322
+ generic_keys_lst = list(h5_dict.values())
323
+ return generic_keys_lst, pressure_dict, df
324
+
325
+
326
+ def _check_ortho_prism(
327
+ prism: UnfoldingPrism, rtol: float = 0.0, atol: float = 1e-08
328
+ ) -> bool:
329
+ """
330
+ Check if the rotation matrix of the UnfoldingPrism object is sufficiently close to a unit matrix
331
+
332
+ Args:
333
+ prism (pyiron_atomistics.lammps.structure.UnfoldingPrism): UnfoldingPrism object to check
334
+ rtol (float): relative precision for numpy.isclose()
335
+ atol (float): absolute precision for numpy.isclose()
336
+
337
+ Returns:
338
+ boolean: True or False
339
+ """
340
+ return np.isclose(prism.R, np.eye(3), rtol=rtol, atol=atol).all()
@@ -0,0 +1,251 @@
1
+ from __future__ import annotations
2
+
3
+ import warnings
4
+ from dataclasses import asdict, dataclass, field
5
+ from io import StringIO
6
+ from typing import Dict, List, Union
7
+
8
+ import numpy as np
9
+ import pandas as pd
10
+
11
+
12
+ @dataclass
13
+ class DumpData:
14
+ steps: List = field(default_factory=lambda: [])
15
+ natoms: List = field(default_factory=lambda: [])
16
+ cells: List = field(default_factory=lambda: [])
17
+ indices: List = field(default_factory=lambda: [])
18
+ forces: List = field(default_factory=lambda: [])
19
+ mean_forces: List = field(default_factory=lambda: [])
20
+ velocities: List = field(default_factory=lambda: [])
21
+ mean_velocities: List = field(default_factory=lambda: [])
22
+ unwrapped_positions: List = field(default_factory=lambda: [])
23
+ mean_unwrapped_positions: List = field(default_factory=lambda: [])
24
+ positions: List = field(default_factory=lambda: [])
25
+ computes: Dict = field(default_factory=lambda: {})
26
+
27
+
28
+ def to_amat(l_list: Union[np.ndarray, List]) -> List:
29
+ lst = np.reshape(l_list, -1)
30
+ if len(lst) == 9:
31
+ (
32
+ xlo_bound,
33
+ xhi_bound,
34
+ xy,
35
+ ylo_bound,
36
+ yhi_bound,
37
+ xz,
38
+ zlo_bound,
39
+ zhi_bound,
40
+ yz,
41
+ ) = lst
42
+
43
+ elif len(lst) == 6:
44
+ xlo_bound, xhi_bound, ylo_bound, yhi_bound, zlo_bound, zhi_bound = lst
45
+ xy, xz, yz = 0.0, 0.0, 0.0
46
+ else:
47
+ raise ValueError("This format for amat not yet implemented: " + str(len(lst)))
48
+
49
+ # > xhi_bound - xlo_bound = xhi -xlo + MAX(0.0, xy, xz, xy + xz) - MIN(0.0, xy, xz, xy + xz)
50
+ # > xhili = xhi -xlo = xhi_bound - xlo_bound - MAX(0.0, xy, xz, xy + xz) + MIN(0.0, xy, xz, xy + xz)
51
+ xhilo = (
52
+ (xhi_bound - xlo_bound)
53
+ - max([0.0, xy, xz, xy + xz])
54
+ + min([0.0, xy, xz, xy + xz])
55
+ )
56
+
57
+ # > yhilo = yhi -ylo = yhi_bound -ylo_bound - MAX(0.0, yz) + MIN(0.0, yz)
58
+ yhilo = (yhi_bound - ylo_bound) - max([0.0, yz]) + min([0.0, yz])
59
+
60
+ # > zhi - zlo = zhi_bound- zlo_bound
61
+ zhilo = zhi_bound - zlo_bound
62
+
63
+ cell = [[xhilo, 0, 0], [xy, yhilo, 0], [xz, yz, zhilo]]
64
+ return cell
65
+
66
+
67
+ def parse_raw_dump_from_h5md(file_name: str) -> Dict:
68
+ import h5py
69
+
70
+ with h5py.File(file_name, mode="r", libver="latest", swmr=True) as h5md:
71
+ positions = [pos_i.tolist() for pos_i in h5md["/particles/all/position/value"]]
72
+ steps = [steps_i.tolist() for steps_i in h5md["/particles/all/position/step"]]
73
+ forces = [for_i.tolist() for for_i in h5md["/particles/all/force/value"]]
74
+ # following the explanation at: http://nongnu.org/h5md/h5md.html
75
+ cell = [
76
+ np.eye(3) * np.array(cell_i.tolist())
77
+ for cell_i in h5md["/particles/all/box/edges/value"]
78
+ ]
79
+ return {
80
+ "forces": forces,
81
+ "positions": positions,
82
+ "steps": steps,
83
+ "cells": cell,
84
+ }
85
+
86
+
87
+ def parse_raw_dump_from_text(file_name: str) -> Dict:
88
+ """
89
+ Docstring for _parse_dump_from_text
90
+
91
+ Args:
92
+ file_name (str): The path to the lammps dump file.
93
+
94
+ Returns:
95
+ Dict: Parsed dump data.
96
+ """
97
+ with open(file_name, "r") as f:
98
+ dump = DumpData()
99
+
100
+ for line in f:
101
+ if "ITEM: TIMESTEP" in line:
102
+ dump.steps.append(int(f.readline()))
103
+
104
+ elif "ITEM: BOX BOUNDS" in line:
105
+ c1 = np.fromstring(f.readline(), dtype=float, sep=" ")
106
+ c2 = np.fromstring(f.readline(), dtype=float, sep=" ")
107
+ c3 = np.fromstring(f.readline(), dtype=float, sep=" ")
108
+ cell = np.concatenate([c1, c2, c3])
109
+ dump.cells.append(to_amat(cell))
110
+
111
+ elif "ITEM: NUMBER OF ATOMS" in line:
112
+ n = int(f.readline())
113
+ dump.natoms.append(n)
114
+
115
+ elif "ITEM: ATOMS" in line:
116
+ # get column names from line
117
+ columns = line.lstrip("ITEM: ATOMS").split()
118
+
119
+ # Read line by line of snapshot into a string buffer
120
+ # Than parse using pandas for speed and column acces
121
+ buf = StringIO()
122
+ for _ in range(n):
123
+ buf.write(f.readline())
124
+ buf.seek(0)
125
+ df = pd.read_csv(
126
+ buf,
127
+ nrows=n,
128
+ sep="\\s+",
129
+ header=None,
130
+ names=columns,
131
+ engine="c",
132
+ )
133
+ df.sort_values(by="id", ignore_index=True, inplace=True)
134
+ # Coordinate transform lammps->pyiron
135
+ dump.indices.append(df["type"].array.astype(int))
136
+
137
+ dump.forces.append(
138
+ np.stack([df["fx"].array, df["fy"].array, df["fz"].array], axis=1)
139
+ )
140
+ if "f_mean_forces[1]" in columns:
141
+ dump.mean_forces.append(
142
+ np.stack(
143
+ [
144
+ df["f_mean_forces[1]"].array,
145
+ df["f_mean_forces[2]"].array,
146
+ df["f_mean_forces[3]"].array,
147
+ ],
148
+ axis=1,
149
+ )
150
+ )
151
+ if "vx" in columns and "vy" in columns and "vz" in columns:
152
+ dump.velocities.append(
153
+ np.stack(
154
+ [
155
+ df["vx"].array,
156
+ df["vy"].array,
157
+ df["vz"].array,
158
+ ],
159
+ axis=1,
160
+ )
161
+ )
162
+
163
+ if "f_mean_velocities[1]" in columns:
164
+ dump.mean_velocities.append(
165
+ np.stack(
166
+ [
167
+ df["f_mean_velocities[1]"].array,
168
+ df["f_mean_velocities[2]"].array,
169
+ df["f_mean_velocities[3]"].array,
170
+ ],
171
+ axis=1,
172
+ )
173
+ )
174
+
175
+ if "xsu" in columns:
176
+ direct_unwrapped_positions = np.stack(
177
+ [
178
+ df["xsu"].array,
179
+ df["ysu"].array,
180
+ df["zsu"].array,
181
+ ],
182
+ axis=1,
183
+ )
184
+ dump.unwrapped_positions.append(direct_unwrapped_positions)
185
+ dump.positions.append(
186
+ direct_unwrapped_positions
187
+ - np.floor(direct_unwrapped_positions)
188
+ )
189
+
190
+ if "f_mean_positions[1]" in columns:
191
+ dump.mean_unwrapped_positions.append(
192
+ np.stack(
193
+ [
194
+ df["f_mean_positions[1]"].array,
195
+ df["f_mean_positions[2]"].array,
196
+ df["f_mean_positions[3]"].array,
197
+ ],
198
+ axis=1,
199
+ )
200
+ )
201
+ for k in columns:
202
+ if k.startswith("c_"):
203
+ kk = k.replace("c_", "")
204
+ if kk not in dump.computes.keys():
205
+ dump.computes[kk] = []
206
+ dump.computes[kk].append(df[k].array)
207
+
208
+ return asdict(dump)
209
+
210
+
211
+ def parse_raw_lammps_log(file_name: str) -> pd.DataFrame:
212
+ """
213
+ Docstring for _parse_lammps_log
214
+
215
+ Args:
216
+ file_name (str): The path to the lammps log file.
217
+
218
+ Returns:
219
+ pd.DataFrame: Dataframe containing the parsed log data.
220
+ """
221
+ with open(file_name, "r") as f:
222
+ dfs = []
223
+ read_thermo = False
224
+ for l in f:
225
+ l = l.lstrip()
226
+
227
+ if l.startswith("Step"):
228
+ thermo_lines = ""
229
+ read_thermo = True
230
+
231
+ if read_thermo:
232
+ if l.startswith("Loop") or l.startswith("ERROR"):
233
+ read_thermo = False
234
+ dfs.append(
235
+ pd.read_csv(StringIO(thermo_lines), sep="\\s+", engine="c")
236
+ )
237
+
238
+ elif l.startswith("WARNING:"):
239
+ warnings.warn(f"A warning was found in the log:\n{l}")
240
+
241
+ else:
242
+ thermo_lines += l
243
+
244
+ if len(dfs) == 1:
245
+ df = dfs[0]
246
+ else:
247
+ for i in range(len(dfs)):
248
+ df = dfs[i]
249
+ df["LogStep"] = np.ones(len(df)) * i
250
+ df = pd.concat(dfs, ignore_index=True)
251
+ return df