Coverage for /builds/debichem-team/python-ase/ase/build/tools.py: 72.99%

211 statements  

« prev     ^ index     » next       coverage.py v7.5.3, created at 2025-03-06 04:00 +0000

1import numpy as np 

2 

3from ase.build.niggli import niggli_reduce_cell 

4 

5 

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

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

8 maxatoms=None): 

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

10 sufficiently repeated copy of *atoms*. 

11 

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

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

14 coordinates and defines the returned cell and should normally be 

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

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

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

18 directions (and will be treated correctly). 

19 

20 Parameters: 

21 

22 atoms: Atoms instance 

23 This should correspond to a repeatable unit cell. 

24 a: int | 3 floats 

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

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

27 atom with index *a*. 

28 b: int | 3 floats 

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

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

31 atom with index *b*. 

32 c: None | int | 3 floats 

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

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

35 the atom with index *c*. 

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

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

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

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

40 c = cross(a, b). 

41 clength: None | float 

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

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

44 *nlayers*. 

45 origo: int | 3 floats 

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

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

48 nlayers: None | int 

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

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

51 extend: 1 or 3 floats 

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

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

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

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

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

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

58 visualisation. 

59 tolerance: float 

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

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

62 belong to that plane. 

63 maxatoms: None | int 

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

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

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

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

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

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

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

71 *tolerance* will automatically be gradually reduced until 

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

73 exceeds *maxatoms*. 

74 

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

76 

77 >>> import ase 

78 >>> from ase.spacegroup import crystal 

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

80 

81 # First, a unit cell of Al 

82 >>> a = 4.05 

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

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

85 

86 # Then cut out the slab 

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

88 

89 Example: Visualisation of the skutterudite unit cell 

90 

91 >>> from ase.spacegroup import crystal 

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

93 

94 # Again, create a skutterudite unit cell 

95 >>> a = 9.04 

96 >>> skutterudite = crystal( 

97 ... ('Co', 'Sb'), 

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

99 ... spacegroup=204, 

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

101 

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

103 # include all corner and edge atoms. 

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

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

106 """ 

107 atoms = atoms.copy() 

108 cell = atoms.cell 

109 

110 if isinstance(origo, int): 

111 origo = atoms.get_scaled_positions()[origo] 

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

113 

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

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

116 atoms.set_scaled_positions(scaled) 

117 

118 if isinstance(a, int): 

119 a = scaled[a] - origo 

120 if isinstance(b, int): 

121 b = scaled[b] - origo 

122 if isinstance(c, int): 

123 c = scaled[c] - origo 

124 

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

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

127 if c is None: 

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

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

130 h = np.cross(a, b) 

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

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

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

134 

135 if nlayers: 

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

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

138 while True: 

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

140 tolerance=tolerance) 

141 scaled = at.get_scaled_positions() 

142 d = scaled[:, 2] 

143 keys = np.argsort(d) 

144 ikeys = np.argsort(keys) 

145 tol = tolerance 

146 while True: 

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

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

149 levels = d[keys][mask] 

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

151 len(levels) > nlayers): 

152 break 

153 tol *= 0.9 

154 if len(levels) > nlayers: 

155 break 

156 c *= 2 

157 

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

159 return at[tags < nlayers] 

160 

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

162 if nlayers is None and clength is not None: 

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

164 

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

166 # it completely covers the new cell 

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

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

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

170 [1., 1., 0.], [1., 1., 1.]]) 

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

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

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

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

175 atoms = atoms.repeat(rep) 

176 atoms.translate(trans) 

177 atoms.set_cell(newcell) 

178 

179 # Mask out atoms outside new cell 

180 stol = 0.1 * tolerance # scaled tolerance, XXX 

181 maskcell = atoms.cell * extend 

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

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

184 atoms = atoms[mask] 

185 return atoms 

186 

187 

188class IncompatibleCellError(ValueError): 

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

190 between *atoms1* and *atoms2*.""" 

191 

192 

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

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

195 output_strained=False): 

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

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

198 ensured. 

199 

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

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

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

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

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

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

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

207 *atoms2*. 

208 

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

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

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

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

213 disables this check. 

214 

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

216 direction perpendicular to the interface, will be adjusted such 

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

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

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

220 

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

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

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

224 

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

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

227 structure. 

228 

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

230 on each side. 

231 

232 >>> import ase 

233 >>> from ase.spacegroup import crystal 

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

235 >>> 

236 >>> a_ag = 4.09 

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

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

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

240 >>> 

241 >>> a_si = 5.43 

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

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

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

245 >>> 

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

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

248 >>> 

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

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

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

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

253 Optimization terminated successfully. 

254 ... 

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

256 """ 

257 atoms1 = atoms1.copy() 

258 atoms2 = atoms2.copy() 

259 

260 for atoms in [atoms1, atoms2]: 

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

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

263 

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

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

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

267 'same handedness.') 

268 

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

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

271 if cell is None: 

272 cell1 = atoms1.cell.copy() 

273 cell2 = atoms2.cell.copy() 

274 cell1[axis] /= c1 

275 cell2[axis] /= c2 

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

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

278 cell1 = cell.copy() 

279 cell2 = cell.copy() 

280 cell1[axis] *= c1 

281 cell2[axis] *= c2 

282 

283 if maxstrain: 

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

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

286 if strain1 > maxstrain or strain2 > maxstrain: 

287 raise IncompatibleCellError( 

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

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

290 

291 atoms1.set_cell(cell1, scale_atoms=True) 

292 atoms2.set_cell(cell2, scale_atoms=True) 

293 if output_strained: 

294 atoms1_strained = atoms1.copy() 

295 atoms2_strained = atoms2.copy() 

296 

297 if distance is not None: 

298 from scipy.optimize import fmin 

299 

300 def mindist(pos1, pos2): 

301 n1 = len(pos1) 

302 n2 = len(pos2) 

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

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

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

306 

307 def func(x): 

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

309 pos1 = atoms1.positions + t1 

310 pos2 = atoms2.positions + t2 

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

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

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

314 

315 atoms1.center() 

316 atoms2.center() 

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

318 x = fmin(func, x0) 

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

320 atoms1.translate(t1) 

321 atoms2.translate(t2) 

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

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

324 

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

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

327 atoms1.extend(atoms2) 

328 

329 if reorder: 

330 atoms1 = sort(atoms1) 

331 

332 if output_strained: 

333 return atoms1, atoms1_strained, atoms2_strained 

334 else: 

335 return atoms1 

336 

337 

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

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

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

341 

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

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

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

345 """ 

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

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

348 c1 = np.cross(a1, b1) 

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

350 

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

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

353 c2 = np.cross(a2, b2) 

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

355 

356 # Calculate rotated *b2* 

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

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

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

360 

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

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

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

364 return R 

365 

366 

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

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

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

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

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

372 atoms. 

373 

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

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

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

377 True. 

378 """ 

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

380 center = atoms.get_center_of_mass() 

381 

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

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

384 

385 if rotate_cell: 

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

387 

388 

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

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

391 

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

393 that is kept fixed. 

394 """ 

395 

396 orgcell_cc = atoms.get_cell() 

397 pbc_c = atoms.get_pbc() 

398 i = fixed 

399 j = modified 

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

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

402 

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

404 cell_cc = 1. * orgcell_cc 

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

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

407 

408 # sanity check 

409 def volume(cell): 

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

411 V = volume(cell_cc) 

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

413 

414 atoms.set_cell(cell_cc) 

415 

416 if fold_atoms: 

417 atoms.wrap() 

418 

419 

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

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

422 pbc_c = atoms.get_pbc() 

423 

424 for i1, c1 in enumerate(order): 

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

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

427 minimize_tilt_ij(atoms, c1, c2, fold_atoms) 

428 

429 

430def update_cell_and_positions(atoms, new_cell, op): 

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

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

433 

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

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

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

437 

438 atoms.set_cell(new_cell) 

439 atoms.set_scaled_positions(scpos) 

440 

441 

442def niggli_reduce(atoms): 

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

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

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

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

447 

448 References: 

449 

450 Niggli, P. "Krystallographische und strukturtheoretische Grundbegriffe. 

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

452 

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

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

455 

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

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

458 2004, A60, 1-6. 

459 """ 

460 from ase.geometry.geometry import permute_axes 

461 

462 # Make sure non-periodic cell vectors are orthogonal 

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

464 periodic_cv = atoms.cell[atoms.pbc] 

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

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

467 

468 input_atoms = atoms 

469 

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

471 # since niggli_reduce_cell will change the order of axes. 

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

473 ipermutation = np.empty_like(permutation) 

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

475 atoms = permute_axes(atoms, permutation) 

476 

477 # Perform the Niggli reduction on the cell 

478 nonpbc = ~atoms.pbc 

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

480 new_cell, op = niggli_reduce_cell(uncompleted_cell) 

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

482 update_cell_and_positions(atoms, new_cell, op) 

483 

484 # Undo the prior permutation. 

485 atoms = permute_axes(atoms, ipermutation) 

486 input_atoms.cell[:] = atoms.cell 

487 input_atoms.positions[:] = atoms.positions 

488 

489 

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

491 """Reduce atoms object to canonical lattice. 

492 

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

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

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

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

497 from ase.lattice import identify_lattice 

498 niggli_reduce(atoms) 

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

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

501 

502 

503def sort(atoms, tags=None): 

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

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

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

507 

508 Example: 

509 

510 >>> from ase.build import bulk 

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

512 >>> # Two unit cells of NaCl: 

513 >>> a = 5.64 

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

515 >>> nacl.get_chemical_symbols() 

516 ['Na', 'Cl', 'Na', 'Cl'] 

517 >>> nacl_sorted = sort(nacl) 

518 >>> nacl_sorted.get_chemical_symbols() 

519 ['Cl', 'Cl', 'Na', 'Na'] 

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

521 True 

522 """ 

523 if tags is None: 

524 tags = atoms.get_chemical_symbols() 

525 else: 

526 tags = list(tags) 

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

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

529 return atoms[indices]