Coverage for /builds/kinetik161/ase/ase/geometry/geometry.py: 97.56%

205 statements  

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

1# Copyright (C) 2010, Jesper Friis 

2# (see accompanying license files for details). 

3 

4"""Utility tools for atoms/geometry manipulations. 

5 - convenient creation of slabs and interfaces of 

6different orientations. 

7 - detection of duplicate atoms / atoms within cutoff radius 

8""" 

9 

10import itertools 

11 

12import numpy as np 

13 

14from ase.cell import Cell 

15from ase.geometry import complete_cell 

16from ase.geometry.minkowski_reduction import minkowski_reduce 

17from ase.utils import pbc2pbc 

18 

19 

20def translate_pretty(fractional, pbc): 

21 """Translates atoms such that fractional positions are minimized.""" 

22 

23 for i in range(3): 

24 if not pbc[i]: 

25 continue 

26 

27 indices = np.argsort(fractional[:, i]) 

28 sp = fractional[indices, i] 

29 

30 widths = (np.roll(sp, 1) - sp) % 1.0 

31 fractional[:, i] -= sp[np.argmin(widths)] 

32 fractional[:, i] %= 1.0 

33 return fractional 

34 

35 

36def wrap_positions(positions, cell, pbc=True, center=(0.5, 0.5, 0.5), 

37 pretty_translation=False, eps=1e-7): 

38 """Wrap positions to unit cell. 

39 

40 Returns positions changed by a multiple of the unit cell vectors to 

41 fit inside the space spanned by these vectors. See also the 

42 :meth:`ase.Atoms.wrap` method. 

43 

44 Parameters: 

45 

46 positions: float ndarray of shape (n, 3) 

47 Positions of the atoms 

48 cell: float ndarray of shape (3, 3) 

49 Unit cell vectors. 

50 pbc: one or 3 bool 

51 For each axis in the unit cell decides whether the positions 

52 will be moved along this axis. 

53 center: three float 

54 The positons in fractional coordinates that the new positions 

55 will be nearest possible to. 

56 pretty_translation: bool 

57 Translates atoms such that fractional coordinates are minimized. 

58 eps: float 

59 Small number to prevent slightly negative coordinates from being 

60 wrapped. 

61 

62 Example: 

63 

64 >>> from ase.geometry import wrap_positions 

65 >>> wrap_positions([[-0.1, 1.01, -0.5]], 

66 ... [[1, 0, 0], [0, 1, 0], [0, 0, 4]], 

67 ... pbc=[1, 1, 0]) 

68 array([[ 0.9 , 0.01, -0.5 ]]) 

69 """ 

70 

71 if not hasattr(center, '__len__'): 

72 center = (center,) * 3 

73 

74 pbc = pbc2pbc(pbc) 

75 shift = np.asarray(center) - 0.5 - eps 

76 

77 # Don't change coordinates when pbc is False 

78 shift[np.logical_not(pbc)] = 0.0 

79 

80 assert np.asarray(cell)[np.asarray(pbc)].any(axis=1).all(), (cell, pbc) 

81 

82 cell = complete_cell(cell) 

83 fractional = np.linalg.solve(cell.T, 

84 np.asarray(positions).T).T - shift 

85 

86 if pretty_translation: 

87 fractional = translate_pretty(fractional, pbc) 

88 shift = np.asarray(center) - 0.5 

89 shift[np.logical_not(pbc)] = 0.0 

90 fractional += shift 

91 else: 

92 for i, periodic in enumerate(pbc): 

93 if periodic: 

94 fractional[:, i] %= 1.0 

95 fractional[:, i] += shift[i] 

96 

97 return np.dot(fractional, cell) 

98 

99 

100def get_layers(atoms, miller, tolerance=0.001): 

101 """Returns two arrays describing which layer each atom belongs 

102 to and the distance between the layers and origo. 

103 

104 Parameters: 

105 

106 miller: 3 integers 

107 The Miller indices of the planes. Actually, any direction 

108 in reciprocal space works, so if a and b are two float 

109 vectors spanning an atomic plane, you can get all layers 

110 parallel to this with miller=np.cross(a,b). 

111 tolerance: float 

112 The maximum distance in Angstrom along the plane normal for 

113 counting two atoms as belonging to the same plane. 

114 

115 Returns: 

116 

117 tags: array of integres 

118 Array of layer indices for each atom. 

119 levels: array of floats 

120 Array of distances in Angstrom from each layer to origo. 

121 

122 Example: 

123 

124 >>> import numpy as np 

125 

126 >>> from ase.spacegroup import crystal 

127 >>> from ase.geometry.geometry import get_layers 

128 

129 >>> atoms = crystal('Al', [(0,0,0)], spacegroup=225, cellpar=4.05) 

130 >>> np.round(atoms.positions, decimals=5) # doctest: +NORMALIZE_WHITESPACE 

131 array([[ 0. , 0. , 0. ], 

132 [ 0. , 2.025, 2.025], 

133 [ 2.025, 0. , 2.025], 

134 [ 2.025, 2.025, 0. ]]) 

135 >>> get_layers(atoms, (0,0,1)) # doctest: +ELLIPSIS 

136 (array([0, 1, 1, 0]...), array([ 0. , 2.025])) 

137 """ 

138 miller = np.asarray(miller) 

139 

140 metric = np.dot(atoms.cell, atoms.cell.T) 

141 c = np.linalg.solve(metric.T, miller.T).T 

142 miller_norm = np.sqrt(np.dot(c, miller)) 

143 d = np.dot(atoms.get_scaled_positions(), miller) / miller_norm 

144 

145 keys = np.argsort(d) 

146 ikeys = np.argsort(keys) 

147 mask = np.concatenate(([True], np.diff(d[keys]) > tolerance)) 

148 tags = np.cumsum(mask)[ikeys] 

149 if tags.min() == 1: 

150 tags -= 1 

151 

152 levels = d[keys][mask] 

153 return tags, levels 

154 

155 

156def naive_find_mic(v, cell): 

157 """Finds the minimum-image representation of vector(s) v. 

158 Safe to use for (pbc.all() and (norm(v_mic) < 0.5 * min(cell.lengths()))). 

159 Can otherwise fail for non-orthorhombic cells. 

160 Described in: 

161 W. Smith, "The Minimum Image Convention in Non-Cubic MD Cells", 1989, 

162 http://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.57.1696.""" 

163 f = Cell(cell).scaled_positions(v) 

164 f -= np.floor(f + 0.5) 

165 vmin = f @ cell 

166 vlen = np.linalg.norm(vmin, axis=1) 

167 return vmin, vlen 

168 

169 

170def general_find_mic(v, cell, pbc=True): 

171 """Finds the minimum-image representation of vector(s) v. Using the 

172 Minkowski reduction the algorithm is relatively slow but safe for any cell. 

173 """ 

174 

175 cell = complete_cell(cell) 

176 rcell, _ = minkowski_reduce(cell, pbc=pbc) 

177 positions = wrap_positions(v, rcell, pbc=pbc, eps=0) 

178 

179 # In a Minkowski-reduced cell we only need to test nearest neighbors, 

180 # or "Voronoi-relevant" vectors. These are a subset of combinations of 

181 # [-1, 0, 1] of the reduced cell vectors. 

182 

183 # Define ranges [-1, 0, 1] for periodic directions and [0] for aperiodic 

184 # directions. 

185 ranges = [np.arange(-1 * p, p + 1) for p in pbc] 

186 

187 # Get Voronoi-relevant vectors. 

188 # Pre-pend (0, 0, 0) to resolve issue #772 

189 hkls = np.array([(0, 0, 0)] + list(itertools.product(*ranges))) 

190 vrvecs = hkls @ rcell 

191 

192 # Map positions into neighbouring cells. 

193 x = positions + vrvecs[:, None] 

194 

195 # Find minimum images 

196 lengths = np.linalg.norm(x, axis=2) 

197 indices = np.argmin(lengths, axis=0) 

198 vmin = x[indices, np.arange(len(positions)), :] 

199 vlen = lengths[indices, np.arange(len(positions))] 

200 return vmin, vlen 

201 

202 

203def find_mic(v, cell, pbc=True): 

204 """Finds the minimum-image representation of vector(s) v using either one 

205 of two find mic algorithms depending on the given cell, v and pbc.""" 

206 

207 cell = Cell(cell) 

208 pbc = cell.any(1) & pbc2pbc(pbc) 

209 dim = np.sum(pbc) 

210 v = np.asarray(v) 

211 single = v.ndim == 1 

212 v = np.atleast_2d(v) 

213 

214 if dim > 0: 

215 naive_find_mic_is_safe = False 

216 if dim == 3: 

217 vmin, vlen = naive_find_mic(v, cell) 

218 # naive find mic is safe only for the following condition 

219 if (vlen < 0.5 * min(cell.lengths())).all(): 

220 naive_find_mic_is_safe = True # hence skip Minkowski reduction 

221 

222 if not naive_find_mic_is_safe: 

223 vmin, vlen = general_find_mic(v, cell, pbc=pbc) 

224 else: 

225 vmin = v.copy() 

226 vlen = np.linalg.norm(vmin, axis=1) 

227 

228 if single: 

229 return vmin[0], vlen[0] 

230 else: 

231 return vmin, vlen 

232 

233 

234def conditional_find_mic(vectors, cell, pbc): 

235 """Return list of vector arrays and corresponding list of vector lengths 

236 for a given list of vector arrays. The minimum image convention is applied 

237 if cell and pbc are set. Can be used like a simple version of get_distances. 

238 """ 

239 if (cell is None) != (pbc is None): 

240 raise ValueError("cell or pbc must be both set or both be None") 

241 if cell is not None: 

242 mics = [find_mic(v, cell, pbc) for v in vectors] 

243 vectors, vector_lengths = zip(*mics) 

244 else: 

245 vector_lengths = np.linalg.norm(vectors, axis=2) 

246 return [np.asarray(v) for v in vectors], vector_lengths 

247 

248 

249def get_angles(v0, v1, cell=None, pbc=None): 

250 """Get angles formed by two lists of vectors. 

251 

252 Calculate angle in degrees between vectors v0 and v1 

253 

254 Set a cell and pbc to enable minimum image 

255 convention, otherwise angles are taken as-is. 

256 """ 

257 (v0, v1), (nv0, nv1) = conditional_find_mic([v0, v1], cell, pbc) 

258 

259 if (nv0 <= 0).any() or (nv1 <= 0).any(): 

260 raise ZeroDivisionError('Undefined angle') 

261 v0n = v0 / nv0[:, np.newaxis] 

262 v1n = v1 / nv1[:, np.newaxis] 

263 # We just normalized the vectors, but in some cases we can get 

264 # bad things like 1+2e-16. These we clip away: 

265 angles = np.arccos(np.einsum('ij,ij->i', v0n, v1n).clip(-1.0, 1.0)) 

266 return np.degrees(angles) 

267 

268 

269def get_angles_derivatives(v0, v1, cell=None, pbc=None): 

270 """Get derivatives of angles formed by two lists of vectors (v0, v1) w.r.t. 

271 Cartesian coordinates in degrees. 

272 

273 Set a cell and pbc to enable minimum image 

274 convention, otherwise derivatives of angles are taken as-is. 

275 

276 There is a singularity in the derivatives for sin(angle) -> 0 for which 

277 a ZeroDivisionError is raised. 

278 

279 Derivative output format: [[dx_a0, dy_a0, dz_a0], [...], [..., dz_a2]. 

280 """ 

281 (v0, v1), (nv0, nv1) = conditional_find_mic([v0, v1], cell, pbc) 

282 

283 angles = np.radians(get_angles(v0, v1, cell=cell, pbc=pbc)) 

284 sin_angles = np.sin(angles) 

285 cos_angles = np.cos(angles) 

286 if (sin_angles == 0.).any(): # identify singularities 

287 raise ZeroDivisionError('Singularity for derivative of a planar angle') 

288 

289 product = nv0 * nv1 

290 deriv_d0 = (-(v1 / product[:, np.newaxis] # derivatives by atom 0 

291 - np.einsum('ij,i->ij', v0, cos_angles / nv0**2)) 

292 / sin_angles[:, np.newaxis]) 

293 deriv_d2 = (-(v0 / product[:, np.newaxis] # derivatives by atom 2 

294 - np.einsum('ij,i->ij', v1, cos_angles / nv1**2)) 

295 / sin_angles[:, np.newaxis]) 

296 deriv_d1 = -(deriv_d0 + deriv_d2) # derivatives by atom 1 

297 derivs = np.stack((deriv_d0, deriv_d1, deriv_d2), axis=1) 

298 return np.degrees(derivs) 

299 

300 

301def get_dihedrals(v0, v1, v2, cell=None, pbc=None): 

302 """Get dihedral angles formed by three lists of vectors. 

303 

304 Calculate dihedral angle (in degrees) between the vectors a0->a1, 

305 a1->a2 and a2->a3, written as v0, v1 and v2. 

306 

307 Set a cell and pbc to enable minimum image 

308 convention, otherwise angles are taken as-is. 

309 """ 

310 (v0, v1, v2), (_, nv1, _) = conditional_find_mic([v0, v1, v2], cell, pbc) 

311 

312 v1n = v1 / nv1[:, np.newaxis] 

313 # v, w: projection of v0, v2 onto plane perpendicular to v1 

314 v = -v0 - np.einsum('ij,ij,ik->ik', -v0, v1n, v1n) 

315 w = v2 - np.einsum('ij,ij,ik->ik', v2, v1n, v1n) 

316 

317 # formula returns 0 for undefined dihedrals; prefer ZeroDivisionError 

318 undefined_v = np.all(v == 0.0, axis=1) 

319 undefined_w = np.all(w == 0.0, axis=1) 

320 if np.any([undefined_v, undefined_w]): 

321 raise ZeroDivisionError('Undefined dihedral for planar inner angle') 

322 

323 x = np.einsum('ij,ij->i', v, w) 

324 y = np.einsum('ij,ij->i', np.cross(v1n, v, axis=1), w) 

325 dihedrals = np.arctan2(y, x) # dihedral angle in [-pi, pi] 

326 dihedrals[dihedrals < 0.] += 2 * np.pi # dihedral angle in [0, 2*pi] 

327 return np.degrees(dihedrals) 

328 

329 

330def get_dihedrals_derivatives(v0, v1, v2, cell=None, pbc=None): 

331 """Get derivatives of dihedrals formed by three lists of vectors 

332 (v0, v1, v2) w.r.t Cartesian coordinates in degrees. 

333 

334 Set a cell and pbc to enable minimum image 

335 convention, otherwise dihedrals are taken as-is. 

336 

337 Derivative output format: [[dx_a0, dy_a0, dz_a0], ..., [..., dz_a3]]. 

338 """ 

339 (v0, v1, v2), (nv0, nv1, nv2) = conditional_find_mic([v0, v1, v2], cell, 

340 pbc) 

341 

342 v0 /= nv0[:, np.newaxis] 

343 v1 /= nv1[:, np.newaxis] 

344 v2 /= nv2[:, np.newaxis] 

345 normal_v01 = np.cross(v0, v1, axis=1) 

346 normal_v12 = np.cross(v1, v2, axis=1) 

347 cos_psi01 = np.einsum('ij,ij->i', v0, v1) # == np.sum(v0 * v1, axis=1) 

348 sin_psi01 = np.sin(np.arccos(cos_psi01)) 

349 cos_psi12 = np.einsum('ij,ij->i', v1, v2) 

350 sin_psi12 = np.sin(np.arccos(cos_psi12)) 

351 if (sin_psi01 == 0.).any() or (sin_psi12 == 0.).any(): 

352 msg = ('Undefined derivative for undefined dihedral with planar inner ' 

353 'angle') 

354 raise ZeroDivisionError(msg) 

355 

356 deriv_d0 = -normal_v01 / (nv0 * sin_psi01**2)[:, np.newaxis] # by atom 0 

357 deriv_d3 = normal_v12 / (nv2 * sin_psi12**2)[:, np.newaxis] # by atom 3 

358 deriv_d1 = (((nv1 + nv0 * cos_psi01) / nv1)[:, np.newaxis] * -deriv_d0 

359 + (cos_psi12 * nv2 / nv1)[:, np.newaxis] * deriv_d3) # by a1 

360 deriv_d2 = (-((nv1 + nv2 * cos_psi12) / nv1)[:, np.newaxis] * deriv_d3 

361 - (cos_psi01 * nv0 / nv1)[:, np.newaxis] * -deriv_d0) # by a2 

362 derivs = np.stack((deriv_d0, deriv_d1, deriv_d2, deriv_d3), axis=1) 

363 return np.degrees(derivs) 

364 

365 

366def get_distances(p1, p2=None, cell=None, pbc=None): 

367 """Return distance matrix of every position in p1 with every position in p2 

368 

369 If p2 is not set, it is assumed that distances between all positions in p1 

370 are desired. p2 will be set to p1 in this case. 

371 

372 Use set cell and pbc to use the minimum image convention. 

373 """ 

374 p1 = np.atleast_2d(p1) 

375 if p2 is None: 

376 np1 = len(p1) 

377 ind1, ind2 = np.triu_indices(np1, k=1) 

378 D = p1[ind2] - p1[ind1] 

379 else: 

380 p2 = np.atleast_2d(p2) 

381 D = (p2[np.newaxis, :, :] - p1[:, np.newaxis, :]).reshape((-1, 3)) 

382 

383 (D, ), (D_len, ) = conditional_find_mic([D], cell=cell, pbc=pbc) 

384 

385 if p2 is None: 

386 Dout = np.zeros((np1, np1, 3)) 

387 Dout[(ind1, ind2)] = D 

388 Dout -= np.transpose(Dout, axes=(1, 0, 2)) 

389 

390 Dout_len = np.zeros((np1, np1)) 

391 Dout_len[(ind1, ind2)] = D_len 

392 Dout_len += Dout_len.T 

393 return Dout, Dout_len 

394 

395 # Expand back to matrix indexing 

396 D.shape = (-1, len(p2), 3) 

397 D_len.shape = (-1, len(p2)) 

398 

399 return D, D_len 

400 

401 

402def get_distances_derivatives(v0, cell=None, pbc=None): 

403 """Get derivatives of distances for all vectors in v0 w.r.t. Cartesian 

404 coordinates in Angstrom. 

405 

406 Set cell and pbc to use the minimum image convention. 

407 

408 There is a singularity for distances -> 0 for which a ZeroDivisionError is 

409 raised. 

410 Derivative output format: [[dx_a0, dy_a0, dz_a0], [dx_a1, dy_a1, dz_a1]]. 

411 """ 

412 (v0, ), (dists, ) = conditional_find_mic([v0], cell, pbc) 

413 

414 if (dists <= 0.).any(): # identify singularities 

415 raise ZeroDivisionError('Singularity for derivative of a zero distance') 

416 

417 derivs_d0 = np.einsum('i,ij->ij', -1. / dists, v0) # derivatives by atom 0 

418 derivs_d1 = -derivs_d0 # derivatives by atom 1 

419 derivs = np.stack((derivs_d0, derivs_d1), axis=1) 

420 return derivs 

421 

422 

423def get_duplicate_atoms(atoms, cutoff=0.1, delete=False): 

424 """Get list of duplicate atoms and delete them if requested. 

425 

426 Identify all atoms which lie within the cutoff radius of each other. 

427 Delete one set of them if delete == True. 

428 """ 

429 from scipy.spatial.distance import pdist 

430 dists = pdist(atoms.get_positions(), 'sqeuclidean') 

431 dup = np.nonzero(dists < cutoff**2) 

432 rem = np.array(_row_col_from_pdist(len(atoms), dup[0])) 

433 if delete: 

434 if rem.size != 0: 

435 del atoms[rem[:, 0]] 

436 else: 

437 return rem 

438 

439 

440def _row_col_from_pdist(dim, i): 

441 """Calculate the i,j index in the square matrix for an index in a 

442 condensed (triangular) matrix. 

443 """ 

444 i = np.array(i) 

445 b = 1 - 2 * dim 

446 x = (np.floor((-b - np.sqrt(b**2 - 8 * i)) / 2)).astype(int) 

447 y = (i + x * (b + x + 2) / 2 + 1).astype(int) 

448 if i.shape: 

449 return list(zip(x, y)) 

450 else: 

451 return [(x, y)] 

452 

453 

454def permute_axes(atoms, permutation): 

455 """Permute axes of unit cell and atom positions. Considers only cell and 

456 atomic positions. Other vector quantities such as momenta are not 

457 modified.""" 

458 assert (np.sort(permutation) == np.arange(3)).all() 

459 

460 permuted = atoms.copy() 

461 scaled = permuted.get_scaled_positions() 

462 permuted.set_cell(permuted.cell.permute_axes(permutation), 

463 scale_atoms=False) 

464 permuted.set_scaled_positions(scaled[:, permutation]) 

465 permuted.set_pbc(permuted.pbc[permutation]) 

466 return permuted