Coverage for /builds/debichem-team/python-ase/ase/cell.py: 100.00%

141 statements  

« prev     ^ index     » next       coverage.py v7.5.3, created at 2025-03-06 04:00 +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=None, copy=False): 

231 return np.array(self.array, dtype=dtype, copy=copy) 

232 

233 def __bool__(self): 

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

235 

236 @property 

237 def volume(self) -> float: 

238 """Get the volume of this cell. 

239 

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

241 # Fail or 0 for <3D cells? 

242 # Definitely 0 since this is currently a property. 

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

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

245 

246 @property 

247 def handedness(self) -> int: 

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

249 

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

251 do not span three dimensions.""" 

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

253 

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

255 """Calculate scaled positions from Cartesian positions. 

256 

257 The scaled positions are the positions given in the basis 

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

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

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

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

262 

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

264 """Calculate Cartesian positions from scaled positions.""" 

265 return scaled_positions @ self.complete() 

266 

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

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

269 

270 The reciprocal cell is defined such that 

271 

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

273 

274 within machine precision. 

275 

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

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

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

279 return icell 

280 

281 def normal(self, i): 

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

283 

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

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

286 

287 def normals(self): 

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

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

290 

291 def area(self, i): 

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

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

294 

295 def areas(self): 

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

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

298 

299 def __repr__(self): 

300 if self.orthorhombic: 

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

302 else: 

303 numbers = self.tolist() 

304 

305 return f'Cell({numbers})' 

306 

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

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

309 

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

311 from ase.build.tools import niggli_reduce_cell 

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

313 result = Cell(cell) 

314 return result, op 

315 

316 def minkowski_reduce(self): 

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

318 

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

320 from ase.geometry.minkowski_reduction import minkowski_reduce 

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

322 result = Cell(cell) 

323 return result, op 

324 

325 def permute_axes(self, permutation): 

326 """Permute axes of cell.""" 

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

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

329 return permuted 

330 

331 def standard_form(self): 

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

333 handedness is preserved. 

334 

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

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

337 are negative. 

338 

339 Returns: 

340 

341 rcell: the standardized cell object 

342 

343 Q: ndarray 

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

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

346 """ 

347 

348 sign = self.handedness 

349 if sign == 0: 

350 sign = 1 

351 

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

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

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

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

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

357 Q = Q.T 

358 L = L.T 

359 

360 # correct the signs of the diagonal elements 

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

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

363 signs[indices] = 1 

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

365 L[:, indices] *= -1 

366 Q[indices] *= -1 

367 return Cell(L), Q 

368 

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

370 # standard form as defined by Setyawan and Curtarolo.