Coverage for /builds/kinetik161/ase/ase/build/bulk.py: 88.89%

171 statements  

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

1"""Build crystalline systems""" 

2from math import sqrt 

3from typing import Any 

4 

5from ase.atoms import Atoms 

6from ase.symbols import string2symbols 

7from ase.data import reference_states, atomic_numbers, chemical_symbols 

8from ase.utils import plural 

9 

10 

11def incompatible_cell(*, want, have): 

12 return RuntimeError(f'Cannot create {want} cell for {have} structure') 

13 

14 

15def bulk( 

16 name: str, 

17 crystalstructure: str = None, 

18 a: float = None, 

19 b: float = None, 

20 c: float = None, 

21 *, 

22 alpha: float = None, 

23 covera: float = None, 

24 u: float = None, 

25 orthorhombic: bool = False, 

26 cubic: bool = False, 

27 basis=None, 

28) -> Atoms: 

29 """Creating bulk systems. 

30 

31 Crystal structure and lattice constant(s) will be guessed if not 

32 provided. 

33 

34 name: str 

35 Chemical symbol or symbols as in 'MgO' or 'NaCl'. 

36 crystalstructure: str 

37 Must be one of sc, fcc, bcc, tetragonal, bct, hcp, rhombohedral, 

38 orthorhombic, mcl, diamond, zincblende, rocksalt, cesiumchloride, 

39 fluorite or wurtzite. 

40 a: float 

41 Lattice constant. 

42 b: float 

43 Lattice constant. If only a and b is given, b will be interpreted 

44 as c instead. 

45 c: float 

46 Lattice constant. 

47 alpha: float 

48 Angle in degrees for rhombohedral lattice. 

49 covera: float 

50 c/a ratio used for hcp. Default is ideal ratio: sqrt(8/3). 

51 u: float 

52 Internal coordinate for Wurtzite structure. 

53 orthorhombic: bool 

54 Construct orthorhombic unit cell instead of primitive cell 

55 which is the default. 

56 cubic: bool 

57 Construct cubic unit cell if possible. 

58 """ 

59 

60 if c is None and b is not None: 

61 # If user passes (a, b) positionally, we want it as (a, c) instead: 

62 c, b = b, c 

63 

64 if covera is not None and c is not None: 

65 raise ValueError("Don't specify both c and c/a!") 

66 

67 xref = '' 

68 ref: Any = {} 

69 

70 if name in chemical_symbols: # single element 

71 atomic_number = atomic_numbers[name] 

72 ref = reference_states[atomic_number] 

73 if ref is None: 

74 ref = {} # easier to 'get' things from empty dictionary than None 

75 else: 

76 xref = ref['symmetry'] 

77 

78 if crystalstructure is None: 

79 # `ref` requires `basis` but not given and not pre-defined 

80 if basis is None and 'basis' in ref and ref['basis'] is None: 

81 raise ValueError('This structure requires an atomic basis') 

82 if xref == 'cubic': 

83 # P and Mn are listed as 'cubic' but the lattice constants 

84 # are 7 and 9. They must be something other than simple cubic 

85 # then. We used to just return the cubic one but that must 

86 # have been wrong somehow. --askhl 

87 raise ValueError( 

88 f'The reference structure of {name} is not implemented') 

89 

90 # Mapping of name to number of atoms in primitive cell. 

91 structures = {'sc': 1, 'fcc': 1, 'bcc': 1, 

92 'tetragonal': 1, 

93 'bct': 1, 

94 'hcp': 1, 

95 'rhombohedral': 1, 

96 'orthorhombic': 1, 

97 'mcl': 1, 

98 'diamond': 1, 

99 'zincblende': 2, 'rocksalt': 2, 'cesiumchloride': 2, 

100 'fluorite': 3, 'wurtzite': 2} 

101 

102 if crystalstructure is None: 

103 crystalstructure = xref 

104 if crystalstructure not in structures: 

105 raise ValueError(f'No suitable reference data for bulk {name}.' 

106 f' Reference data: {ref}') 

107 

108 magmom_per_atom = None 

109 if crystalstructure == xref: 

110 magmom_per_atom = ref.get('magmom_per_atom') 

111 

112 if crystalstructure not in structures: 

113 raise ValueError(f'Unknown structure: {crystalstructure}.') 

114 

115 # Check name: 

116 natoms = len(string2symbols(name)) 

117 natoms0 = structures[crystalstructure] 

118 if natoms != natoms0: 

119 raise ValueError('Please specify {} for {} and not {}' 

120 .format(plural(natoms0, 'atom'), 

121 crystalstructure, natoms)) 

122 

123 if alpha is None: 

124 alpha = ref.get('alpha') 

125 

126 if a is None: 

127 if xref != crystalstructure: 

128 raise ValueError('You need to specify the lattice constant.') 

129 if 'a' in ref: 

130 a = ref['a'] 

131 else: 

132 raise KeyError(f'No reference lattice parameter "a" for "{name}"') 

133 

134 if b is None: 

135 bovera = ref.get('b/a') 

136 if bovera is not None and a is not None: 

137 b = bovera * a 

138 

139 if crystalstructure in ['hcp', 'wurtzite']: 

140 if cubic: 

141 raise incompatible_cell(want='cubic', have=crystalstructure) 

142 

143 if c is not None: 

144 covera = c / a 

145 elif covera is None: 

146 if xref == crystalstructure: 

147 covera = ref['c/a'] 

148 else: 

149 covera = sqrt(8 / 3) 

150 

151 if covera is None: 

152 covera = ref.get('c/a') 

153 if c is None and covera is not None: 

154 c = covera * a 

155 

156 if orthorhombic and crystalstructure not in ['sc', 'tetragonal', 

157 'orthorhombic']: 

158 atoms = _orthorhombic_bulk(name, crystalstructure, a, covera, u) 

159 elif cubic and crystalstructure in ['bcc', 'cesiumchloride']: 

160 atoms = _orthorhombic_bulk(name, crystalstructure, a, covera) 

161 elif cubic and crystalstructure != 'sc': 

162 atoms = _cubic_bulk(name, crystalstructure, a) 

163 elif crystalstructure == 'sc': 

164 atoms = Atoms(name, cell=(a, a, a), pbc=True) 

165 elif crystalstructure == 'fcc': 

166 b = a / 2 

167 atoms = Atoms(name, cell=[(0, b, b), (b, 0, b), (b, b, 0)], pbc=True) 

168 elif crystalstructure == 'bcc': 

169 b = a / 2 

170 atoms = Atoms(name, cell=[(-b, b, b), (b, -b, b), (b, b, -b)], 

171 pbc=True) 

172 elif crystalstructure == 'hcp': 

173 atoms = Atoms(2 * name, 

174 scaled_positions=[(0, 0, 0), 

175 (1 / 3, 2 / 3, 0.5)], 

176 cell=[(a, 0, 0), 

177 (-0.5 * a, a * sqrt(3) / 2, 0), 

178 (0, 0, covera * a)], 

179 pbc=True) 

180 elif crystalstructure == 'diamond': 

181 atoms = bulk(2 * name, 'zincblende', a) 

182 elif crystalstructure == 'zincblende': 

183 symbol1, symbol2 = string2symbols(name) 

184 atoms = bulk(symbol1, 'fcc', a) + bulk(symbol2, 'fcc', a) 

185 atoms.positions[1] += a / 4 

186 elif crystalstructure == 'rocksalt': 

187 symbol1, symbol2 = string2symbols(name) 

188 atoms = bulk(symbol1, 'fcc', a) + bulk(symbol2, 'fcc', a) 

189 atoms.positions[1, 0] += a / 2 

190 elif crystalstructure == 'cesiumchloride': 

191 symbol1, symbol2 = string2symbols(name) 

192 atoms = bulk(symbol1, 'sc', a) + bulk(symbol2, 'sc', a) 

193 atoms.positions[1, :] += a / 2 

194 elif crystalstructure == 'fluorite': 

195 symbol1, symbol2, symbol3 = string2symbols(name) 

196 atoms = \ 

197 bulk(symbol1, 'fcc', a) + \ 

198 bulk(symbol2, 'fcc', a) + \ 

199 bulk(symbol3, 'fcc', a) 

200 atoms.positions[1, :] += a / 4 

201 atoms.positions[2, :] += a * 3 / 4 

202 elif crystalstructure == 'wurtzite': 

203 u = u or 0.25 + 1 / 3 / covera**2 

204 atoms = Atoms(2 * name, 

205 scaled_positions=[(0, 0, 0), 

206 (1 / 3, 2 / 3, 0.5 - u), 

207 (1 / 3, 2 / 3, 0.5), 

208 (0, 0, 1 - u)], 

209 cell=[(a, 0, 0), 

210 (-0.5 * a, a * sqrt(3) / 2, 0), 

211 (0, 0, a * covera)], 

212 pbc=True) 

213 elif crystalstructure == 'bct': 

214 from ase.lattice import BCT 

215 if basis is None: 

216 basis = ref.get('basis') 

217 if basis is not None: 

218 natoms = len(basis) 

219 lat = BCT(a=a, c=c) 

220 atoms = Atoms([name] * natoms, cell=lat.tocell(), pbc=True, 

221 scaled_positions=basis) 

222 elif crystalstructure == 'rhombohedral': 

223 atoms = _build_rhl(name, a, alpha, basis) 

224 elif crystalstructure == 'orthorhombic': 

225 atoms = Atoms(name, cell=[a, b, c], pbc=True) 

226 else: 

227 raise ValueError(f'Unknown crystal structure: {crystalstructure!r}') 

228 

229 if magmom_per_atom is not None: 

230 magmoms = [magmom_per_atom] * len(atoms) 

231 atoms.set_initial_magnetic_moments(magmoms) 

232 

233 if orthorhombic: 

234 assert atoms.cell.orthorhombic 

235 

236 if cubic: 

237 assert abs(atoms.cell.angles() - 90).all() < 1e-10 

238 

239 return atoms 

240 

241 

242def _build_rhl(name, a, alpha, basis): 

243 from ase.lattice import RHL 

244 lat = RHL(a, alpha) 

245 cell = lat.tocell() 

246 if basis is None: 

247 # RHL: Given by A&M as scaled coordinates "x" of cell.sum(0): 

248 basis_x = reference_states[atomic_numbers[name]]['basis_x'] 

249 basis = basis_x[:, None].repeat(3, axis=1) 

250 natoms = len(basis) 

251 return Atoms([name] * natoms, cell=cell, scaled_positions=basis, pbc=True) 

252 

253 

254def _orthorhombic_bulk(name, crystalstructure, a, covera=None, u=None): 

255 if crystalstructure == 'fcc': 

256 b = a / sqrt(2) 

257 atoms = Atoms(2 * name, cell=(b, b, a), pbc=True, 

258 scaled_positions=[(0, 0, 0), (0.5, 0.5, 0.5)]) 

259 elif crystalstructure == 'bcc': 

260 atoms = Atoms(2 * name, cell=(a, a, a), pbc=True, 

261 scaled_positions=[(0, 0, 0), (0.5, 0.5, 0.5)]) 

262 elif crystalstructure == 'hcp': 

263 atoms = Atoms(4 * name, 

264 cell=(a, a * sqrt(3), covera * a), 

265 scaled_positions=[(0, 0, 0), 

266 (0.5, 0.5, 0), 

267 (0.5, 1 / 6, 0.5), 

268 (0, 2 / 3, 0.5)], 

269 pbc=True) 

270 elif crystalstructure == 'diamond': 

271 atoms = _orthorhombic_bulk(2 * name, 'zincblende', a) 

272 elif crystalstructure == 'zincblende': 

273 s1, s2 = string2symbols(name) 

274 b = a / sqrt(2) 

275 atoms = Atoms(2 * name, cell=(b, b, a), pbc=True, 

276 scaled_positions=[(0, 0, 0), (0.5, 0, 0.25), 

277 (0.5, 0.5, 0.5), (0, 0.5, 0.75)]) 

278 elif crystalstructure == 'rocksalt': 

279 s1, s2 = string2symbols(name) 

280 b = a / sqrt(2) 

281 atoms = Atoms(2 * name, cell=(b, b, a), pbc=True, 

282 scaled_positions=[(0, 0, 0), (0.5, 0.5, 0), 

283 (0.5, 0.5, 0.5), (0, 0, 0.5)]) 

284 elif crystalstructure == 'cesiumchloride': 

285 atoms = Atoms(name, cell=(a, a, a), pbc=True, 

286 scaled_positions=[(0, 0, 0), (0.5, 0.5, 0.5)]) 

287 elif crystalstructure == 'wurtzite': 

288 u = u or 0.25 + 1 / 3 / covera**2 

289 atoms = Atoms(4 * name, 

290 cell=(a, a * 3**0.5, covera * a), 

291 scaled_positions=[(0, 0, 0), 

292 (0, 1 / 3, 0.5 - u), 

293 (0, 1 / 3, 0.5), 

294 (0, 0, 1 - u), 

295 (0.5, 0.5, 0), 

296 (0.5, 5 / 6, 0.5 - u), 

297 (0.5, 5 / 6, 0.5), 

298 (0.5, 0.5, 1 - u)], 

299 pbc=True) 

300 else: 

301 raise incompatible_cell(want='orthorhombic', have=crystalstructure) 

302 

303 return atoms 

304 

305 

306def _cubic_bulk(name: str, crystalstructure: str, a: float) -> Atoms: 

307 if crystalstructure == 'fcc': 

308 atoms = Atoms(4 * name, cell=(a, a, a), pbc=True, 

309 scaled_positions=[(0, 0, 0), (0, 0.5, 0.5), 

310 (0.5, 0, 0.5), (0.5, 0.5, 0)]) 

311 elif crystalstructure == 'diamond': 

312 atoms = _cubic_bulk(2 * name, 'zincblende', a) 

313 elif crystalstructure == 'zincblende': 

314 atoms = Atoms(4 * name, cell=(a, a, a), pbc=True, 

315 scaled_positions=[(0, 0, 0), (0.25, 0.25, 0.25), 

316 (0, 0.5, 0.5), (0.25, 0.75, 0.75), 

317 (0.5, 0, 0.5), (0.75, 0.25, 0.75), 

318 (0.5, 0.5, 0), (0.75, 0.75, 0.25)]) 

319 elif crystalstructure == 'rocksalt': 

320 atoms = Atoms(4 * name, cell=(a, a, a), pbc=True, 

321 scaled_positions=[(0, 0, 0), (0.5, 0, 0), 

322 (0, 0.5, 0.5), (0.5, 0.5, 0.5), 

323 (0.5, 0, 0.5), (0, 0, 0.5), 

324 (0.5, 0.5, 0), (0, 0.5, 0)]) 

325 elif crystalstructure == 'fluorite': 

326 atoms = Atoms( 

327 4 * name, 

328 cell=(a, a, a), 

329 pbc=True, 

330 scaled_positions=[ 

331 (0.00, 0.00, 0.00), (0.25, 0.25, 0.25), (0.75, 0.75, 0.75), 

332 (0.00, 0.50, 0.50), (0.25, 0.75, 0.75), (0.75, 0.25, 0.25), 

333 (0.50, 0.00, 0.50), (0.75, 0.25, 0.75), (0.25, 0.75, 0.25), 

334 (0.50, 0.50, 0.00), (0.75, 0.75, 0.25), (0.25, 0.25, 0.75), 

335 ], 

336 ) 

337 else: 

338 raise incompatible_cell(want='cubic', have=crystalstructure) 

339 

340 return atoms