Coverage for /builds/ase/ase/ase/io/dlp4.py: 89.12%

147 statements  

« prev     ^ index     » next       coverage.py v7.5.3, created at 2025-08-02 00:12 +0000

1# fmt: off 

2 

3""" Read/Write DL_POLY_4 CONFIG files """ 

4import itertools 

5import re 

6from typing import IO, Generator, Iterable, List, Optional, Tuple, Union 

7 

8import numpy as np 

9 

10from ase.atoms import Atoms 

11from ase.calculators.singlepoint import SinglePointCalculator 

12from ase.data import chemical_symbols 

13from ase.units import _amu, _auf, _auv 

14from ase.utils import reader, writer 

15 

16__all__ = ["read_dlp4", "write_dlp4"] 

17 

18# dlp4 labels will be registered in atoms.arrays[DLP4_LABELS_KEY] 

19DLP4_LABELS_KEY = "dlp4_labels" 

20DLP4_DISP_KEY = "dlp4_displacements" 

21DLP_F_SI = 1.0e-10 * _amu / (1.0e-12 * 1.0e-12) # Å*Da/ps^2 

22DLP_F_ASE = DLP_F_SI / _auf 

23DLP_V_SI = 1.0e-10 / 1.0e-12 # Å/ps 

24DLP_V_ASE = DLP_V_SI / _auv 

25 

26 

27def _get_frame_positions(fd: IO) -> Tuple[int, int, int, List[int]]: 

28 """Get positions of frames in HISTORY file.""" 

29 init_pos = fd.tell() 

30 

31 fd.seek(0) 

32 

33 record_len = len(fd.readline()) # system name, and record size 

34 

35 items = fd.readline().strip().split() 

36 

37 if len(items) not in (3, 5): 

38 raise RuntimeError("Cannot determine version of HISTORY file format.") 

39 

40 levcfg, imcon, natoms = map(int, items[0:3]) 

41 

42 if len(items) == 3: # DLPoly classic 

43 # we have to iterate over the entire file 

44 pos = [fd.tell() for line in fd if "timestep" in line] 

45 else: 

46 nframes = int(items[3]) 

47 pos = [((natoms * (levcfg + 2) + 4) * i + 3) * record_len 

48 for i in range(nframes)] 

49 

50 fd.seek(init_pos) 

51 return levcfg, imcon, natoms, pos 

52 

53 

54@reader 

55def read_dlp_history(fd: IO, 

56 index: Optional[Union[int, slice]] = None, 

57 symbols: Optional[List[str]] = None) -> List[Atoms]: 

58 """Read a HISTORY file. 

59 

60 Compatible with DLP4 and DLP_CLASSIC. 

61 

62 *Index* can be integer or a slice. 

63 

64 Provide a list of element strings to symbols to ignore naming 

65 from the HISTORY file. 

66 """ 

67 return list(iread_dlp_history(fd, index, symbols)) 

68 

69 

70@reader 

71def iread_dlp_history(fd: IO, 

72 index: Optional[Union[int, slice]] = None, 

73 symbols: Optional[List[str]] = None 

74 ) -> Generator[Atoms, None, None]: 

75 """Generator version of read_dlp_history 

76 

77 Compatible with DLP4 and DLP_CLASSIC. 

78 

79 *Index* can be integer or a slice. 

80 

81 Provide a list of element strings to symbols to ignore naming 

82 from the HISTORY file. 

83 """ 

84 levcfg, imcon, natoms, pos = _get_frame_positions(fd) 

85 

86 positions: Iterable[int] = () 

87 if index is None: 

88 positions = pos 

89 elif isinstance(index, int): 

90 positions = (pos[index],) 

91 elif isinstance(index, slice): 

92 positions = itertools.islice(pos, index.start, index.stop, index.step) 

93 

94 for pos_i in positions: 

95 fd.seek(pos_i + 1) 

96 yield read_single_image(fd, levcfg, imcon, natoms, is_trajectory=True, 

97 symbols=symbols) 

98 

99 

100@reader 

101def read_dlp4(fd: IO, 

102 symbols: Optional[List[str]] = None) -> Atoms: 

103 """Read a DL_POLY_4 config/revcon file. 

104 

105 Typically used indirectly through read("filename", atoms, format="dlp4"). 

106 

107 Can be unforgiving with custom chemical element names. 

108 Please complain to alin@elena.space for bugs.""" 

109 

110 fd.readline() 

111 levcfg, imcon = map(int, fd.readline().split()[0:2]) 

112 

113 position = fd.tell() 

114 is_trajectory = fd.readline().split()[0] == "timestep" 

115 

116 if not is_trajectory: 

117 # Difficult to distinguish between trajectory and non-trajectory 

118 # formats without reading one line too many. 

119 fd.seek(position) 

120 

121 natoms = (int(fd.readline().split()[2]) if is_trajectory else None) 

122 return read_single_image(fd, levcfg, imcon, natoms, is_trajectory, 

123 symbols) 

124 

125 

126def read_single_image(fd: IO, levcfg: int, imcon: int, 

127 natoms: Optional[int], is_trajectory: bool, 

128 symbols: Optional[List[str]] = None) -> Atoms: 

129 """ Read a DLP frame """ 

130 sym = symbols if symbols else [] 

131 positions = [] 

132 velocities = [] 

133 forces = [] 

134 charges = [] 

135 masses = [] 

136 disps = [] 

137 labels = [] 

138 

139 is_pbc = imcon > 0 

140 

141 cell = np.zeros((3, 3), dtype=np.float64) 

142 if is_pbc or is_trajectory: 

143 for j, line in enumerate(itertools.islice(fd, 3)): 

144 cell[j, :] = np.array(line.split(), dtype=np.float64) 

145 

146 i = 0 

147 for i, line in enumerate(itertools.islice(fd, natoms), 1): 

148 match = re.match(r"\s*([A-Za-z][a-z]?)(\S*)", line) 

149 if not match: 

150 raise OSError(f"Line {line} does not match valid format.") 

151 

152 symbol, label = match.group(1, 2) 

153 symbol = symbol.capitalize() 

154 

155 if is_trajectory: 

156 mass, charge, *disp = map(float, line.split()[2:]) 

157 charges.append(charge) 

158 masses.append(mass) 

159 disps.extend(disp if disp else [None]) # type: ignore[list-item] 

160 

161 if not symbols: 

162 if symbol not in chemical_symbols: 

163 raise ValueError( 

164 f"Symbol '{symbol}' not found in chemical symbols table" 

165 ) 

166 sym.append(symbol) 

167 

168 # make sure label is not empty 

169 labels.append(label if label else "") 

170 

171 positions.append([float(x) for x in next(fd).split()[:3]]) 

172 if levcfg > 0: 

173 velocities.append([float(x) * DLP_V_ASE 

174 for x in next(fd).split()[:3]]) 

175 if levcfg > 1: 

176 forces.append([float(x) * DLP_F_ASE 

177 for x in next(fd).split()[:3]]) 

178 

179 if symbols and (i != len(symbols)): 

180 raise ValueError( 

181 f"Counter is at {i} but {len(symbols)} symbols provided." 

182 ) 

183 

184 if imcon == 0: 

185 pbc = (False, False, False) 

186 elif imcon in (1, 2, 3): 

187 pbc = (True, True, True) 

188 elif imcon == 6: 

189 pbc = (True, True, False) 

190 elif imcon in (4, 5): 

191 raise ValueError(f"Unsupported imcon: {imcon}") 

192 else: 

193 raise ValueError(f"Invalid imcon: {imcon}") 

194 

195 atoms = Atoms(positions=positions, 

196 symbols=sym, 

197 cell=cell, 

198 pbc=pbc, 

199 # Cell is centered around (0, 0, 0) in dlp4: 

200 celldisp=-cell.sum(axis=0) / 2) 

201 

202 if is_trajectory: 

203 atoms.set_masses(masses) 

204 atoms.set_array(DLP4_DISP_KEY, disps, float) 

205 atoms.set_initial_charges(charges) 

206 

207 atoms.set_array(DLP4_LABELS_KEY, labels, str) 

208 if levcfg > 0: 

209 atoms.set_velocities(velocities) 

210 if levcfg > 1: 

211 atoms.calc = SinglePointCalculator(atoms, forces=forces) 

212 

213 return atoms 

214 

215 

216@writer 

217def write_dlp4(fd: IO, atoms: Atoms, 

218 levcfg: int = 0, 

219 title: str = "CONFIG generated by ASE"): 

220 """Write a DL_POLY_4 config file. 

221 

222 Typically used indirectly through write("filename", atoms, format="dlp4"). 

223 

224 Can be unforgiven with custom chemical element names. 

225 Please complain to alin@elena.space in case of bugs""" 

226 

227 def float_format(flt): 

228 return format(flt, "20.10f") 

229 

230 natoms = len(atoms) 

231 

232 if tuple(atoms.pbc) == (False, False, False): 

233 imcon = 0 

234 elif tuple(atoms.pbc) == (True, True, True): 

235 imcon = 3 

236 elif tuple(atoms.pbc) == (True, True, False): 

237 imcon = 6 

238 else: 

239 raise ValueError(f"Unsupported pbc: {atoms.pbc}. " 

240 "Supported pbc are 000, 110, and 111.") 

241 

242 print(f"{title:72s}", file=fd) 

243 print(f"{levcfg:10d}{imcon:10d} {natoms}", file=fd) 

244 

245 if imcon > 0: 

246 for row in atoms.get_cell(): 

247 print("".join(map(float_format, row)), file=fd) 

248 

249 vels = atoms.get_velocities() / DLP_V_ASE if levcfg > 0 else None 

250 forces = atoms.get_forces() / DLP_F_ASE if levcfg > 1 else None 

251 

252 labels = atoms.arrays.get(DLP4_LABELS_KEY) 

253 

254 for i, atom in enumerate(atoms): 

255 

256 sym = atom.symbol 

257 if labels is not None: 

258 sym += labels[i] 

259 

260 print(f"{sym:8s}{atom.index + 1:10d}", file=fd) 

261 

262 to_write = (atom.x, atom.y, atom.z) 

263 print("".join(map(float_format, to_write)), file=fd) 

264 

265 if levcfg > 0: 

266 to_write = (vels[atom.index, :] 

267 if vels is not None else (0.0, 0.0, 0.0)) 

268 print("".join(map(float_format, to_write)), file=fd) 

269 

270 if levcfg > 1: 

271 to_write = (forces[atom.index, :] 

272 if forces is not None else (0.0, 0.0, 0.0)) 

273 print("".join(map(float_format, to_write)), file=fd)