Coverage for ase / build / tools.py: 72.99%

211 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-03-30 08:22 +0000

1# fmt: off 

2 

3import numpy as np 

4 

5from ase.build.niggli import niggli_reduce_cell 

6 

7 

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

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

10 maxatoms=None): 

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

12 sufficiently repeated copy of *atoms*. 

13 

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

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

16 coordinates and defines the returned cell and should normally be 

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

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

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

20 directions (and will be treated correctly). 

21 

22 Parameters 

23 ---------- 

24 

25 atoms: Atoms instance 

26 This should correspond to a repeatable unit cell. 

27 a: int | 3 floats 

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

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

30 atom with index *a*. 

31 b: int | 3 floats 

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

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

34 atom with index *b*. 

35 c: None | int | 3 floats 

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

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

38 the atom with index *c*. 

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

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

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

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

43 c = cross(a, b). 

44 clength: None | float 

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

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

47 *nlayers*. 

48 origo: int | 3 floats 

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

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

51 nlayers: None | int 

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

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

54 extend: 1 or 3 floats 

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

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

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

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

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

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

61 visualisation. 

62 tolerance: float 

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

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

65 belong to that plane. 

66 maxatoms: None | int 

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

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

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

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

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

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

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

74 *tolerance* will automatically be gradually reduced until 

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

76 exceeds *maxatoms*. 

77 

78 Examples 

79 -------- 

80 Create an aluminium (111) slab with three layers. 

81 

82 >>> import ase 

83 >>> from ase.spacegroup import crystal 

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

85 

86 # First, a unit cell of Al 

87 >>> a = 4.05 

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

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

90 

91 # Then cut out the slab 

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

93 

94 Example: Visualisation of the skutterudite unit cell 

95 

96 >>> from ase.spacegroup import crystal 

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

98 

99 # Again, create a skutterudite unit cell 

100 >>> a = 9.04 

101 >>> skutterudite = crystal( 

102 ... ('Co', 'Sb'), 

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

104 ... spacegroup=204, 

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

106 

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

108 # include all corner and edge atoms. 

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

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

111 """ 

112 atoms = atoms.copy() 

113 cell = atoms.cell 

114 

115 if isinstance(origo, int): 

116 origo = atoms.get_scaled_positions()[origo] 

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

118 

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

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

121 atoms.set_scaled_positions(scaled) 

122 

123 if isinstance(a, int): 

124 a = scaled[a] - origo 

125 if isinstance(b, int): 

126 b = scaled[b] - origo 

127 if isinstance(c, int): 

128 c = scaled[c] - origo 

129 

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

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

132 if c is None: 

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

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

135 h = np.cross(a, b) 

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

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

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

139 

140 if nlayers: 

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

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

143 while True: 

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

145 tolerance=tolerance) 

146 scaled = at.get_scaled_positions() 

147 d = scaled[:, 2] 

148 keys = np.argsort(d) 

149 ikeys = np.argsort(keys) 

150 tol = tolerance 

151 while True: 

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

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

154 levels = d[keys][mask] 

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

156 len(levels) > nlayers): 

157 break 

158 tol *= 0.9 

159 if len(levels) > nlayers: 

160 break 

161 c *= 2 

162 

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

164 return at[tags < nlayers] 

165 

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

167 if nlayers is None and clength is not None: 

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

169 

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

171 # it completely covers the new cell 

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

173 [0., 1., 0.], [0., 1., 1.], 

174 [1., 0., 0.], [1., 0., 1.], 

175 [1., 1., 0.], [1., 1., 1.]]) 

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

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

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

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

180 atoms = atoms.repeat(rep) 

181 atoms.translate(trans) 

182 atoms.set_cell(newcell) 

183 

184 # Mask out atoms outside new cell 

185 stol = 0.1 * tolerance # scaled tolerance, XXX 

186 maskcell = atoms.cell * extend 

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

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

189 atoms = atoms[mask] 

190 return atoms 

191 

192 

193class IncompatibleCellError(ValueError): 

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

195 between *atoms1* and *atoms2*.""" 

196 

197 

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

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

200 output_strained=False): 

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

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

203 ensured. 

204 

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

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

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

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

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

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

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

212 *atoms2*. 

213 

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

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

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

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

218 disables this check. 

219 

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

221 direction perpendicular to the interface, will be adjusted such 

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

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

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

225 

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

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

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

229 

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

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

232 structure. 

233 

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

235 on each side. 

236 

237 >>> import ase 

238 >>> from ase.spacegroup import crystal 

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

240 >>> 

241 >>> a_ag = 4.09 

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

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

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

245 >>> 

246 >>> a_si = 5.43 

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

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

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

250 >>> 

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

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

253 >>> 

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

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

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

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

258 Optimization terminated successfully. 

259 ... 

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

261 """ 

262 atoms1 = atoms1.copy() 

263 atoms2 = atoms2.copy() 

264 

265 for atoms in [atoms1, atoms2]: 

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

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

268 

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

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

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

272 'same handedness.') 

273 

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

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

276 if cell is None: 

277 cell1 = atoms1.cell.copy() 

278 cell2 = atoms2.cell.copy() 

279 cell1[axis] /= c1 

280 cell2[axis] /= c2 

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

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

283 cell1 = cell.copy() 

284 cell2 = cell.copy() 

285 cell1[axis] *= c1 

286 cell2[axis] *= c2 

287 

288 if maxstrain: 

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

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

291 if strain1 > maxstrain or strain2 > maxstrain: 

292 raise IncompatibleCellError( 

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

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

295 

296 atoms1.set_cell(cell1, scale_atoms=True) 

297 atoms2.set_cell(cell2, scale_atoms=True) 

298 if output_strained: 

299 atoms1_strained = atoms1.copy() 

300 atoms2_strained = atoms2.copy() 

301 

302 if distance is not None: 

303 from scipy.optimize import fmin 

304 

305 def mindist(pos1, pos2): 

306 n1 = len(pos1) 

307 n2 = len(pos2) 

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

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

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

311 

312 def func(x): 

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

314 pos1 = atoms1.positions + t1 

315 pos2 = atoms2.positions + t2 

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

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

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

319 

320 atoms1.center() 

321 atoms2.center() 

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

323 x = fmin(func, x0) 

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

325 atoms1.translate(t1) 

326 atoms2.translate(t2) 

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

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

329 

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

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

332 atoms1.extend(atoms2) 

333 

334 if reorder: 

335 atoms1 = sort(atoms1) 

336 

337 if output_strained: 

338 return atoms1, atoms1_strained, atoms2_strained 

339 else: 

340 return atoms1 

341 

342 

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

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

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

346 

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

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

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

350 """ 

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

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

353 c1 = np.cross(a1, b1) 

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

355 

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

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

358 c2 = np.cross(a2, b2) 

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

360 

361 # Calculate rotated *b2* 

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

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

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

365 

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

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

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

369 return R 

370 

371 

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

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

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

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

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

377 atoms. 

378 

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

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

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

382 True. 

383 """ 

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

385 center = atoms.get_center_of_mass() 

386 

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

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

389 

390 if rotate_cell: 

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

392 

393 

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

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

396 

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

398 that is kept fixed. 

399 """ 

400 

401 orgcell_cc = atoms.get_cell() 

402 pbc_c = atoms.get_pbc() 

403 i = fixed 

404 j = modified 

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

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

407 

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

409 cell_cc = 1. * orgcell_cc 

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

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

412 

413 # sanity check 

414 def volume(cell): 

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

416 V = volume(cell_cc) 

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

418 

419 atoms.set_cell(cell_cc) 

420 

421 if fold_atoms: 

422 atoms.wrap() 

423 

424 

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

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

427 pbc_c = atoms.get_pbc() 

428 

429 for i1, c1 in enumerate(order): 

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

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

432 minimize_tilt_ij(atoms, c1, c2, fold_atoms) 

433 

434 

435def update_cell_and_positions(atoms, new_cell, op): 

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

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

438 

439 # We do this twice because -1e-20 % 1 == 1: 

440 scpos[:, atoms.pbc] %= 1.0 

441 scpos[:, atoms.pbc] %= 1.0 

442 

443 atoms.set_cell(new_cell) 

444 atoms.set_scaled_positions(scpos) 

445 

446 

447def niggli_reduce(atoms): 

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

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

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

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

452 

453 References 

454 ---------- 

455 

456 Niggli, P. "Krystallographische und strukturtheoretische Grundbegriffe. 

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

458 

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

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

461 

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

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

464 2004, A60, 1-6. 

465 """ 

466 from ase.geometry.geometry import permute_axes 

467 

468 # Make sure non-periodic cell vectors are orthogonal 

469 non_periodic_cv = atoms.cell[~atoms.pbc] 

470 periodic_cv = atoms.cell[atoms.pbc] 

471 if not np.isclose(np.dot(non_periodic_cv, periodic_cv.T), 0).all(): 

472 raise ValueError('Non-orthogonal cell along non-periodic dimensions') 

473 

474 input_atoms = atoms 

475 

476 # Permute axes, such that the non-periodic are along the last dimensions, 

477 # since niggli_reduce_cell will change the order of axes. 

478 permutation = np.argsort(~atoms.pbc) 

479 ipermutation = np.empty_like(permutation) 

480 ipermutation[permutation] = np.arange(len(permutation)) 

481 atoms = permute_axes(atoms, permutation) 

482 

483 # Perform the Niggli reduction on the cell 

484 nonpbc = ~atoms.pbc 

485 uncompleted_cell = atoms.cell.uncomplete(atoms.pbc) 

486 new_cell, op = niggli_reduce_cell(uncompleted_cell) 

487 new_cell[nonpbc] = atoms.cell[nonpbc] 

488 update_cell_and_positions(atoms, new_cell, op) 

489 

490 # Undo the prior permutation. 

491 atoms = permute_axes(atoms, ipermutation) 

492 input_atoms.cell[:] = atoms.cell 

493 input_atoms.positions[:] = atoms.positions 

494 

495 

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

497 """Reduce atoms object to canonical lattice. 

498 

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

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

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

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

503 from ase.lattice import identify_lattice 

504 niggli_reduce(atoms) 

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

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

507 

508 

509def sort(atoms, tags=None): 

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

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

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

513 

514 Example: 

515 

516 >>> from ase.build import bulk 

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

518 >>> # Two unit cells of NaCl: 

519 >>> a = 5.64 

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

521 >>> nacl.get_chemical_symbols() 

522 ['Na', 'Cl', 'Na', 'Cl'] 

523 >>> nacl_sorted = sort(nacl) 

524 >>> nacl_sorted.get_chemical_symbols() 

525 ['Cl', 'Cl', 'Na', 'Na'] 

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

527 True 

528 """ 

529 if tags is None: 

530 tags = atoms.get_chemical_symbols() 

531 else: 

532 tags = list(tags) 

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

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

535 return atoms[indices]