Coverage for icet/tools/geometry.py: 97%

99 statements  

« prev     ^ index     » next       coverage.py v7.5.0, created at 2024-12-26 04:12 +0000

1import numpy as np 

2import spglib 

3from typing import List, Sequence, Tuple, TypeVar 

4from ase import Atoms 

5from ase.data import chemical_symbols 

6from icet.core.lattice_site import LatticeSite 

7from icet.core.structure import Structure 

8 

9Vector = List[float] 

10T = TypeVar('T') 

11 

12 

13def get_scaled_positions(positions: np.ndarray, 

14 cell: np.ndarray, 

15 wrap: bool = True, 

16 pbc: List[bool] = [True, True, True]) -> np.ndarray: 

17 """ 

18 Returns the positions in reduced (or scaled) coordinates. 

19 

20 Parameters 

21 ---------- 

22 positions 

23 Atomic positions in Cartesian coordinates. 

24 cell 

25 Cell metric. 

26 wrap 

27 If ``True`` positions outside the unit cell will be wrapped into 

28 the cell in the directions with periodic boundary conditions 

29 such that the scaled coordinates are between zero and one. 

30 pbc 

31 Periodic boundary conditions. 

32 """ 

33 

34 fractional = np.linalg.solve(cell.T, positions.T).T 

35 

36 if wrap: 

37 for i, periodic in enumerate(pbc): 

38 if periodic: 

39 # Yes, we need to do it twice. 

40 # See the scaled_positions.py test. 

41 fractional[:, i] %= 1.0 

42 fractional[:, i] %= 1.0 

43 

44 return fractional 

45 

46 

47def get_primitive_structure(structure: Atoms, 

48 no_idealize: bool = True, 

49 to_primitive: bool = True, 

50 symprec: float = 1e-5) -> Atoms: 

51 """ 

52 Returns the primitive structure using spglib. 

53 

54 Parameters 

55 ---------- 

56 structure 

57 Input atomic structure. 

58 no_idealize 

59 If ``True`` lengths and angles are not idealized. 

60 to_primitive 

61 If ``True`` convert to primitive structure. 

62 symprec 

63 Tolerance imposed when analyzing the symmetry using spglib. 

64 """ 

65 structure_copy = structure.copy() 

66 structure_as_tuple = ase_atoms_to_spglib_cell(structure_copy) 

67 

68 ret = spglib.standardize_cell( 

69 structure_as_tuple, to_primitive=to_primitive, 

70 no_idealize=no_idealize, symprec=symprec) 

71 if ret is None: 

72 raise ValueError('spglib failed to find the primitive cell, maybe caused by large symprec.') 

73 lattice, scaled_positions, numbers = ret 

74 scaled_positions = [np.round(pos, 12) for pos in scaled_positions] 

75 structure_prim = Atoms(scaled_positions=scaled_positions, 

76 numbers=numbers, cell=lattice, pbc=structure.pbc) 

77 structure_prim.wrap() 

78 

79 return structure_prim 

80 

81 

82def get_fractional_positions_from_neighbor_list( 

83 structure: Structure, neighbor_list: List) -> List[Vector]: 

84 """ 

85 Returns the fractional positions of the lattice sites in structure from 

86 a neighbor list. 

87 

88 Parameters 

89 ---------- 

90 structure 

91 Input atomic structure. 

92 neighbor_list 

93 List of lattice neighbors of the input structure. 

94 """ 

95 neighbor_positions = [] 

96 fractional_positions = [] 

97 lattice_site = LatticeSite(0, [0, 0, 0]) 

98 

99 for i in range(len(neighbor_list)): 

100 lattice_site.index = i 

101 position = structure.get_position(lattice_site) 

102 neighbor_positions.append(position) 

103 for neighbor in neighbor_list[i]: 

104 position = structure.get_position(neighbor) 

105 neighbor_positions.append(position) 

106 

107 if len(neighbor_positions) > 0: 107 ↛ 113line 107 didn't jump to line 113, because the condition on line 107 was never false

108 fractional_positions = get_scaled_positions( 

109 np.array(neighbor_positions), 

110 structure.cell, wrap=False, 

111 pbc=structure.pbc) 

112 

113 return fractional_positions 

114 

115 

116def get_position_from_lattice_site(structure: Atoms, lattice_site: LatticeSite): 

117 """ 

118 Returns the corresponding position from the lattice site. 

119 

120 Parameters 

121 --------- 

122 structure 

123 Input atomic structure. 

124 lattice_site 

125 Specific lattice site of the input structure. 

126 """ 

127 return structure[lattice_site.index].position + \ 

128 np.dot(lattice_site.unitcell_offset, structure.cell) 

129 

130 

131def fractional_to_cartesian(structure: Atoms, 

132 frac_positions: List[Vector]) -> np.ndarray: 

133 """ 

134 Converts fractional positions into Cartesian positions. 

135 

136 Parameters 

137 ---------- 

138 structure 

139 Input atomic structure. 

140 frac_positions 

141 Fractional positions. 

142 """ 

143 return np.dot(frac_positions, structure.cell) 

144 

145 

146def get_permutation(container: Sequence[T], 

147 permutation: List[int]) -> Sequence[T]: 

148 """ 

149 Returns the permuted version of container. 

150 """ 

151 if len(permutation) != len(container): 

152 raise RuntimeError('Container and permutation not of same size.' 

153 f'{len(container)} != {len(permutation)}') 

154 if len(set(permutation)) != len(permutation): 

155 raise Exception 

156 return [container[s] for s in permutation] 

157 

158 

159def ase_atoms_to_spglib_cell(structure: Atoms) -> Tuple[np.ndarray, np.ndarray, np.ndarray]: 

160 """Returns a tuple comprising three components, corresponding to cell 

161 metric, atomic positions, and atomic species. 

162 

163 Parameters 

164 ---------- 

165 structure 

166 Input atomic structure. 

167 """ 

168 return (structure.cell, structure.get_scaled_positions(), structure.get_atomic_numbers()) 

169 

170 

171def get_occupied_primitive_structure(structure: Atoms, 

172 allowed_species: List[List[str]], 

173 symprec: float) -> Tuple[Atoms, List[Tuple[str, ...]]]: 

174 """Returns an occupied primitive structure with hydrogen on 

175 sublattice 1, Helium on sublattice 2 and so on 

176 

177 Parameters 

178 ---------- 

179 structure 

180 Input structure. 

181 allowed_species 

182 Chemical symbols that are allowed on each site. 

183 symprec 

184 Tolerance imposed when analyzing the symmetry using spglib. 

185 """ 

186 if len(structure) != len(allowed_species): 186 ↛ 187line 186 didn't jump to line 187, because the condition on line 186 was never true

187 raise ValueError( 

188 'structure and chemical symbols need to be the same size.') 

189 sorted_symbols = sorted({tuple(sorted(s)) for s in allowed_species}) 

190 

191 decorated_primitive = structure.copy() 

192 for i, sym in enumerate(allowed_species): 

193 sublattice = sorted_symbols.index(tuple(sorted(sym))) + 1 

194 decorated_primitive[i].symbol = chemical_symbols[sublattice] 

195 

196 decorated_primitive = get_primitive_structure(decorated_primitive, symprec=symprec) 

197 decorated_primitive.wrap() 

198 primitive_chemical_symbols: List[Tuple[str, ...]] = [] 

199 for atom in decorated_primitive: 

200 sublattice = chemical_symbols.index(atom.symbol) 

201 primitive_chemical_symbols.append(sorted_symbols[sublattice - 1]) 

202 

203 for symbols in allowed_species: 

204 if tuple(sorted(symbols)) in primitive_chemical_symbols: 

205 index = primitive_chemical_symbols.index(tuple(sorted(symbols))) 

206 primitive_chemical_symbols[index] = symbols 

207 return decorated_primitive, primitive_chemical_symbols 

208 

209 

210def atomic_number_to_chemical_symbol(numbers: List[int]) -> List[str]: 

211 """Returns the chemical symbols equivalent to the input atomic 

212 numbers. 

213 

214 Parameters 

215 ---------- 

216 numbers 

217 Atomic numbers. 

218 """ 

219 

220 symbols = [chemical_symbols[number] for number in numbers] 

221 return symbols 

222 

223 

224def chemical_symbols_to_numbers(symbols: List[str]) -> List[int]: 

225 """Returns the atomic numbers equivalent to the input chemical 

226 symbols. 

227 

228 Parameters 

229 ---------- 

230 symbols 

231 Chemical symbols. 

232 """ 

233 

234 numbers = [chemical_symbols.index(symbols) for symbols in symbols] 

235 return numbers 

236 

237 

238def get_wyckoff_sites(structure: Atoms, 

239 map_occupations: List[List[str]] = None, 

240 symprec: float = 1e-5) -> List[str]: 

241 """Returns the Wyckoff symbols of the input structure. The Wyckoff 

242 sites are of general interest for symmetry analysis but can be 

243 especially useful when setting up, e.g., a 

244 :class:`SiteOccupancyObserver 

245 <mchammer.observers.SiteOccupancyObserver>`. 

246 The Wyckoff labels can be conveniently attached as an array to the 

247 structure object as demonstrated in the examples section below. 

248 

249 By default the occupation of the sites is part of the symmetry 

250 analysis. If a chemically disordered structure is provided this 

251 will usually reduce the symmetry substantially. If one is 

252 interested in the symmetry of the underlying structure one can 

253 control how occupations are handled. To this end, one can provide 

254 the :attr:`map_occupations` keyword argument. The latter must be a 

255 list, each entry of which is a list of species that should be 

256 treated as indistinguishable. As a shortcut, if *all* species 

257 should be treated as indistinguishable one can provide an empty 

258 list. Examples that illustrate the usage of the keyword are given 

259 below. 

260 

261 Parameters 

262 ---------- 

263 structure 

264 Input structure. Note that the occupation of the sites is 

265 included in the symmetry analysis. 

266 map_occupations 

267 Each sublist in this list specifies a group of chemical 

268 species that shall be treated as indistinguishable for the 

269 purpose of the symmetry analysis. 

270 symprec 

271 Tolerance imposed when analyzing the symmetry using spglib. 

272 

273 Examples 

274 -------- 

275 Wyckoff sites of a hexagonal-close packed structure:: 

276 

277 >>> from ase.build import bulk 

278 >>> structure = bulk('Ti') 

279 >>> wyckoff_sites = get_wyckoff_sites(structure) 

280 >>> print(wyckoff_sites) 

281 ['2d', '2d'] 

282 

283 

284 The Wyckoff labels can also be attached as an array to the 

285 structure, in which case the information is also included when 

286 storing the Atoms object:: 

287 

288 >>> from ase.io import write 

289 >>> structure.new_array('wyckoff_sites', wyckoff_sites, str) 

290 >>> write('structure.xyz', structure) 

291 

292 The function can also be applied to supercells:: 

293 

294 >>> structure = bulk('GaAs', crystalstructure='zincblende', a=3.0).repeat(2) 

295 >>> wyckoff_sites = get_wyckoff_sites(structure) 

296 >>> print(wyckoff_sites) 

297 ['4a', '4c', '4a', '4c', '4a', '4c', '4a', '4c', 

298 '4a', '4c', '4a', '4c', '4a', '4c', '4a', '4c'] 

299 

300 Now assume that one is given a supercell of a (Ga,Al)As 

301 alloy. Applying the function directly yields much lower symmetry 

302 since the symmetry of the original structure is broken:: 

303 

304 >>> structure.set_chemical_symbols( 

305 ... ['Ga', 'As', 'Al', 'As', 'Ga', 'As', 'Al', 'As', 

306 ... 'Ga', 'As', 'Ga', 'As', 'Al', 'As', 'Ga', 'As']) 

307 >>> print(get_wyckoff_sites(structure)) 

308 ['8g', '8i', '4e', '8i', '8g', '8i', '2c', '8i', 

309 '2d', '8i', '8g', '8i', '4e', '8i', '8g', '8i'] 

310 

311 Since Ga and Al occupy the same sublattice, they should, however, 

312 be treated as indistinguishable for the purpose of the symmetry 

313 analysis, which can be achieved via the :attr:`map_occupations` 

314 keyword:: 

315 

316 >>> print(get_wyckoff_sites(structure, map_occupations=[['Ga', 'Al'], ['As']])) 

317 ['4a', '4c', '4a', '4c', '4a', '4c', '4a', '4c', 

318 '4a', '4c', '4a', '4c', '4a', '4c', '4a', '4c'] 

319 

320 If occupations are to ignored entirely, one can simply provide an 

321 empty list. In the present case, this turns the zincblende lattice 

322 into a diamond lattice, on which case there is only one Wyckoff 

323 site:: 

324 

325 >>> print(get_wyckoff_sites(structure, map_occupations=[])) 

326 ['8a', '8a', '8a', '8a', '8a', '8a', '8a', '8a', 

327 '8a', '8a', '8a', '8a', '8a', '8a', '8a', '8a'] 

328 """ 

329 structure_copy = structure.copy() 

330 if map_occupations is not None: 

331 if len(map_occupations) > 0: 

332 new_symbols = [] 

333 for symb in structure_copy.get_chemical_symbols(): 

334 for group in map_occupations: 334 ↛ 333line 334 didn't jump to line 333, because the loop on line 334 didn't complete

335 if symb in group: 

336 new_symbols.append(group[0]) 

337 break 

338 else: 

339 new_symbols = len(structure) * ['H'] 

340 structure_copy.set_chemical_symbols(new_symbols) 

341 dataset = spglib.get_symmetry_dataset(ase_atoms_to_spglib_cell(structure_copy), symprec=symprec) 

342 n_unitcells = np.linalg.det(dataset['transformation_matrix']) 

343 

344 equivalent_atoms = list(dataset['equivalent_atoms']) 

345 wyckoffs = {} 

346 for index in set(equivalent_atoms): 

347 multiplicity = list(dataset['equivalent_atoms']).count(index) / n_unitcells 

348 multiplicity = int(round(multiplicity)) 

349 wyckoffs[index] = '{}{}'.format(multiplicity, dataset['wyckoffs'][index]) 

350 

351 return [wyckoffs[equivalent_atoms[a.index]] for a in structure_copy]