Coverage for dynasor/trajectory/lammps_trajectory_reader.py: 99%
125 statements
« prev ^ index » next coverage.py v7.16.0, created at 2026-09-03 19:46 +0000
« prev ^ index » next coverage.py v7.16.0, created at 2026-09-03 19:46 +0000
1import numpy as np
2import re
4from collections import deque
5from typing import Optional
6from dynasor.trajectory.abstract_trajectory_reader import AbstractTrajectoryReader
7from dynasor.trajectory.trajectory_frame import ReaderFrame
8from itertools import count
9from numpy import array, arange, zeros
12class LammpsTrajectoryReader(AbstractTrajectoryReader):
13 """Read LAMMPS trajectory file
15 This is a naive (and comparatively slow) implementation,
16 written entirely in python.
18 Parameters
19 ----------
20 filename
21 Name of input file.
22 length_unit
23 Unit of length for the input trajectory (``'Angstrom'``, ``'nm'``, ``'pm'``, ``'fm'``).
24 time_unit
25 Unit of time for the input trajectory (``'fs'``, ``'ps'``, ``'ns'``).
26 force_unit
27 Unit of force for the input trajectory (``'eV/Angstrom'``, ``'eV/nm'``,
28 ``'kJ/mol/Angstrom'``, ``'kJ/mol/nm'``, ``'kcal/mol/Angstrom'``, ``'kcal/mol/nm'``,
29 ``'Hartree/Bohr'``).
30 """
32 def __init__(
33 self,
34 filename: str,
35 length_unit: Optional[str] = None,
36 time_unit: Optional[str] = None,
37 force_unit: Optional[str] = None
38 ):
40 if filename.endswith('.gz'):
41 import gzip
42 self._fh = gzip.open(filename, 'rt')
43 elif filename.endswith('.bz2'):
44 import bz2
45 self._fh = bz2.open(filename, 'rt')
46 else:
47 self._fh = open(filename, 'r')
49 self._open = True
50 regexp = r'^ITEM: (TIMESTEP|NUMBER OF ATOMS|BOX BOUNDS|ATOMS) ?(.*)$'
51 self._item_re = re.compile(regexp)
53 self._first_called = False
54 self._frame_index = count(0)
56 # set up units
57 self.set_unit_scaling_factors(length_unit, time_unit, force_unit)
59 # ITEM: TIMESTEP
60 # 81000
61 # ITEM: NUMBER OF ATOMS
62 # 1536
63 # ITEM: BOX BOUNDS pp pp pp
64 # 1.54223 26.5378
65 # 1.54223 26.5378
66 # 1.54223 26.5378
67 # ITEM: ATOMS id type x y z vx vy vz
68 # 247 1 3.69544 2.56202 3.27701 0.00433856 -0.00099307 -0.00486166
69 # 249 2 3.73324 3.05962 4.14359 0.00346029 0.00332502 -0.00731005
70 # 463 1 3.5465 4.12841 5.34888 0.000523332 0.00145597 -0.00418675
72 def _read_frame_header(self):
73 while True:
74 L = self._fh.readline()
75 m = self._item_re.match(L)
76 if not m:
77 if L == '':
78 self._fh.close()
79 self._open = False
80 raise StopIteration
81 if L.strip() == '':
82 continue
83 raise IOError('TRJ_reader: Failed to parse TRJ frame header')
84 if m.group(1) == 'TIMESTEP':
85 step = int(self._fh.readline())
86 elif m.group(1) == 'NUMBER OF ATOMS':
87 n_atoms = int(self._fh.readline())
88 elif m.group(1) == 'BOX BOUNDS':
89 bbounds = [deque(map(float, self._fh.readline().split()))
90 for _ in range(3)]
91 x = array(bbounds)
92 cell = np.diag(x[:, 1] - x[:, 0])
93 if x.shape == (3, 3):
94 cell[1, 0] = x[0, 2]
95 cell[2, 0] = x[1, 2]
96 cell[2, 1] = x[2, 2]
97 elif x.shape != (3, 2):
98 raise IOError('TRJ_reader: Malformed cell bounds')
99 elif m.group(1) == 'ATOMS': 99 ↛ 73line 99 didn't jump to line 73 because the condition on line 99 was always true
100 cols = tuple(m.group(2).split())
101 # At this point, there should be only atomic data left
102 return (step, n_atoms, cell, cols)
104 def _get_first(self):
105 # Read first frame, update state of self, create indices etc
106 step, N, cell, cols = self._read_frame_header()
107 self._n_atoms = N
108 self._step = step
109 self._cols = cols
110 self._cell = cell
112 def _all_in_cols(keys):
113 for k in keys:
114 if k not in cols:
115 return False
116 return True
118 self._x_map = None
119 if _all_in_cols(('id', 'xu', 'yu', 'zu')):
120 self._x_I = array(deque(map(cols.index, ('xu', 'yu', 'zu'))))
121 elif _all_in_cols(('id', 'x', 'y', 'z')):
122 self._x_I = array(deque(map(cols.index, ('x', 'y', 'z'))))
123 elif _all_in_cols(('id', 'xs', 'ys', 'zs')):
124 self._x_I = array(deque(map(cols.index, ('xs', 'ys', 'zs'))))
125 _x_factor = self._cell.diagonal()
126 # xs.shape == (n, 3)
127 self._x_map = lambda xs: xs * _x_factor
128 else:
129 raise RuntimeError('TRJ file must contain at least atom-id, x, y,'
130 ' and z coordinates to be useful.')
131 self._id_I = cols.index('id')
133 if _all_in_cols(('vx', 'vy', 'vz')):
134 self._v_I = array(deque(map(cols.index, ('vx', 'vy', 'vz'))))
135 else:
136 self._v_I = None
138 if _all_in_cols(('fx', 'fy', 'fz')):
139 self._f_I = array(deque(map(cols.index, ('fx', 'fy', 'fz'))))
140 else:
141 self._f_I = None
143 if 'type' in cols:
144 self._type_I = cols.index('type')
145 else:
146 self._type_I = None
148 data = array([list(map(float, self._fh.readline().split())) for _ in range(N)])
149 # data.shape == (N, Ncols)
150 II = np.asarray(data[:, self._id_I], dtype=np.int_)
151 # Unless dump is done for group 'all' ...
152 II[np.argsort(II)] = arange(len(II))
153 self._x = zeros((N, 3))
154 if self._x_map is None:
155 self._x[II] = data[:, self._x_I]
156 else:
157 self._x[II] = self._x_map(data[:, self._x_I])
158 if self._v_I is not None:
159 self._v = zeros((N, 3))
160 self._v[II] = data[:, self._v_I]
161 if self._f_I is not None:
162 self._f = zeros((N, 3))
163 self._f[II] = data[:, self._f_I]
165 self._first_called = True
167 def _get_next(self):
168 # get next frame, update state of self
169 step, N, cell, cols = self._read_frame_header()
170 assert self._n_atoms == N
171 assert self._cols == cols
172 self._step = step
173 self._cell = cell
175 data = array([deque(map(float, self._fh.readline().split()))
176 for _ in range(N)])
177 II = np.asarray(data[:, self._id_I], dtype=np.int_) - 1
178 if self._x_map is None:
179 self._x[II] = data[:, self._x_I]
180 else:
181 self._x[II] = self._x_map(data[:, self._x_I])
182 if self._v_I is not None:
183 self._v[II] = data[:, self._v_I]
184 if self._f_I is not None:
185 self._f[II] = data[:, self._f_I]
187 def __iter__(self):
188 return self
190 def close(self):
191 self._open = False
192 if not self._fh.closed:
193 self._fh.close()
195 def __next__(self):
196 if not self._open:
197 raise StopIteration
199 if self._first_called:
200 self._get_next()
201 else:
202 self._get_first()
204 return ReaderFrame(frame_index=next(self._frame_index),
205 n_atoms=int(self._n_atoms),
206 cell=self.x_factor * self._cell.copy('F'),
207 positions=self.x_factor * self._x,
208 velocities=None if self._v_I is None else self.v_factor * self._v,
209 forces=None if self._f_I is None else self.f_factor * self._f
210 )