Coverage for /builds/ase/ase/ase/cell.py: 99.32%

147 statements  

« prev     ^ index     » next       coverage.py v7.5.3, created at 2025-08-02 00:12 +0000

1# fmt: off 

2 

3from typing import Mapping, Sequence, Union 

4 

5import numpy as np 

6 

7import ase 

8from ase.utils import pbc2pbc 

9from ase.utils.arraywrapper import arraylike 

10 

11__all__ = ['Cell'] 

12 

13 

14@arraylike 

15class Cell: 

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

17 

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

19 Cartesian coordinate of the ith unit vector. 

20 

21 Cells of less than three dimensions are represented by placeholder 

22 unit vectors that are zero.""" 

23 

24 ase_objtype = 'cell' # For JSON'ing 

25 

26 def __init__(self, array): 

27 """Create cell. 

28 

29 Parameters: 

30 

31 array: 3x3 arraylike object 

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

33 """ 

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

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

36 self.array = array 

37 

38 def cellpar(self, radians=False): 

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

40 

41 First three are unit cell vector lengths and second three 

42 are angles between them:: 

43 

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

45 

46 in degrees. 

47 

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

49 from ase.geometry.cell import cell_to_cellpar 

50 return cell_to_cellpar(self.array, radians) 

51 

52 def todict(self): 

53 return dict(array=self.array) 

54 

55 @classmethod 

56 def ascell(cls, cell): 

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

58 

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

60 if isinstance(cell, cls): 

61 return cell 

62 return cls.new(cell) 

63 

64 @classmethod 

65 def new(cls, cell=None): 

66 """Create new cell from any parameters. 

67 

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

69 

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

71 

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

73 

74 if cell is None: 

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

76 

77 cell = np.array(cell, float) 

78 

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

80 cell = np.diag(cell) 

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

82 from ase.geometry.cell import cellpar_to_cell 

83 cell = cellpar_to_cell(cell) 

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

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

86 'sequence or 3x3 matrix!') 

87 

88 cellobj = cls(cell) 

89 return cellobj 

90 

91 @classmethod 

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

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

94 

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

96 from ase.geometry.cell import cellpar_to_cell 

97 cell = cellpar_to_cell(cellpar, ab_normal, a_direction) 

98 return cls(cell) 

99 

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

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

102 

103 >>> from ase.cell import Cell 

104 

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

106 >>> print(cell.get_bravais_lattice()) 

107 FCC(a=5.65685) 

108 

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

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

111 differ from the original cell by a permutation or other 

112 operation which maps it to the AFlow convention. For 

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

114 

115 To build a bandpath for a particular cell, use 

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

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

118 

119 """ 

120 from ase.lattice import identify_lattice 

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

122 lat, _op = identify_lattice(self, eps=eps, pbc=pbc) 

123 return lat 

124 

125 def bandpath( 

126 self, 

127 path: str = None, 

128 npoints: int = None, 

129 *, 

130 density: float = None, 

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

132 eps: float = 2e-4, 

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

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

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

136 

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

138 this cell and return a suitable Brillouin zone path with 

139 standard special points. 

140 

141 If special special points are given, interpolate the path 

142 directly from the available data. 

143 

144 Parameters: 

145 

146 path: string 

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

148 npoints: int 

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

150 is added for each special point in the path. 

151 density: float 

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

153 special_points: dict 

154 Dictionary mapping special points to scaled kpoint coordinates. 

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

156 eps: float 

157 Tolerance for determining Bravais lattice. 

158 pbc: three bools 

159 Whether cell is periodic in each direction. Normally not 

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

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

162 

163 Example 

164 ------- 

165 

166 >>> from ase.cell import Cell 

167 

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

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

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

171 

172 """ 

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

174 

175 cell = self.uncomplete(pbc) 

176 

177 if special_points is None: 

178 from ase.lattice import identify_lattice 

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

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

181 return bandpath.transform(op) 

182 else: 

183 from ase.dft.kpoints import BandPath, resolve_custom_points 

184 path, special_points = resolve_custom_points( 

185 path, special_points, eps=eps) 

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

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

188 

189 def uncomplete(self, pbc): 

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

191 _pbc = np.empty(3, bool) 

192 _pbc[:] = pbc 

193 cell = self.copy() 

194 cell[~_pbc] = 0 

195 return cell 

196 

197 def complete(self): 

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

199 from ase.geometry.cell import complete_cell 

200 return Cell(complete_cell(self.array)) 

201 

202 def copy(self): 

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

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

205 

206 def mask(self): 

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

208 return self.any(1) 

209 

210 @property 

211 def rank(self) -> int: 

212 """"Return the dimension of the cell. 

213 

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

215 # The name ndim clashes with ndarray.ndim 

216 return sum(self.mask()) 

217 

218 @property 

219 def orthorhombic(self) -> bool: 

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

221 from ase.geometry.cell import is_orthorhombic 

222 return is_orthorhombic(self) 

223 

224 def lengths(self): 

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

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

227 

228 def angles(self): 

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

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

231 

232 def __array__(self, dtype=None, copy=False): 

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

234 

235 def __bool__(self): 

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

237 

238 @property 

239 def volume(self) -> float: 

240 """Get the volume of this cell. 

241 

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

243 # Fail or 0 for <3D cells? 

244 # Definitely 0 since this is currently a property. 

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

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

247 

248 @property 

249 def handedness(self) -> int: 

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

251 

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

253 do not span three dimensions.""" 

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

255 

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

257 """Calculate scaled positions from Cartesian positions. 

258 

259 The scaled positions are the positions given in the basis 

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

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

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

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

264 

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

266 """Calculate Cartesian positions from scaled positions.""" 

267 return scaled_positions @ self.complete() 

268 

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

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

271 

272 The reciprocal cell is defined such that 

273 

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

275 

276 within machine precision. 

277 

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

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

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

281 return icell 

282 

283 def normal(self, i): 

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

285 

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

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

288 

289 def normals(self): 

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

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

292 

293 def area(self, i): 

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

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

296 

297 def areas(self): 

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

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

300 

301 def __repr__(self): 

302 if self.orthorhombic: 

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

304 else: 

305 numbers = self.tolist() 

306 

307 return f'Cell({numbers})' 

308 

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

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

311 

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

313 from ase.build.tools import niggli_reduce_cell 

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

315 result = Cell(cell) 

316 return result, op 

317 

318 def minkowski_reduce(self): 

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

320 

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

322 from ase.geometry.minkowski_reduction import minkowski_reduce 

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

324 result = Cell(cell) 

325 return result, op 

326 

327 def permute_axes(self, permutation): 

328 """Permute axes of cell.""" 

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

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

331 return permuted 

332 

333 def standard_form(self, form='lower'): 

334 """Rotate axes such that unit cell is lower/upper triangular. The cell 

335 handedness is preserved. 

336 

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

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

339 are negative. 

340 

341 Parameters: 

342 

343 form: str 

344 'lower' or 'upper' triangular form. The default is 'lower'. 

345 

346 Returns: 

347 

348 rcell: the standardized cell object 

349 

350 Q: ndarray 

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

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

353 """ 

354 

355 sign = self.handedness 

356 if sign == 0: 

357 sign = 1 

358 

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

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

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

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

363 if form == 'lower': 

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

365 Q = Q.T 

366 L = L.T 

367 elif form == 'upper': 

368 Q, L = np.linalg.qr(self.T[::-1, ::-1]) 

369 Q = Q.T[::-1, ::-1] 

370 L = L.T[::-1, ::-1] 

371 else: 

372 raise ValueError('form must be either "lower" or "upper"') 

373 

374 # correct the signs of the diagonal elements 

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

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

377 signs[indices] = 1 

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

379 L[:, indices] *= -1 

380 Q[indices] *= -1 

381 return Cell(L), Q 

382 

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

384 # standard form as defined by Setyawan and Curtarolo.