Coverage for hiphive/core/structures.py: 94%

109 statements  

« prev     ^ index     » next       coverage.py v7.10.1, created at 2025-08-01 17:04 +0000

1import numpy as np 

2from ase import Atoms 

3 

4 

5def site_offset_to_spos( 

6 site: int, 

7 offset: np.ndarray, 

8 basis_spos: list[np.ndarray], 

9) -> np.ndarray: 

10 """ Returns the scaled position of an atom at the specified site and offset 

11 relative to the basis in scaled coordinates. 

12 

13 Parameters 

14 ---------- 

15 site 

16 Site index. 

17 offset 

18 Offset in scaled coordinates (should be integers). 

19 basis_spos 

20 Positions of atoms in basis in scaled coordinates. 

21 """ 

22 return offset + basis_spos[site] 

23 

24 

25def spos_to_pos(spos: np.ndarray, cell: np.ndarray) -> np.ndarray: 

26 """ Returns the Cartesian coordinate given the scaled coordinate and cell 

27 metric (cell vectors as rows). 

28 

29 Parameters 

30 ---------- 

31 spos 

32 Position in scaled coordinates. 

33 cell 

34 Cell metric. 

35 """ 

36 return np.dot(spos, cell) 

37 

38 

39def pos_to_spos(pos: np.ndarray, cell: np.ndarray) -> np.ndarray: 

40 """Returns the scaled coordinate given the Cartesian coordinate and the cell metric. 

41 Inverse of :func:`spos_to_pos`. 

42 

43 Parameters 

44 ---------- 

45 pos 

46 Position in Cartesian coordinates. 

47 cell 

48 Cell metric. 

49 """ 

50 return np.linalg.solve(cell.T, pos) 

51 

52 

53def spos_to_site_offset( 

54 spos: np.ndarray, 

55 basis_spos: list[np.ndarray], 

56 symprec: float, 

57) -> np.ndarray: 

58 """ Returns the site and offset of the atom at the specified scaled 

59 coordinate given the scaled positions of the basis atoms. 

60 

61 Parameters 

62 ---------- 

63 spos 

64 Position in scaled coordinates. 

65 basis_spos 

66 Positions of atoms in basis in scaled coordinates. 

67 symprec 

68 Tolerance imposed when rounding the offset. 

69 """ 

70 for site, sp in enumerate(basis_spos): 70 ↛ 76line 70 didn't jump to line 76 because the loop on line 70 didn't complete

71 offset = spos - sp 

72 rounded_offset = offset.round(0).astype(np.int64) 

73 # TODO: fix tolerance (symprec should be w.r.t. cart. coord.) 

74 if np.allclose(rounded_offset, offset, rtol=0, atol=symprec): 

75 return site, rounded_offset 

76 raise Exception('spos {} not compatible with basis {} using symprec {}' 

77 .format(spos, basis_spos, symprec)) 

78 

79 

80def pos_to_site_offset( 

81 pos: np.ndarray, 

82 cell: np.ndarray, 

83 basis_spos: list[np.ndarray], 

84 symprec: float, 

85) -> np.ndarray: 

86 """Returns offset given the position in Cartesian coordinates and the cell metric. 

87 

88 Parameters 

89 ---------- 

90 pos 

91 Position in Cartesian coordinates. 

92 cell 

93 Cell metric. 

94 basis_spos 

95 Positions of atoms in basis in scaled coordinates. 

96 symprec 

97 Tolerance imposed when rounding the offset. 

98 """ 

99 spos = pos_to_spos(pos, cell) 

100 return spos_to_site_offset(spos, basis_spos, symprec) 

101 

102 

103def site_offset_to_pos( 

104 site: int, 

105 offset: np.ndarray, 

106 cell: np.ndarray, 

107 basis_spos: list[np.ndarray], 

108) -> np.ndarray: 

109 """Returns the position in Cartesian coordinates given a site index, 

110 an offset, the cell metric, and the position in the basis. 

111 

112 Parameters 

113 ---------- 

114 site 

115 Site index. 

116 offset 

117 Offset in scaled coordinates (should be integers). 

118 cell 

119 Cell metric. 

120 basis_spos 

121 Positions of atoms in basis in scaled coordinates. 

122 """ 

123 spos = site_offset_to_spos(site, offset, basis_spos) 

124 return spos_to_pos(spos, cell) 

125 

126 

127class BaseAtom: 

128 """ This class represents an atom placed in an infinite crystal. 

129 

130 Attributes 

131 ---------- 

132 site : int 

133 Site index. 

134 offset : list[int] 

135 Offset in scaled coordinates. 

136 """ 

137 def __init__(self, site, offset): 

138 assert type(site) is int, type(site) 

139 assert len(offset) == 3, len(offset) 

140 assert (all(type(i) is int for i in offset) or 

141 all(type(i) is np.int64 for i in offset)), type(offset[0]) 

142 self._site = site 

143 self._offset = np.array(offset) 

144 

145 @property 

146 def site(self) -> int: 

147 """ Site index. """ 

148 return self._site 

149 

150 @property 

151 def offset(self) -> np.ndarray: 

152 """ Offset in scaled coordinates. """ 

153 return self._offset 

154 

155 def astype(self, dtype): 

156 """ Useful arguments: list, tuple, np.int64""" 

157 return dtype((self._site, *self._offset)) 

158 

159 

160class Atom(BaseAtom): 

161 """ This class represents a crystal atom in a given structure. 

162 """ 

163 def __init__(self, *args, **kwargs): 

164 self._structure = kwargs.pop('structure', None) 

165 super().__init__(*args, **kwargs) 

166 

167 @property 

168 def pos(self) -> np.ndarray: 

169 return site_offset_to_pos(self._site, self._offset, 

170 self._structure.cell, 

171 self._structure.spos) 

172 

173 @property 

174 def number(self) -> int: 

175 return self._structure.numbers[self._site] 

176 

177 

178class SupercellAtom(Atom): 

179 """ Represents an atom in a supercell but site and offset given by an 

180 underlying primitve cell.""" 

181 def __init__(self, *args, **kwargs): 

182 self._index = kwargs.pop('index') 

183 assert type(self._index) is int 

184 super().__init__(*args, **kwargs) 

185 

186 @property 

187 def index(self): 

188 return self._index 

189 

190 

191class Structure: 

192 """ This class essentially wraps the ASE :class:`Atoms` class but is a bit more 

193 careful with respect to periodic boundary conditions and scaled coordinates. 

194 Note that it returns :class:`hiphive.Atom` objects when queried for individual atoms. 

195 

196 Parameters 

197 ---------- 

198 atoms 

199 Atomic structure. 

200 symprec 

201 Tolerance imposed when rounding scaled coordinates. 

202 """ 

203 def __init__(self, atoms: Atoms, symprec: float = 1e-6): 

204 spos = atoms.get_scaled_positions(wrap=False) 

205 for sp in spos.flat: 

206 if not (-symprec < sp < (1 - symprec)): 206 ↛ 207line 206 didn't jump to line 207 because the condition on line 206 was never true

207 raise ValueError('bad spos {}'.format(sp)) 

208 self._spos = spos 

209 self._cell = atoms.cell 

210 self._numbers = atoms.numbers 

211 

212 def __len__(self): 

213 return len(self._spos) 

214 

215 @property 

216 def spos(self) -> np.ndarray: 

217 """ Scaled coordinates. """ 

218 return self._spos 

219 

220 @property 

221 def cell(self): 

222 """ Cell metric. """ 

223 return self._cell 

224 

225 def __getitem__(self, index): 

226 if index >= len(self): 

227 raise IndexError('Structure contains {} atoms'.format(len(self))) 

228 return Atom(index, (0, 0, 0), structure=self) 

229 

230 def atom_from_pos(self, pos: np.ndarray, symprec: float = None) -> Atom: 

231 """ Returns atom given a position in Cartesian coordinates. 

232 

233 Parameters 

234 ---------- 

235 pos 

236 Position in Cartesian coordinates. 

237 symprec 

238 Tolerance imposed when rounding scaled coordinates. 

239 """ 

240 if symprec is None: 240 ↛ 241line 240 didn't jump to line 241 because the condition on line 240 was never true

241 symprec = self._symprec 

242 site, offset = pos_to_site_offset(pos, self._cell, self._spos, symprec) 

243 return Atom(site, offset, structure=self) 

244 

245 

246class Supercell: 

247 """ This class tries to represent atoms in a supercell as positioned on the 

248 primitive lattice. 

249 

250 Parameters 

251 ---------- 

252 supercell 

253 Supercell structure. 

254 prim 

255 Primitive structure. 

256 symprec 

257 Tolerance imposed when rounding scaled coordinates. 

258 """ 

259 def __init__(self, supercell: Atoms, prim: Atoms, symprec: float): 

260 self._supercell = Structure(supercell) 

261 self._prim = Structure(prim) 

262 self._symprec = symprec 

263 self._map = list() 

264 self._inverse_map_lookup = dict() 

265 self._create_map() 

266 

267 def _create_map(self): 

268 for atom in self._supercell: 

269 atom = self._prim.atom_from_pos(atom.pos, self._symprec) 

270 self._map.append(atom.astype(tuple)) 

271 

272 def wrap_atom(self, atom): 

273 atom = Atom(atom.site, atom.offset, structure=self._prim) 

274 tup = atom.astype(tuple) 

275 index = self._inverse_map_lookup.get(tup, None) 

276 if index is None: 

277 atom = self._supercell.atom_from_pos(atom.pos, self._symprec) 

278 index = atom.site 

279 self._inverse_map_lookup[tup] = index 

280 return self[index] 

281 

282 def index(self, site: int, offset: np.ndarray) -> int: 

283 """" Returns index of atom given a site index and an offset. 

284 

285 Parameters 

286 ---------- 

287 site 

288 Site index. 

289 offset 

290 Offset in scaled coordinates (should be integers). 

291 """ 

292 atom = self.wrap_atom(BaseAtom(site, offset)) 

293 return atom.index 

294 

295 def __getitem__(self, index): 

296 tup = self._map[index] 

297 return SupercellAtom(tup[0], tup[1:], structure=self._prim, 

298 index=index) 

299 

300 def __len__(self): 

301 return len(self._supercell)