Coverage for /builds/kinetik161/ase/ase/cell.py: 99.30%

143 statements  

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

1from typing import Mapping, Sequence, Union 

2 

3import numpy as np 

4 

5import ase 

6from ase.utils import pbc2pbc 

7from ase.utils.arraywrapper import arraylike 

8 

9__all__ = ['Cell'] 

10 

11 

12@arraylike 

13class Cell: 

14 """Parallel epipedal unit cell of up to three dimensions. 

15 

16 This object resembles a 3x3 array whose [i, j]-th element is the jth 

17 Cartesian coordinate of the ith unit vector. 

18 

19 Cells of less than three dimensions are represented by placeholder 

20 unit vectors that are zero.""" 

21 

22 ase_objtype = 'cell' # For JSON'ing 

23 

24 def __init__(self, array): 

25 """Create cell. 

26 

27 Parameters: 

28 

29 array: 3x3 arraylike object 

30 The three cell vectors: cell[0], cell[1], and cell[2]. 

31 """ 

32 array = np.asarray(array, dtype=float) 

33 assert array.shape == (3, 3) 

34 self.array = array 

35 

36 def cellpar(self, radians=False): 

37 """Get unit cell parameters. Sequence of 6 numbers. 

38 

39 First three are unit cell vector lengths and second three 

40 are angles between them:: 

41 

42 [len(a), len(b), len(c), angle(b,c), angle(a,c), angle(a,b)] 

43 

44 in degrees. 

45 

46 See also :func:`ase.geometry.cell.cell_to_cellpar`.""" 

47 from ase.geometry.cell import cell_to_cellpar 

48 return cell_to_cellpar(self.array, radians) 

49 

50 def todict(self): 

51 return dict(array=self.array) 

52 

53 @classmethod 

54 def ascell(cls, cell): 

55 """Return argument as a Cell object. See :meth:`ase.cell.Cell.new`. 

56 

57 A new Cell object is created if necessary.""" 

58 if isinstance(cell, cls): 

59 return cell 

60 return cls.new(cell) 

61 

62 @classmethod 

63 def new(cls, cell=None): 

64 """Create new cell from any parameters. 

65 

66 If cell is three numbers, assume three lengths with right angles. 

67 

68 If cell is six numbers, assume three lengths, then three angles. 

69 

70 If cell is 3x3, assume three cell vectors.""" 

71 

72 if cell is None: 

73 cell = np.zeros((3, 3)) 

74 

75 cell = np.array(cell, float) 

76 

77 if cell.shape == (3,): 

78 cell = np.diag(cell) 

79 elif cell.shape == (6,): 

80 from ase.geometry.cell import cellpar_to_cell 

81 cell = cellpar_to_cell(cell) 

82 elif cell.shape != (3, 3): 

83 raise ValueError('Cell must be length 3 sequence, length 6 ' 

84 'sequence or 3x3 matrix!') 

85 

86 cellobj = cls(cell) 

87 return cellobj 

88 

89 @classmethod 

90 def fromcellpar(cls, cellpar, ab_normal=(0, 0, 1), a_direction=None): 

91 """Return new Cell from cell lengths and angles. 

92 

93 See also :func:`~ase.geometry.cell.cellpar_to_cell()`.""" 

94 from ase.geometry.cell import cellpar_to_cell 

95 cell = cellpar_to_cell(cellpar, ab_normal, a_direction) 

96 return cls(cell) 

97 

98 def get_bravais_lattice(self, eps=2e-4, *, pbc=True): 

99 """Return :class:`~ase.lattice.BravaisLattice` for this cell: 

100 

101 >>> from ase.cell import Cell 

102 

103 >>> cell = Cell.fromcellpar([4, 4, 4, 60, 60, 60]) 

104 >>> print(cell.get_bravais_lattice()) 

105 FCC(a=5.65685) 

106 

107 .. note:: The Bravais lattice object follows the AFlow 

108 conventions. ``cell.get_bravais_lattice().tocell()`` may 

109 differ from the original cell by a permutation or other 

110 operation which maps it to the AFlow convention. For 

111 example, the orthorhombic lattice enforces a < b < c. 

112 

113 To build a bandpath for a particular cell, use 

114 :meth:`ase.cell.Cell.bandpath` instead of this method. 

115 This maps the kpoints back to the original input cell. 

116 

117 """ 

118 from ase.lattice import identify_lattice 

119 pbc = self.mask() & pbc2pbc(pbc) 

120 lat, op = identify_lattice(self, eps=eps, pbc=pbc) 

121 return lat 

122 

123 def bandpath( 

124 self, 

125 path: str = None, 

126 npoints: int = None, 

127 *, 

128 density: float = None, 

129 special_points: Mapping[str, Sequence[float]] = None, 

130 eps: float = 2e-4, 

131 pbc: Union[bool, Sequence[bool]] = True 

132 ) -> "ase.dft.kpoints.BandPath": 

133 """Build a :class:`~ase.dft.kpoints.BandPath` for this cell. 

134 

135 If special points are None, determine the Bravais lattice of 

136 this cell and return a suitable Brillouin zone path with 

137 standard special points. 

138 

139 If special special points are given, interpolate the path 

140 directly from the available data. 

141 

142 Parameters: 

143 

144 path: string 

145 String of special point names defining the path, e.g. 'GXL'. 

146 npoints: int 

147 Number of points in total. Note that at least one point 

148 is added for each special point in the path. 

149 density: float 

150 density of kpoints along the path in Å⁻¹. 

151 special_points: dict 

152 Dictionary mapping special points to scaled kpoint coordinates. 

153 For example ``{'G': [0, 0, 0], 'X': [1, 0, 0]}``. 

154 eps: float 

155 Tolerance for determining Bravais lattice. 

156 pbc: three bools 

157 Whether cell is periodic in each direction. Normally not 

158 necessary. If cell has three nonzero cell vectors, use 

159 e.g. pbc=[1, 1, 0] to request a 2D bandpath nevertheless. 

160 

161 Example 

162 ------- 

163 

164 >>> from ase.cell import Cell 

165 

166 >>> cell = Cell.fromcellpar([4, 4, 4, 60, 60, 60]) 

167 >>> cell.bandpath('GXW', npoints=20) 

168 BandPath(path='GXW', cell=[3x3], special_points={GKLUWX}, kpts=[20x3]) 

169 

170 """ 

171 # TODO: Combine with the rotation transformation from bandpath() 

172 

173 cell = self.uncomplete(pbc) 

174 

175 if special_points is None: 

176 from ase.lattice import identify_lattice 

177 lat, op = identify_lattice(cell, eps=eps) 

178 bandpath = lat.bandpath(path, npoints=npoints, density=density) 

179 return bandpath.transform(op) 

180 else: 

181 from ase.dft.kpoints import BandPath, resolve_custom_points 

182 path, special_points = resolve_custom_points( 

183 path, special_points, eps=eps) 

184 bandpath = BandPath(cell, path=path, special_points=special_points) 

185 return bandpath.interpolate(npoints=npoints, density=density) 

186 

187 def uncomplete(self, pbc): 

188 """Return new cell, zeroing cell vectors where not periodic.""" 

189 _pbc = np.empty(3, bool) 

190 _pbc[:] = pbc 

191 cell = self.copy() 

192 cell[~_pbc] = 0 

193 return cell 

194 

195 def complete(self): 

196 """Convert missing cell vectors into orthogonal unit vectors.""" 

197 from ase.geometry.cell import complete_cell 

198 return Cell(complete_cell(self.array)) 

199 

200 def copy(self): 

201 """Return a copy of this cell.""" 

202 return Cell(self.array.copy()) 

203 

204 def mask(self): 

205 """Boolean mask of which cell vectors are nonzero.""" 

206 return self.any(1) 

207 

208 @property 

209 def rank(self) -> int: 

210 """"Return the dimension of the cell. 

211 

212 Equal to the number of nonzero lattice vectors.""" 

213 # The name ndim clashes with ndarray.ndim 

214 return sum(self.mask()) 

215 

216 @property 

217 def orthorhombic(self) -> bool: 

218 """Return whether this cell is represented by a diagonal matrix.""" 

219 from ase.geometry.cell import is_orthorhombic 

220 return is_orthorhombic(self) 

221 

222 def lengths(self): 

223 """Return the length of each lattice vector as an array.""" 

224 return np.linalg.norm(self, axis=1) 

225 

226 def angles(self): 

227 """Return an array with the three angles alpha, beta, and gamma.""" 

228 return self.cellpar()[3:].copy() 

229 

230 def __array__(self, dtype=float): 

231 if dtype != float: 

232 raise ValueError('Cannot convert cell to array of type {}' 

233 .format(dtype)) 

234 return self.array 

235 

236 def __bool__(self): 

237 return bool(self.any()) # need to convert from np.bool_ 

238 

239 @property 

240 def volume(self) -> float: 

241 """Get the volume of this cell. 

242 

243 If there are less than 3 lattice vectors, return 0.""" 

244 # Fail or 0 for <3D cells? 

245 # Definitely 0 since this is currently a property. 

246 # I think normally it is more convenient just to get zero 

247 return np.abs(np.linalg.det(self)) 

248 

249 @property 

250 def handedness(self) -> int: 

251 """Sign of the determinant of the matrix of cell vectors. 

252 

253 1 for right-handed cells, -1 for left, and 0 for cells that 

254 do not span three dimensions.""" 

255 return int(np.sign(np.linalg.det(self))) 

256 

257 def scaled_positions(self, positions) -> np.ndarray: 

258 """Calculate scaled positions from Cartesian positions. 

259 

260 The scaled positions are the positions given in the basis 

261 of the cell vectors. For the purpose of defining the basis, cell 

262 vectors that are zero will be replaced by unit vectors as per 

263 :meth:`~ase.cell.Cell.complete`.""" 

264 return np.linalg.solve(self.complete().T, np.transpose(positions)).T 

265 

266 def cartesian_positions(self, scaled_positions) -> np.ndarray: 

267 """Calculate Cartesian positions from scaled positions.""" 

268 return scaled_positions @ self.complete() 

269 

270 def reciprocal(self) -> 'Cell': 

271 """Get reciprocal lattice as a Cell object. 

272 

273 The reciprocal cell is defined such that 

274 

275 cell.reciprocal() @ cell.T == np.diag(cell.mask()) 

276 

277 within machine precision. 

278 

279 Does not include factor of 2 pi.""" 

280 icell = Cell(np.linalg.pinv(self).transpose()) 

281 icell[~self.mask()] = 0.0 # type: ignore[index] 

282 return icell 

283 

284 def normal(self, i): 

285 """Normal vector of the two vectors with index different from i. 

286 

287 This is the cross product of those vectors in cyclic order from i.""" 

288 return np.cross(self[i - 2], self[i - 1]) 

289 

290 def normals(self): 

291 """Normal vectors of each axis as a 3x3 matrix.""" 

292 return np.array([self.normal(i) for i in range(3)]) 

293 

294 def area(self, i): 

295 """Area spanned by the two vectors with index different from i.""" 

296 return np.linalg.norm(self.normal(i)) 

297 

298 def areas(self): 

299 """Areas spanned by cell vector pairs (1, 2), (2, 0), and (0, 2).""" 

300 return np.linalg.norm(self.normals(), axis=1) 

301 

302 def __repr__(self): 

303 if self.orthorhombic: 

304 numbers = self.lengths().tolist() 

305 else: 

306 numbers = self.tolist() 

307 

308 return f'Cell({numbers})' 

309 

310 def niggli_reduce(self, eps=1e-5): 

311 """Niggli reduce this cell, returning a new cell and mapping. 

312 

313 See also :func:`ase.build.tools.niggli_reduce_cell`.""" 

314 from ase.build.tools import niggli_reduce_cell 

315 cell, op = niggli_reduce_cell(self, epsfactor=eps) 

316 result = Cell(cell) 

317 return result, op 

318 

319 def minkowski_reduce(self): 

320 """Minkowski-reduce this cell, returning new cell and mapping. 

321 

322 See also :func:`ase.geometry.minkowski_reduction.minkowski_reduce`.""" 

323 from ase.geometry.minkowski_reduction import minkowski_reduce 

324 cell, op = minkowski_reduce(self, self.mask()) 

325 result = Cell(cell) 

326 return result, op 

327 

328 def permute_axes(self, permutation): 

329 """Permute axes of cell.""" 

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

331 permuted = Cell(self[permutation][:, permutation]) 

332 return permuted 

333 

334 def standard_form(self): 

335 """Rotate axes such that unit cell is lower triangular. The cell 

336 handedness is preserved. 

337 

338 A lower-triangular cell with positive diagonal entries is a canonical 

339 (i.e. unique) description. For a left-handed cell the diagonal entries 

340 are negative. 

341 

342 Returns: 

343 

344 rcell: the standardized cell object 

345 

346 Q: ndarray 

347 The orthogonal transformation. Here, rcell @ Q = cell, where cell 

348 is the input cell and rcell is the lower triangular (output) cell. 

349 """ 

350 

351 sign = self.handedness 

352 if sign == 0: 

353 sign = 1 

354 

355 # LQ decomposition provides an axis-aligned description of the cell. 

356 # Q is an orthogonal matrix and L is a lower triangular matrix. The 

357 # decomposition is a unique description if the diagonal elements are 

358 # all positive (negative for a left-handed cell). 

359 Q, L = np.linalg.qr(self.T) 

360 Q = Q.T 

361 L = L.T 

362 

363 # correct the signs of the diagonal elements 

364 signs = np.sign(np.diag(L)) 

365 indices = np.where(signs == 0)[0] 

366 signs[indices] = 1 

367 indices = np.where(signs != sign)[0] 

368 L[:, indices] *= -1 

369 Q[indices] *= -1 

370 return Cell(L), Q 

371 

372 # XXX We want a reduction function that brings the cell into 

373 # standard form as defined by Setyawan and Curtarolo.