Coverage for /builds/ase/ase/ase/geometry/minkowski_reduction.py: 96.95%

131 statements  

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

1# fmt: off 

2 

3import itertools 

4 

5import numpy as np 

6 

7from ase.cell import Cell 

8from ase.utils import pbc2pbc 

9 

10TOL = 1E-12 

11MAX_IT = 100000 # in practice this is not exceeded 

12 

13 

14class CycleChecker: 

15 

16 def __init__(self, d): 

17 assert d in [2, 3] 

18 

19 # worst case is the hexagonal cell in 2D and the fcc cell in 3D 

20 n = {2: 6, 3: 12}[d] 

21 

22 # max cycle length is total number of primtive cell descriptions 

23 max_cycle_length = np.prod([n - i for i in range(d)]) * np.prod(d) 

24 self.visited = np.zeros((max_cycle_length, 3 * d), dtype=int) 

25 

26 def add_site(self, H): 

27 # flatten array for simplicity 

28 H = H.ravel() 

29 

30 # check if site exists 

31 found = (self.visited == H).all(axis=1).any() 

32 

33 # shift all visited sites down and place current site at the top 

34 self.visited = np.roll(self.visited, 1, axis=0) 

35 self.visited[0] = H 

36 return found 

37 

38 

39def reduction_gauss(B, hu, hv): 

40 """Calculate a Gauss-reduced lattice basis (2D reduction).""" 

41 cycle_checker = CycleChecker(d=2) 

42 u = hu @ B 

43 v = hv @ B 

44 

45 for _ in range(MAX_IT): 

46 x = int(round(np.dot(u, v) / np.dot(u, u))) 

47 hu, hv = hv - x * hu, hu 

48 u = hu @ B 

49 v = hv @ B 

50 site = np.array([hu, hv]) 

51 if np.dot(u, u) >= np.dot(v, v) or cycle_checker.add_site(site): 

52 return hv, hu 

53 

54 raise RuntimeError(f"Gaussian basis not found after {MAX_IT} iterations") 

55 

56 

57def relevant_vectors_2D(u, v): 

58 cs = np.array([e for e in itertools.product([-1, 0, 1], repeat=2)]) 

59 vs = cs @ [u, v] 

60 indices = np.argsort(np.linalg.norm(vs, axis=1))[:7] 

61 return vs[indices], cs[indices] 

62 

63 

64def closest_vector(t0, u, v): 

65 t = t0 

66 a = np.zeros(2, dtype=int) 

67 rs, cs = relevant_vectors_2D(u, v) 

68 

69 dprev = float("inf") 

70 for _ in range(MAX_IT): 

71 ds = np.linalg.norm(rs + t, axis=1) 

72 index = np.argmin(ds) 

73 if index == 0 or ds[index] >= dprev: 

74 return a 

75 

76 dprev = ds[index] 

77 r = rs[index] 

78 kopt = int(round(-np.dot(t, r) / np.dot(r, r))) 

79 a += kopt * cs[index] 

80 t = t0 + a[0] * u + a[1] * v 

81 

82 raise RuntimeError(f"Closest vector not found after {MAX_IT} iterations") 

83 

84 

85def reduction_full(B): 

86 """Calculate a Minkowski-reduced lattice basis (3D reduction).""" 

87 cycle_checker = CycleChecker(d=3) 

88 H = np.eye(3, dtype=int) 

89 norms = np.linalg.norm(B, axis=1) 

90 

91 for _ in range(MAX_IT): 

92 # Sort vectors by norm 

93 H = H[np.argsort(norms, kind='merge')] 

94 

95 # Gauss-reduce smallest two vectors 

96 hw = H[2] 

97 hu, hv = reduction_gauss(B, H[0], H[1]) 

98 H = np.array([hu, hv, hw]) 

99 R = H @ B 

100 

101 # Orthogonalize vectors using Gram-Schmidt 

102 u, v, _ = R 

103 X = u / np.linalg.norm(u) 

104 Y = v - X * np.dot(v, X) 

105 Y /= np.linalg.norm(Y) 

106 

107 # Find closest vector to last element of R 

108 pu, pv, pw = R @ np.array([X, Y]).T 

109 nb = closest_vector(pw, pu, pv) 

110 

111 # Update basis 

112 H[2] = [nb[0], nb[1], 1] @ H 

113 R = H @ B 

114 

115 norms = np.linalg.norm(R, axis=1) 

116 if norms[2] >= norms[1] or cycle_checker.add_site(H): 

117 return R, H 

118 

119 raise RuntimeError(f"Reduced basis not found after {MAX_IT} iterations") 

120 

121 

122def is_minkowski_reduced(cell, pbc=True): 

123 """Tests if a cell is Minkowski-reduced. 

124 

125 Parameters: 

126 

127 cell: array 

128 The lattice basis to test (in row-vector format). 

129 pbc: array, optional 

130 The periodic boundary conditions of the cell (Default `True`). 

131 If `pbc` is provided, only periodic cell vectors are tested. 

132 

133 Returns: 

134 

135 is_reduced: bool 

136 True if cell is Minkowski-reduced, False otherwise. 

137 """ 

138 

139 """These conditions are due to Minkowski, but a nice description in English 

140 can be found in the thesis of Carine Jaber: "Algorithmic approaches to 

141 Siegel's fundamental domain", https://www.theses.fr/2017UBFCK006.pdf 

142 This is also good background reading for Minkowski reduction. 

143 

144 0D and 1D cells are trivially reduced. For 2D cells, the conditions which 

145 an already-reduced basis fulfil are: 

146 |b1| ≤ |b2| 

147 |b2| ≤ |b1 - b2| 

148 |b2| ≤ |b1 + b2| 

149 

150 For 3D cells, the conditions which an already-reduced basis fulfil are: 

151 |b1| ≤ |b2| ≤ |b3| 

152 

153 |b1 + b2| ≥ |b2| 

154 |b1 + b3| ≥ |b3| 

155 |b2 + b3| ≥ |b3| 

156 |b1 - b2| ≥ |b2| 

157 |b1 - b3| ≥ |b3| 

158 |b2 - b3| ≥ |b3| 

159 |b1 + b2 + b3| ≥ |b3| 

160 |b1 - b2 + b3| ≥ |b3| 

161 |b1 + b2 - b3| ≥ |b3| 

162 |b1 - b2 - b3| ≥ |b3| 

163 """ 

164 pbc = pbc2pbc(pbc) 

165 dim = pbc.sum() 

166 if dim <= 1: 

167 return True 

168 

169 if dim == 2: 

170 # reorder cell vectors to [shortest, longest, aperiodic] 

171 cell = cell.copy() 

172 cell[np.argmin(pbc)] = 0 

173 norms = np.linalg.norm(cell, axis=1) 

174 cell = cell[np.argsort(norms)[[1, 2, 0]]] 

175 

176 A = [[0, 1, 0], 

177 [1, -1, 0], 

178 [1, 1, 0]] 

179 lhs = np.linalg.norm(A @ cell, axis=1) 

180 norms = np.linalg.norm(cell, axis=1) 

181 rhs = norms[[0, 1, 1]] 

182 else: 

183 A = [[0, 1, 0], 

184 [0, 0, 1], 

185 [1, 1, 0], 

186 [1, 0, 1], 

187 [0, 1, 1], 

188 [1, -1, 0], 

189 [1, 0, -1], 

190 [0, 1, -1], 

191 [1, 1, 1], 

192 [1, -1, 1], 

193 [1, 1, -1], 

194 [1, -1, -1]] 

195 lhs = np.linalg.norm(A @ cell, axis=1) 

196 norms = np.linalg.norm(cell, axis=1) 

197 rhs = norms[[0, 1, 1, 2, 2, 1, 2, 2, 2, 2, 2, 2]] 

198 return (lhs >= rhs - TOL).all() 

199 

200 

201def minkowski_reduce(cell, pbc=True): 

202 """Calculate a Minkowski-reduced lattice basis. The reduced basis 

203 has the shortest possible vector lengths and has 

204 norm(a) <= norm(b) <= norm(c). 

205 

206 Implements the method described in: 

207 

208 Low-dimensional Lattice Basis Reduction Revisited 

209 Nguyen, Phong Q. and Stehlé, Damien, 

210 ACM Trans. Algorithms 5(4) 46:1--46:48, 2009 

211 :doi:`10.1145/1597036.1597050` 

212 

213 Parameters: 

214 

215 cell: array 

216 The lattice basis to reduce (in row-vector format). 

217 pbc: array, optional 

218 The periodic boundary conditions of the cell (Default `True`). 

219 If `pbc` is provided, only periodic cell vectors are reduced. 

220 

221 Returns: 

222 

223 rcell: array 

224 The reduced lattice basis. 

225 op: array 

226 The unimodular matrix transformation (rcell = op @ cell). 

227 """ 

228 cell = Cell(cell) 

229 pbc = pbc2pbc(pbc) 

230 dim = pbc.sum() 

231 op = np.eye(3, dtype=int) 

232 if is_minkowski_reduced(cell, pbc): 

233 return cell, op 

234 

235 if dim == 2: 

236 # permute cell so that first two vectors are the periodic ones 

237 perm = np.argsort(pbc, kind='merge')[::-1] # stable sort 

238 pcell = cell[perm][:, perm] 

239 

240 # perform gauss reduction 

241 norms = np.linalg.norm(pcell, axis=1) 

242 norms[2] = float("inf") 

243 indices = np.argsort(norms) 

244 op = op[indices] 

245 hu, hv = reduction_gauss(pcell, op[0], op[1]) 

246 op[0] = hu 

247 op[1] = hv 

248 

249 # undo above permutation 

250 invperm = np.argsort(perm) 

251 op = op[invperm][:, invperm] 

252 

253 # maintain cell handedness 

254 index = np.argmin(pbc) 

255 normal = np.cross(cell[index - 2], cell[index - 1]) 

256 normal /= np.linalg.norm(normal) 

257 

258 _cell = cell.copy() 

259 _cell[index] = normal 

260 _rcell = op @ cell 

261 _rcell[index] = normal 

262 if _cell.handedness != Cell(_rcell).handedness: 

263 op[index - 1] *= -1 

264 

265 elif dim == 3: 

266 _, op = reduction_full(cell) 

267 # maintain cell handedness 

268 if cell.handedness != Cell(op @ cell).handedness: 

269 op = -op 

270 

271 norms1 = np.sort(np.linalg.norm(cell, axis=1)) 

272 norms2 = np.sort(np.linalg.norm(op @ cell, axis=1)) 

273 if (norms2 > norms1 + TOL).any(): 

274 raise RuntimeError("Minkowski reduction failed") 

275 return op @ cell, op