Coverage for /builds/kinetik161/ase/ase/build/tools.py: 70.92%

196 statements  

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

1import numpy as np 

2from ase.build.niggli import niggli_reduce_cell 

3 

4 

5def cut(atoms, a=(1, 0, 0), b=(0, 1, 0), c=None, clength=None, 

6 origo=(0, 0, 0), nlayers=None, extend=1.0, tolerance=0.01, 

7 maxatoms=None): 

8 """Cuts out a cell defined by *a*, *b*, *c* and *origo* from a 

9 sufficiently repeated copy of *atoms*. 

10 

11 Typically, this function is used to create slabs of different 

12 sizes and orientations. The vectors *a*, *b* and *c* are in scaled 

13 coordinates and defines the returned cell and should normally be 

14 integer-valued in order to end up with a periodic 

15 structure. However, for systems with sub-translations, like fcc, 

16 integer multiples of 1/2 or 1/3 might also make sense for some 

17 directions (and will be treated correctly). 

18 

19 Parameters: 

20 

21 atoms: Atoms instance 

22 This should correspond to a repeatable unit cell. 

23 a: int | 3 floats 

24 The a-vector in scaled coordinates of the cell to cut out. If 

25 integer, the a-vector will be the scaled vector from *origo* to the 

26 atom with index *a*. 

27 b: int | 3 floats 

28 The b-vector in scaled coordinates of the cell to cut out. If 

29 integer, the b-vector will be the scaled vector from *origo* to the 

30 atom with index *b*. 

31 c: None | int | 3 floats 

32 The c-vector in scaled coordinates of the cell to cut out. 

33 if integer, the c-vector will be the scaled vector from *origo* to 

34 the atom with index *c*. 

35 If *None* it will be along cross(a, b) converted to real space 

36 and normalised with the cube root of the volume. Note that this 

37 in general is not perpendicular to a and b for non-cubic 

38 systems. For cubic systems however, this is redused to 

39 c = cross(a, b). 

40 clength: None | float 

41 If not None, the length of the c-vector will be fixed to 

42 *clength* Angstroms. Should not be used together with 

43 *nlayers*. 

44 origo: int | 3 floats 

45 Position of origo of the new cell in scaled coordinates. If 

46 integer, the position of the atom with index *origo* is used. 

47 nlayers: None | int 

48 If *nlayers* is not *None*, the returned cell will have 

49 *nlayers* atomic layers in the c-direction. 

50 extend: 1 or 3 floats 

51 The *extend* argument scales the effective cell in which atoms 

52 will be included. It must either be three floats or a single 

53 float scaling all 3 directions. By setting to a value just 

54 above one, e.g. 1.05, it is possible to all the corner and 

55 edge atoms in the returned cell. This will of cause make the 

56 returned cell non-repeatable, but is very useful for 

57 visualisation. 

58 tolerance: float 

59 Determines what is defined as a plane. All atoms within 

60 *tolerance* Angstroms from a given plane will be considered to 

61 belong to that plane. 

62 maxatoms: None | int 

63 This option is used to auto-tune *tolerance* when *nlayers* is 

64 given for high zone axis systems. For high zone axis one 

65 needs to reduce *tolerance* in order to distinguise the atomic 

66 planes, resulting in the more atoms will be added and 

67 eventually MemoryError. A too small *tolerance*, on the other 

68 hand, might result in inproper splitting of atomic planes and 

69 that too few layers are returned. If *maxatoms* is not None, 

70 *tolerance* will automatically be gradually reduced until 

71 *nlayers* atomic layers is obtained, when the number of atoms 

72 exceeds *maxatoms*. 

73 

74 Example: Create an aluminium (111) slab with three layers. 

75 

76 >>> import ase 

77 >>> from ase.spacegroup import crystal 

78 >>> from ase.build.tools import cut 

79 

80 # First, a unit cell of Al 

81 >>> a = 4.05 

82 >>> aluminium = crystal('Al', [(0,0,0)], spacegroup=225, 

83 ... cellpar=[a, a, a, 90, 90, 90]) 

84 

85 # Then cut out the slab 

86 >>> al111 = cut(aluminium, (1,-1,0), (0,1,-1), nlayers=3) 

87 

88 Example: Visualisation of the skutterudite unit cell 

89 

90 >>> from ase.spacegroup import crystal 

91 >>> from ase.build.tools import cut 

92 

93 # Again, create a skutterudite unit cell 

94 >>> a = 9.04 

95 >>> skutterudite = crystal( 

96 ... ('Co', 'Sb'), 

97 ... basis=[(0.25,0.25,0.25), (0.0, 0.335, 0.158)], 

98 ... spacegroup=204, 

99 ... cellpar=[a, a, a, 90, 90, 90]) 

100 

101 # Then use *origo* to put 'Co' at the corners and *extend* to 

102 # include all corner and edge atoms. 

103 >>> s = cut(skutterudite, origo=(0.25, 0.25, 0.25), extend=1.01) 

104 >>> ase.view(s) # doctest:+SKIP 

105 """ 

106 atoms = atoms.copy() 

107 cell = atoms.cell 

108 

109 if isinstance(origo, int): 

110 origo = atoms.get_scaled_positions()[origo] 

111 origo = np.array(origo, dtype=float) 

112 

113 scaled = (atoms.get_scaled_positions() - origo) % 1.0 

114 scaled %= 1.0 # needed to ensure that all numbers are *less* than one 

115 atoms.set_scaled_positions(scaled) 

116 

117 if isinstance(a, int): 

118 a = scaled[a] - origo 

119 if isinstance(b, int): 

120 b = scaled[b] - origo 

121 if isinstance(c, int): 

122 c = scaled[c] - origo 

123 

124 a = np.array(a, dtype=float) 

125 b = np.array(b, dtype=float) 

126 if c is None: 

127 metric = np.dot(cell, cell.T) 

128 vol = np.sqrt(np.linalg.det(metric)) 

129 h = np.cross(a, b) 

130 H = np.linalg.solve(metric.T, h.T) 

131 c = vol * H / vol**(1. / 3.) 

132 c = np.array(c, dtype=float) 

133 

134 if nlayers: 

135 # Recursive increase the length of c until we have at least 

136 # *nlayers* atomic layers parallel to the a-b plane 

137 while True: 

138 at = cut(atoms, a, b, c, origo=origo, extend=extend, 

139 tolerance=tolerance) 

140 scaled = at.get_scaled_positions() 

141 d = scaled[:, 2] 

142 keys = np.argsort(d) 

143 ikeys = np.argsort(keys) 

144 tol = tolerance 

145 while True: 

146 mask = np.concatenate(([True], np.diff(d[keys]) > tol)) 

147 tags = np.cumsum(mask)[ikeys] - 1 

148 levels = d[keys][mask] 

149 if (maxatoms is None or len(at) < maxatoms or 

150 len(levels) > nlayers): 

151 break 

152 tol *= 0.9 

153 if len(levels) > nlayers: 

154 break 

155 c *= 2 

156 

157 at.cell[2] *= levels[nlayers] 

158 return at[tags < nlayers] 

159 

160 newcell = np.dot(np.array([a, b, c]), cell) 

161 if nlayers is None and clength is not None: 

162 newcell[2, :] *= clength / np.linalg.norm(newcell[2]) 

163 

164 # Create a new atoms object, repeated and translated such that 

165 # it completely covers the new cell 

166 scorners_newcell = np.array([[0., 0., 0.], [0., 0., 1.], 

167 [0., 1., 0.], [0., 1., 1.], 

168 [1., 0., 0.], [1., 0., 1.], 

169 [1., 1., 0.], [1., 1., 1.]]) 

170 corners = np.dot(scorners_newcell, newcell * extend) 

171 scorners = np.linalg.solve(cell.T, corners.T).T 

172 rep = np.ceil(scorners.ptp(axis=0)).astype('int') + 1 

173 trans = np.dot(np.floor(scorners.min(axis=0)), cell) 

174 atoms = atoms.repeat(rep) 

175 atoms.translate(trans) 

176 atoms.set_cell(newcell) 

177 

178 # Mask out atoms outside new cell 

179 stol = 0.1 * tolerance # scaled tolerance, XXX 

180 maskcell = atoms.cell * extend 

181 sp = np.linalg.solve(maskcell.T, (atoms.positions).T).T 

182 mask = np.all(np.logical_and(-stol <= sp, sp < 1 - stol), axis=1) 

183 atoms = atoms[mask] 

184 return atoms 

185 

186 

187class IncompatibleCellError(ValueError): 

188 """Exception raised if stacking fails due to incompatible cells 

189 between *atoms1* and *atoms2*.""" 

190 

191 

192def stack(atoms1, atoms2, axis=2, cell=None, fix=0.5, 

193 maxstrain=0.5, distance=None, reorder=False, 

194 output_strained=False): 

195 """Return a new Atoms instance with *atoms2* stacked on top of 

196 *atoms1* along the given axis. Periodicity in all directions is 

197 ensured. 

198 

199 The size of the final cell is determined by *cell*, except 

200 that the length alongh *axis* will be the sum of 

201 *atoms1.cell[axis]* and *atoms2.cell[axis]*. If *cell* is None, 

202 it will be interpolated between *atoms1* and *atoms2*, where 

203 *fix* determines their relative weight. Hence, if *fix* equals 

204 zero, the final cell will be determined purely from *atoms1* and 

205 if *fix* equals one, it will be determined purely from 

206 *atoms2*. 

207 

208 An ase.geometry.IncompatibleCellError exception is raised if the 

209 cells of *atoms1* and *atoms2* are incompatible, e.g. if the far 

210 corner of the unit cell of either *atoms1* or *atoms2* is 

211 displaced more than *maxstrain*. Setting *maxstrain* to None 

212 disables this check. 

213 

214 If *distance* is not None, the size of the final cell, along the 

215 direction perpendicular to the interface, will be adjusted such 

216 that the distance between the closest atoms in *atoms1* and 

217 *atoms2* will be equal to *distance*. This option uses 

218 scipy.optimize.fmin() and hence require scipy to be installed. 

219 

220 If *reorder* is True, then the atoms will be reordered such that 

221 all atoms with the same symbol will follow sequencially after each 

222 other, eg: 'Al2MnAl10Fe' -> 'Al12FeMn'. 

223 

224 If *output_strained* is True, then the strained versions of 

225 *atoms1* and *atoms2* are returned in addition to the stacked 

226 structure. 

227 

228 Example: Create an Ag(110)-Si(110) interface with three atomic layers 

229 on each side. 

230 

231 >>> import ase 

232 >>> from ase.spacegroup import crystal 

233 >>> from ase.build.tools import cut, stack 

234 >>> 

235 >>> a_ag = 4.09 

236 >>> ag = crystal(['Ag'], basis=[(0,0,0)], spacegroup=225, 

237 ... cellpar=[a_ag, a_ag, a_ag, 90., 90., 90.]) 

238 >>> ag110 = cut(ag, (0, 0, 3), (-1.5, 1.5, 0), nlayers=3) 

239 >>> 

240 >>> a_si = 5.43 

241 >>> si = crystal(['Si'], basis=[(0,0,0)], spacegroup=227, 

242 ... cellpar=[a_si, a_si, a_si, 90., 90., 90.]) 

243 >>> si110 = cut(si, (0, 0, 2), (-1, 1, 0), nlayers=3) 

244 >>> 

245 >>> interface = stack(ag110, si110, maxstrain=1) 

246 >>> ase.view(interface) # doctest: +SKIP 

247 >>> 

248 # Once more, this time adjusted such that the distance between 

249 # the closest Ag and Si atoms will be 2.3 Angstrom (requires scipy). 

250 >>> interface2 = stack(ag110, si110, 

251 ... maxstrain=1, distance=2.3) # doctest:+ELLIPSIS 

252 Optimization terminated successfully. 

253 ... 

254 >>> ase.view(interface2) # doctest: +SKIP 

255 """ 

256 atoms1 = atoms1.copy() 

257 atoms2 = atoms2.copy() 

258 

259 for atoms in [atoms1, atoms2]: 

260 if not atoms.cell[axis].any(): 

261 atoms.center(vacuum=0.0, axis=axis) 

262 

263 if (np.sign(np.linalg.det(atoms1.cell)) != 

264 np.sign(np.linalg.det(atoms2.cell))): 

265 raise IncompatibleCellError('Cells of *atoms1* and *atoms2* must have ' 

266 'same handedness.') 

267 

268 c1 = np.linalg.norm(atoms1.cell[axis]) 

269 c2 = np.linalg.norm(atoms2.cell[axis]) 

270 if cell is None: 

271 cell1 = atoms1.cell.copy() 

272 cell2 = atoms2.cell.copy() 

273 cell1[axis] /= c1 

274 cell2[axis] /= c2 

275 cell = cell1 + fix * (cell2 - cell1) 

276 cell[axis] /= np.linalg.norm(cell[axis]) 

277 cell1 = cell.copy() 

278 cell2 = cell.copy() 

279 cell1[axis] *= c1 

280 cell2[axis] *= c2 

281 

282 if maxstrain: 

283 strain1 = np.sqrt(((cell1 - atoms1.cell).sum(axis=0)**2).sum()) 

284 strain2 = np.sqrt(((cell2 - atoms2.cell).sum(axis=0)**2).sum()) 

285 if strain1 > maxstrain or strain2 > maxstrain: 

286 raise IncompatibleCellError( 

287 '*maxstrain* exceeded. *atoms1* strained %f and ' 

288 '*atoms2* strained %f.' % (strain1, strain2)) 

289 

290 atoms1.set_cell(cell1, scale_atoms=True) 

291 atoms2.set_cell(cell2, scale_atoms=True) 

292 if output_strained: 

293 atoms1_strained = atoms1.copy() 

294 atoms2_strained = atoms2.copy() 

295 

296 if distance is not None: 

297 from scipy.optimize import fmin 

298 

299 def mindist(pos1, pos2): 

300 n1 = len(pos1) 

301 n2 = len(pos2) 

302 idx1 = np.arange(n1).repeat(n2) 

303 idx2 = np.tile(np.arange(n2), n1) 

304 return np.sqrt(((pos1[idx1] - pos2[idx2])**2).sum(axis=1).min()) 

305 

306 def func(x): 

307 t1, t2, h1, h2 = x[0:3], x[3:6], x[6], x[7] 

308 pos1 = atoms1.positions + t1 

309 pos2 = atoms2.positions + t2 

310 d1 = mindist(pos1, pos2 + (h1 + 1.0) * atoms1.cell[axis]) 

311 d2 = mindist(pos2, pos1 + (h2 + 1.0) * atoms2.cell[axis]) 

312 return (d1 - distance)**2 + (d2 - distance)**2 

313 

314 atoms1.center() 

315 atoms2.center() 

316 x0 = np.zeros((8,)) 

317 x = fmin(func, x0) 

318 t1, t2, h1, h2 = x[0:3], x[3:6], x[6], x[7] 

319 atoms1.translate(t1) 

320 atoms2.translate(t2) 

321 atoms1.cell[axis] *= 1.0 + h1 

322 atoms2.cell[axis] *= 1.0 + h2 

323 

324 atoms2.translate(atoms1.cell[axis]) 

325 atoms1.cell[axis] += atoms2.cell[axis] 

326 atoms1.extend(atoms2) 

327 

328 if reorder: 

329 atoms1 = sort(atoms1) 

330 

331 if output_strained: 

332 return atoms1, atoms1_strained, atoms2_strained 

333 else: 

334 return atoms1 

335 

336 

337def rotation_matrix(a1, a2, b1, b2): 

338 """Returns a rotation matrix that rotates the vectors *a1* in the 

339 direction of *a2* and *b1* in the direction of *b2*. 

340 

341 In the case that the angle between *a2* and *b2* is not the same 

342 as between *a1* and *b1*, a proper rotation matrix will anyway be 

343 constructed by first rotate *b2* in the *b1*, *b2* plane. 

344 """ 

345 a1 = np.asarray(a1, dtype=float) / np.linalg.norm(a1) 

346 b1 = np.asarray(b1, dtype=float) / np.linalg.norm(b1) 

347 c1 = np.cross(a1, b1) 

348 c1 /= np.linalg.norm(c1) # clean out rounding errors... 

349 

350 a2 = np.asarray(a2, dtype=float) / np.linalg.norm(a2) 

351 b2 = np.asarray(b2, dtype=float) / np.linalg.norm(b2) 

352 c2 = np.cross(a2, b2) 

353 c2 /= np.linalg.norm(c2) # clean out rounding errors... 

354 

355 # Calculate rotated *b2* 

356 theta = np.arccos(np.dot(a2, b2)) - np.arccos(np.dot(a1, b1)) 

357 b3 = np.sin(theta) * a2 + np.cos(theta) * b2 

358 b3 /= np.linalg.norm(b3) # clean out rounding errors... 

359 

360 A1 = np.array([a1, b1, c1]) 

361 A2 = np.array([a2, b3, c2]) 

362 R = np.linalg.solve(A1, A2).T 

363 return R 

364 

365 

366def rotate(atoms, a1, a2, b1, b2, rotate_cell=True, center=(0, 0, 0)): 

367 """Rotate *atoms*, such that *a1* will be rotated in the direction 

368 of *a2* and *b1* in the direction of *b2*. The point at *center* 

369 is fixed. Use *center='COM'* to fix the center of mass. If 

370 *rotate_cell* is true, the cell will be rotated together with the 

371 atoms. 

372 

373 Note that the 000-corner of the cell is by definition fixed at 

374 origo. Hence, setting *center* to something other than (0, 0, 0) 

375 will rotate the atoms out of the cell, even if *rotate_cell* is 

376 True. 

377 """ 

378 if isinstance(center, str) and center.lower() == 'com': 

379 center = atoms.get_center_of_mass() 

380 

381 R = rotation_matrix(a1, a2, b1, b2) 

382 atoms.positions[:] = np.dot(atoms.positions - center, R.T) + center 

383 

384 if rotate_cell: 

385 atoms.cell[:] = np.dot(atoms.cell, R.T) 

386 

387 

388def minimize_tilt_ij(atoms, modified=1, fixed=0, fold_atoms=True): 

389 """Minimize the tilt angle for two given axes. 

390 

391 The problem is underdetermined. Therefore one can choose one axis 

392 that is kept fixed. 

393 """ 

394 

395 orgcell_cc = atoms.get_cell() 

396 pbc_c = atoms.get_pbc() 

397 i = fixed 

398 j = modified 

399 if not (pbc_c[i] and pbc_c[j]): 

400 raise RuntimeError('Axes have to be periodic') 

401 

402 prod_cc = np.dot(orgcell_cc, orgcell_cc.T) 

403 cell_cc = 1. * orgcell_cc 

404 nji = np.floor(- prod_cc[i, j] / prod_cc[i, i] + 0.5) 

405 cell_cc[j] = orgcell_cc[j] + nji * cell_cc[i] 

406 

407 # sanity check 

408 def volume(cell): 

409 return np.abs(np.dot(cell[2], np.cross(cell[0], cell[1]))) 

410 V = volume(cell_cc) 

411 assert abs(volume(orgcell_cc) - V) / V < 1.e-10 

412 

413 atoms.set_cell(cell_cc) 

414 

415 if fold_atoms: 

416 atoms.wrap() 

417 

418 

419def minimize_tilt(atoms, order=range(3), fold_atoms=True): 

420 """Minimize the tilt angles of the unit cell.""" 

421 pbc_c = atoms.get_pbc() 

422 

423 for i1, c1 in enumerate(order): 

424 for c2 in order[i1 + 1:]: 

425 if pbc_c[c1] and pbc_c[c2]: 

426 minimize_tilt_ij(atoms, c1, c2, fold_atoms) 

427 

428 

429def update_cell_and_positions(atoms, new_cell, op): 

430 """Helper method for transforming cell and positions of atoms object.""" 

431 scpos = np.linalg.solve(op, atoms.get_scaled_positions().T).T 

432 scpos %= 1.0 

433 scpos %= 1.0 

434 

435 atoms.set_cell(new_cell) 

436 atoms.set_scaled_positions(scpos) 

437 

438 

439def niggli_reduce(atoms): 

440 """Convert the supplied atoms object's unit cell into its 

441 maximally-reduced Niggli unit cell. Even if the unit cell is already 

442 maximally reduced, it will be converted into its unique Niggli unit cell. 

443 This will also wrap all atoms into the new unit cell. 

444 

445 References: 

446 

447 Niggli, P. "Krystallographische und strukturtheoretische Grundbegriffe. 

448 Handbuch der Experimentalphysik", 1928, Vol. 7, Part 1, 108-176. 

449 

450 Krivy, I. and Gruber, B., "A Unified Algorithm for Determining the 

451 Reduced (Niggli) Cell", Acta Cryst. 1976, A32, 297-298. 

452 

453 Grosse-Kunstleve, R.W.; Sauter, N. K.; and Adams, P. D. "Numerically 

454 stable algorithms for the computation of reduced unit cells", Acta Cryst. 

455 2004, A60, 1-6. 

456 """ 

457 

458 assert all(atoms.pbc), 'Can only reduce 3d periodic unit cells!' 

459 new_cell, op = niggli_reduce_cell(atoms.cell) 

460 update_cell_and_positions(atoms, new_cell, op) 

461 

462 

463def reduce_lattice(atoms, eps=2e-4): 

464 """Reduce atoms object to canonical lattice. 

465 

466 This changes the cell and positions such that the atoms object has 

467 the canonical form used for defining band paths but is otherwise 

468 physically equivalent. The eps parameter is used as a tolerance 

469 for determining the cell's Bravais lattice.""" 

470 from ase.lattice import identify_lattice 

471 niggli_reduce(atoms) 

472 lat, op = identify_lattice(atoms.cell, eps=eps) 

473 update_cell_and_positions(atoms, lat.tocell(), np.linalg.inv(op)) 

474 

475 

476def sort(atoms, tags=None): 

477 """Return a new Atoms object with sorted atomic order. The default 

478 is to order according to chemical symbols, but if *tags* is not 

479 None, it will be used instead. A stable sorting algorithm is used. 

480 

481 Example: 

482 

483 >>> from ase.build import bulk 

484 >>> from ase.build.tools import sort 

485 >>> # Two unit cells of NaCl: 

486 >>> a = 5.64 

487 >>> nacl = bulk('NaCl', 'rocksalt', a=a) * (2, 1, 1) 

488 >>> nacl.get_chemical_symbols() 

489 ['Na', 'Cl', 'Na', 'Cl'] 

490 >>> nacl_sorted = sort(nacl) 

491 >>> nacl_sorted.get_chemical_symbols() 

492 ['Cl', 'Cl', 'Na', 'Na'] 

493 >>> np.all(nacl_sorted.cell == nacl.cell) 

494 True 

495 """ 

496 if tags is None: 

497 tags = atoms.get_chemical_symbols() 

498 else: 

499 tags = list(tags) 

500 deco = sorted([(tag, i) for i, tag in enumerate(tags)]) 

501 indices = [i for tag, i in deco] 

502 return atoms[indices]