Coverage for /builds/kinetik161/ase/ase/build/supercells.py: 49.51%

103 statements  

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

1"""Helper functions for creating supercells.""" 

2 

3import numpy as np 

4from ase import Atoms 

5 

6 

7class SupercellError(Exception): 

8 """Use if construction of supercell fails""" 

9 

10 

11def get_deviation_from_optimal_cell_shape(cell, target_shape="sc", norm=None): 

12 r""" 

13 Calculates the deviation of the given cell metric from the ideal 

14 cell metric defining a certain shape. Specifically, the function 

15 evaluates the expression `\Delta = || Q \mathbf{h} - 

16 \mathbf{h}_{target}||_2`, where `\mathbf{h}` is the input 

17 metric (*cell*) and `Q` is a normalization factor (*norm*) 

18 while the target metric `\mathbf{h}_{target}` (via 

19 *target_shape*) represent simple cubic ('sc') or face-centered 

20 cubic ('fcc') cell shapes. 

21 

22 Parameters: 

23 

24 cell: 2D array of floats 

25 Metric given as a (3x3 matrix) of the input structure. 

26 target_shape: str 

27 Desired supercell shape. Can be 'sc' for simple cubic or 

28 'fcc' for face-centered cubic. 

29 norm: float 

30 Specify the normalization factor. This is useful to avoid 

31 recomputing the normalization factor when computing the 

32 deviation for a series of P matrices. 

33 

34 """ 

35 

36 if target_shape in ["sc", "simple-cubic"]: 

37 target_metric = np.eye(3) 

38 elif target_shape in ["fcc", "face-centered cubic"]: 

39 target_metric = 0.5 * np.array([[0, 1, 1], [1, 0, 1], [1, 1, 0]]) 

40 if not norm: 

41 norm = (np.linalg.det(cell) / np.linalg.det(target_metric))**(-1.0 / 3) 

42 return np.linalg.norm(norm * cell - target_metric) 

43 

44 

45def find_optimal_cell_shape( 

46 cell, 

47 target_size, 

48 target_shape, 

49 lower_limit=-2, 

50 upper_limit=2, 

51 verbose=False, 

52): 

53 """Returns the transformation matrix that produces a supercell 

54 corresponding to *target_size* unit cells with metric *cell* that 

55 most closely approximates the shape defined by *target_shape*. 

56 

57 Parameters: 

58 

59 cell: 2D array of floats 

60 Metric given as a (3x3 matrix) of the input structure. 

61 target_size: integer 

62 Size of desired super cell in number of unit cells. 

63 target_shape: str 

64 Desired supercell shape. Can be 'sc' for simple cubic or 

65 'fcc' for face-centered cubic. 

66 lower_limit: int 

67 Lower limit of search range. 

68 upper_limit: int 

69 Upper limit of search range. 

70 verbose: bool 

71 Set to True to obtain additional information regarding 

72 construction of transformation matrix. 

73 

74 """ 

75 

76 # Set up target metric 

77 if target_shape in ["sc", "simple-cubic"]: 

78 target_metric = np.eye(3) 

79 elif target_shape in ["fcc", "face-centered cubic"]: 

80 target_metric = 0.5 * np.array([[0, 1, 1], [1, 0, 1], [1, 1, 0]], 

81 dtype=float) 

82 if verbose: 

83 print("target metric (h_target):") 

84 print(target_metric) 

85 

86 # Normalize cell metric to reduce computation time during looping 

87 norm = (target_size * np.linalg.det(cell) / 

88 np.linalg.det(target_metric))**(-1.0 / 3) 

89 norm_cell = norm * cell 

90 if verbose: 

91 print("normalization factor (Q): %g" % norm) 

92 

93 # Approximate initial P matrix 

94 ideal_P = np.dot(target_metric, np.linalg.inv(norm_cell)) 

95 if verbose: 

96 print("idealized transformation matrix:") 

97 print(ideal_P) 

98 starting_P = np.array(np.around(ideal_P, 0), dtype=int) 

99 if verbose: 

100 print("closest integer transformation matrix (P_0):") 

101 print(starting_P) 

102 

103 # Prepare run. 

104 from itertools import product 

105 

106 best_score = 1e6 

107 optimal_P = None 

108 for dP in product(range(lower_limit, upper_limit + 1), repeat=9): 

109 dP = np.array(dP, dtype=int).reshape(3, 3) 

110 P = starting_P + dP 

111 if int(np.around(np.linalg.det(P), 0)) != target_size: 

112 continue 

113 score = get_deviation_from_optimal_cell_shape( 

114 np.dot(P, norm_cell), target_shape=target_shape, norm=1.0) 

115 if score < best_score: 

116 best_score = score 

117 optimal_P = P 

118 

119 if optimal_P is None: 

120 print("Failed to find a transformation matrix.") 

121 return None 

122 

123 # Finalize. 

124 if verbose: 

125 print("smallest score (|Q P h_p - h_target|_2): %f" % best_score) 

126 print("optimal transformation matrix (P_opt):") 

127 print(optimal_P) 

128 print("supercell metric:") 

129 print(np.round(np.dot(optimal_P, cell), 4)) 

130 print("determinant of optimal transformation matrix: %g" % 

131 np.linalg.det(optimal_P)) 

132 return optimal_P 

133 

134 

135def make_supercell(prim, P, *, wrap=True, order="cell-major", tol=1e-5): 

136 r"""Generate a supercell by applying a general transformation (*P*) to 

137 the input configuration (*prim*). 

138 

139 The transformation is described by a 3x3 integer matrix 

140 `\mathbf{P}`. Specifically, the new cell metric 

141 `\mathbf{h}` is given in terms of the metric of the input 

142 configuration `\mathbf{h}_p` by `\mathbf{P h}_p = 

143 \mathbf{h}`. 

144 

145 Parameters: 

146 

147 prim: ASE Atoms object 

148 Input configuration. 

149 P: 3x3 integer matrix 

150 Transformation matrix `\mathbf{P}`. 

151 wrap: bool 

152 wrap in the end 

153 order: str (default: "cell-major") 

154 how to order the atoms in the supercell 

155 

156 "cell-major": 

157 [atom1_shift1, atom2_shift1, ..., atom1_shift2, atom2_shift2, ...] 

158 i.e. run first over all the atoms in cell1 and then move to cell2. 

159 

160 "atom-major": 

161 [atom1_shift1, atom1_shift2, ..., atom2_shift1, atom2_shift2, ...] 

162 i.e. run first over atom1 in all the cells and then move to atom2. 

163 This may be the order preferred by most VASP users. 

164 

165 tol: float 

166 tolerance for wrapping 

167 """ 

168 

169 supercell_matrix = P 

170 supercell = clean_matrix(supercell_matrix @ prim.cell) 

171 

172 # cartesian lattice points 

173 lattice_points_frac = lattice_points_in_supercell(supercell_matrix) 

174 lattice_points = np.dot(lattice_points_frac, supercell) 

175 N = len(lattice_points) 

176 

177 if order == "cell-major": 

178 shifted = prim.positions[None, :, :] + lattice_points[:, None, :] 

179 elif order == "atom-major": 

180 shifted = prim.positions[:, None, :] + lattice_points[None, :, :] 

181 else: 

182 raise ValueError(f"invalid order: {order}") 

183 shifted_reshaped = shifted.reshape(-1, 3) 

184 

185 superatoms = Atoms(positions=shifted_reshaped, 

186 cell=supercell, 

187 pbc=prim.pbc) 

188 

189 # Copy over any other possible arrays, inspired by atoms.__imul__ 

190 for name, arr in prim.arrays.items(): 

191 if name == "positions": 

192 # This was added during construction of the super cell 

193 continue 

194 shape = (N * arr.shape[0], *arr.shape[1:]) 

195 if order == "cell-major": 

196 new_arr = np.repeat(arr[None, :], N, axis=0).reshape(shape) 

197 elif order == "atom-major": 

198 new_arr = np.repeat(arr[:, None], N, axis=1).reshape(shape) 

199 superatoms.set_array(name, new_arr) 

200 

201 # check number of atoms is correct 

202 n_target = abs(int(np.round(np.linalg.det(supercell_matrix) * len(prim)))) 

203 if n_target != len(superatoms): 

204 msg = "Number of atoms in supercell: {}, expected: {}".format( 

205 n_target, len(superatoms)) 

206 raise SupercellError(msg) 

207 

208 if wrap: 

209 superatoms.wrap(eps=tol) 

210 

211 return superatoms 

212 

213 

214def lattice_points_in_supercell(supercell_matrix): 

215 """Find all lattice points contained in a supercell. 

216 

217 Adapted from pymatgen, which is available under MIT license: 

218 The MIT License (MIT) Copyright (c) 2011-2012 MIT & The Regents of the 

219 University of California, through Lawrence Berkeley National Laboratory 

220 """ 

221 

222 diagonals = np.array([ 

223 [0, 0, 0], 

224 [0, 0, 1], 

225 [0, 1, 0], 

226 [0, 1, 1], 

227 [1, 0, 0], 

228 [1, 0, 1], 

229 [1, 1, 0], 

230 [1, 1, 1], 

231 ]) 

232 d_points = np.dot(diagonals, supercell_matrix) 

233 

234 mins = np.min(d_points, axis=0) 

235 maxes = np.max(d_points, axis=0) + 1 

236 

237 ar = np.arange(mins[0], maxes[0])[:, None] * np.array([1, 0, 0])[None, :] 

238 br = np.arange(mins[1], maxes[1])[:, None] * np.array([0, 1, 0])[None, :] 

239 cr = np.arange(mins[2], maxes[2])[:, None] * np.array([0, 0, 1])[None, :] 

240 

241 all_points = ar[:, None, None] + br[None, :, None] + cr[None, None, :] 

242 all_points = all_points.reshape((-1, 3)) 

243 

244 frac_points = np.dot(all_points, np.linalg.inv(supercell_matrix)) 

245 

246 tvects = frac_points[np.all(frac_points < 1 - 1e-10, axis=1) 

247 & np.all(frac_points >= -1e-10, axis=1)] 

248 assert len(tvects) == round(abs(np.linalg.det(supercell_matrix))) 

249 return tvects 

250 

251 

252def clean_matrix(matrix, eps=1e-12): 

253 """ clean from small values""" 

254 matrix = np.array(matrix) 

255 for ij in np.ndindex(matrix.shape): 

256 if abs(matrix[ij]) < eps: 

257 matrix[ij] = 0 

258 return matrix