Coverage for /builds/kinetik161/ase/ase/calculators/lammps/coordinatetransform.py: 95.83%

72 statements  

« prev     ^ index     » next       coverage.py v7.2.7, created at 2023-12-10 11:04 +0000

1"""Prism""" 

2import warnings 

3from typing import Sequence 

4 

5import numpy as np 

6 

7from ase.geometry import wrap_positions 

8 

9 

10def calc_box_parameters(cell: np.ndarray) -> np.ndarray: 

11 """Calculate box parameters 

12 

13 https://docs.lammps.org/Howto_triclinic.html 

14 """ 

15 ax = np.sqrt(cell[0] @ cell[0]) 

16 bx = cell[0] @ cell[1] / ax 

17 by = np.sqrt(cell[1] @ cell[1] - bx ** 2) 

18 cx = cell[0] @ cell[2] / ax 

19 cy = (cell[1] @ cell[2] - bx * cx) / by 

20 cz = np.sqrt(cell[2] @ cell[2] - cx ** 2 - cy ** 2) 

21 return np.array((ax, by, cz, bx, cx, cy)) 

22 

23 

24def calc_rotated_cell(cell: np.ndarray) -> np.ndarray: 

25 """Calculate rotated cell in LAMMPS coordinates 

26 

27 Parameters 

28 ---------- 

29 cell : np.ndarray 

30 Cell to be rotated. 

31 

32 Returns 

33 ------- 

34 rotated_cell : np.ndarray 

35 Rotated cell represented by a lower triangular matrix. 

36 """ 

37 ax, by, cz, bx, cx, cy = calc_box_parameters(cell) 

38 return np.array(((ax, 0.0, 0.0), (bx, by, 0.0), (cx, cy, cz))) 

39 

40 

41def calc_reduced_cell(cell: np.ndarray, pbc: Sequence[bool]) -> np.ndarray: 

42 """Calculate LAMMPS cell with short lattice basis vectors 

43 

44 The lengths of the second and the third lattice basis vectors, b and c, are 

45 shortened with keeping the same periodicity of the system. b is modified by 

46 adding multiple a vectors, and c is modified by adding first multiple b 

47 vectors and then multiple a vectors. 

48 

49 Parameters 

50 ---------- 

51 cell : np.ndarray 

52 Cell to be reduced. This must be already a lower triangular matrix. 

53 pbc : Sequence[bool] 

54 True if the system is periodic along the corresponding direction. 

55 

56 Returns 

57 ------- 

58 reduced_cell : np.ndarray 

59 Reduced cell. `xx`, `yy`, `zz` are the same as the original cell, 

60 and `abs(xy) <= xx`, `abs(xz) <= xx`, `abs(yz) <= yy`. 

61 """ 

62 # cell 1 (reduced) <- cell 2 (original) 

63 # o-----------------------------/==o-----------------------------/--o 

64 # \ /--/ \ /--/ 

65 # \ /--/ \ /--/ 

66 # \ 1 /--/ \ 2 /--/ 

67 # \ /--/ \ /--/ 

68 # \ /--/ \ /--/ 

69 # o==/-----------------------------o--/ 

70 reduced_cell = cell.copy() 

71 # Order in which off-diagonal elements are checked for strong tilt 

72 # yz is updated before xz so that the latter does not affect the former 

73 flip_order = ((1, 0), (2, 1), (2, 0)) 

74 for i, j in flip_order: 

75 if not pbc[j]: 

76 continue 

77 ratio = reduced_cell[i, j] / reduced_cell[j, j] 

78 if abs(ratio) > 0.5: 

79 reduced_cell[i] -= reduced_cell[j] * np.round(ratio) 

80 return reduced_cell 

81 

82 

83class Prism: 

84 """The representation of the unit cell in LAMMPS 

85 

86 The main purpose of the prism-object is to create suitable string 

87 representations of prism limits and atom positions within the prism. 

88 

89 Parameters 

90 ---------- 

91 cell : np.ndarray 

92 Cell in ASE coordinate system. 

93 pbc : one or three bool 

94 Periodic boundary conditions flags. 

95 reduce_cell : bool 

96 If True, the LAMMPS cell is reduced for short lattice basis vectors. 

97 The atomic positions are always wraped into the reduced cell, 

98 regardress of `wrap` in `vector_to_lammps` and `vector_to_ase`. 

99 tolerance : float 

100 Precision for skewness test. 

101 

102 Methods 

103 ------- 

104 vector_to_lammps 

105 Rotate vectors from ASE to LAMMPS coordinates. 

106 Positions can be further wrapped into the LAMMPS cell by `wrap=True`. 

107 

108 vector_to_ase 

109 Rotate vectors from LAMMPS to ASE coordinates. 

110 Positions can be further wrapped into the LAMMPS cell by `wrap=True`. 

111 

112 Notes 

113 ----- 

114 LAMMPS prefers triangular matrixes without a strong tilt. 

115 Therefore the 'Prism'-object contains three coordinate systems: 

116 

117 - ase_cell (the simulated system in the ASE coordination system) 

118 - lammps_tilt (ase-cell rotated to be an lower triangular matrix) 

119 - lammps_cell (same volume as tilted cell, but reduce edge length) 

120 

121 The translation between 'ase_cell' and 'lammps_tilt' is done with a 

122 rotation matrix 'rot_mat'. (Mathematically this is a QR decomposition.) 

123 

124 The transformation between 'lammps_tilt' and 'lammps_cell' is done by 

125 changing the off-diagonal elements. 

126 

127 Depending on the option `reduce`, vectors in ASE coordinates are 

128 transformed either `lammps_tilt` or `lammps_cell`. 

129 

130 The vector conversion can fail as depending on the simulation run LAMMPS 

131 might have changed the simulation box significantly. This is for example a 

132 problem with hexagonal cells. LAMMPS might also wrap atoms across periodic 

133 boundaries, which can lead to problems for example NEB calculations. 

134 """ 

135 

136 # !TODO: derive tolerance from cell-dimensions 

137 def __init__( 

138 self, 

139 cell: np.ndarray, 

140 pbc: bool = True, 

141 reduce_cell: bool = False, 

142 tolerance: float = 1.0e-8, 

143 ): 

144 # rot_mat * lammps_tilt^T = ase_cell^T 

145 # => lammps_tilt * rot_mat^T = ase_cell 

146 # => lammps_tilt = ase_cell * rot_mat 

147 # LAMMPS requires positive diagonal elements of the triangular matrix. 

148 # The diagonals of `lammps_tilt` are always positive by construction. 

149 self.lammps_tilt = calc_rotated_cell(cell) 

150 self.rot_mat = np.linalg.solve(self.lammps_tilt, cell).T 

151 self.ase_cell = cell 

152 self.tolerance = tolerance 

153 self.pbc = np.zeros(3, bool) + pbc 

154 self.lammps_cell = calc_reduced_cell(self.lammps_tilt, self.pbc) 

155 self.is_reduced = reduce_cell 

156 

157 @property 

158 def cell(self) -> np.ndarray: 

159 return self.lammps_cell if self.is_reduced else self.lammps_tilt 

160 

161 def get_lammps_prism(self) -> np.ndarray: 

162 """Return box parameters of the rotated cell in LAMMPS coordinates 

163 

164 Returns 

165 ------- 

166 np.ndarray 

167 xhi - xlo, yhi - ylo, zhi - zlo, xy, xz, yz 

168 """ 

169 return self.cell[(0, 1, 2, 1, 2, 2), (0, 1, 2, 0, 0, 1)] 

170 

171 def update_cell(self, lammps_cell: np.ndarray) -> np.ndarray: 

172 """Rotate new LAMMPS cell into ASE coordinate system 

173 

174 Parameters 

175 ---------- 

176 lammps_cell : np.ndarray 

177 New Cell in LAMMPS coordinates received after executing LAMMPS 

178 

179 Returns 

180 ------- 

181 np.ndarray 

182 New cell in ASE coordinates 

183 """ 

184 # Transformation: integer matrix 

185 # lammps_cell * transformation = lammps_tilt 

186 transformation = np.linalg.solve(self.lammps_cell, self.lammps_tilt) 

187 

188 if self.is_reduced: 

189 self.lammps_cell = lammps_cell 

190 self.lammps_tilt = lammps_cell @ transformation 

191 else: 

192 self.lammps_tilt = lammps_cell 

193 self.lammps_cell = calc_reduced_cell(self.lammps_tilt, self.pbc) 

194 

195 # try to detect potential flips in lammps 

196 # (lammps minimizes the cell-vector lengths) 

197 new_ase_cell = self.lammps_tilt @ self.rot_mat.T 

198 # assuming the cell changes are mostly isotropic 

199 new_vol = np.linalg.det(new_ase_cell) 

200 old_vol = np.linalg.det(self.ase_cell) 

201 test_residual = self.ase_cell.copy() 

202 test_residual *= (new_vol / old_vol) ** (1.0 / 3.0) 

203 test_residual -= new_ase_cell 

204 if any( 

205 np.linalg.norm(test_residual, axis=1) 

206 > 0.5 * np.linalg.norm(self.ase_cell, axis=1) 

207 ): 

208 warnings.warn( 

209 "Significant simulation cell changes from LAMMPS detected. " 

210 "Backtransformation to ASE might fail!" 

211 ) 

212 return new_ase_cell 

213 

214 def vector_to_lammps( 

215 self, 

216 vec: np.ndarray, 

217 wrap: bool = False, 

218 ) -> np.ndarray: 

219 """Rotate vectors from ASE to LAMMPS coordinates 

220 

221 Parameters 

222 ---------- 

223 vec : np.ndarray 

224 Vectors in ASE coordinates to be rotated into LAMMPS coordinates 

225 wrap : bool 

226 If True, the vectors are wrapped into the cell 

227 

228 Returns 

229 ------- 

230 np.array 

231 Vectors in LAMMPS coordinates 

232 """ 

233 # !TODO: right eps-limit 

234 # lammps might not like atoms outside the cell 

235 if wrap or self.is_reduced: 

236 return wrap_positions( 

237 vec @ self.rot_mat, 

238 cell=self.cell, 

239 pbc=self.pbc, 

240 eps=1e-18, 

241 ) 

242 return vec @ self.rot_mat 

243 

244 def vector_to_ase( 

245 self, 

246 vec: np.ndarray, 

247 wrap: bool = False, 

248 ) -> np.ndarray: 

249 """Rotate vectors from LAMMPS to ASE coordinates 

250 

251 Parameters 

252 ---------- 

253 vec : np.ndarray 

254 Vectors in LAMMPS coordinates to be rotated into ASE coordinates 

255 wrap : bool 

256 If True, the vectors are wrapped into the cell 

257 

258 Returns 

259 ------- 

260 np.ndarray 

261 Vectors in ASE coordinates 

262 """ 

263 if wrap or self.is_reduced: 

264 # fractional in `lammps_tilt` (the same shape as ASE cell) 

265 fractional = np.linalg.solve(self.lammps_tilt.T, vec.T).T 

266 # wrap into 0 to 1 for periodic directions 

267 fractional -= np.floor(fractional) * self.pbc 

268 # Cartesian coordinates wrapped into `lammps_tilt` 

269 vec = fractional @ self.lammps_tilt 

270 # rotate back to the ASE cell 

271 return vec @ self.rot_mat.T 

272 

273 def tensor2_to_ase(self, tensor: np.ndarray) -> np.ndarray: 

274 """Rotate a second order tensor from LAMMPS to ASE coordinates 

275 

276 Parameters 

277 ---------- 

278 tensor : np.ndarray 

279 Tensor in LAMMPS coordinates to be rotated into ASE coordinates 

280 

281 Returns 

282 ------- 

283 np.ndarray 

284 Tensor in ASE coordinates 

285 """ 

286 return self.rot_mat @ tensor @ self.rot_mat.T 

287 

288 def is_skewed(self) -> bool: 

289 """Test if the lammps cell is skewed, i.e., monoclinic or triclinic. 

290 

291 Returns 

292 ------- 

293 bool 

294 True if the lammps cell is skewed. 

295 """ 

296 cell_sq = self.cell ** 2 

297 on_diag = np.sum(np.diag(cell_sq)) 

298 off_diag = np.sum(np.tril(cell_sq, -1)) 

299 return off_diag / on_diag > self.tolerance