Hide keyboard shortcuts

Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

79

80

81

82

83

84

85

86

87

88

89

90

91

92

93

94

95

96

97

98

99

100

101

102

103

104

105

106

107

108

109

110

111

112

113

114

115

116

117

118

119

120

121

122

123

124

125

126

127

128

129

130

131

132

133

134

135

136

137

138

139

140

141

142

143

144

145

146

147

148

149

150

151

152

153

154

155

156

157

158

159

160

161

162

163

164

165

166

167

168

169

170

171

172

173

174

175

176

177

178

179

180

181

182

183

184

185

186

187

188

189

190

191

192

193

194

195

196

197

198

199

200

201

202

203

204

205

206

207

208

209

210

211

212

213

214

215

216

217

218

219

220

221

222

223

224

225

226

227

228

229

230

231

232

233

234

235

236

237

238

239

240

241

242

243

244

245

246

247

248

249

250

251

252

253

254

255

256

257

258

259

260

261

262

263

264

265

266

267

268

269

270

271

272

273

274

275

276

277

278

279

280

281

282

283

""" 

Collection of functions and classes for handling information concerning atoms 

and structures, including the relationship between primitive cell and 

supercells that are derived thereof. 

""" 

 

import numbers 

import pickle 

from ase import Atoms as aseAtoms 

import numpy as np 

from .relate_structures import relate_structures 

from ..io.logging import logger 

 

# TODO: Rename logger 

logger = logger.getChild('align_supercell') 

 

 

class Atom: 

# TODO: This class should inherit some immutable to make it clear that 

# there is no reference to any other obj 

"""Unique representation of an atom in a lattice with a basis 

 

Class for storing information about the position of an atom in a supercell 

relative to the origin of the underlying primitive cell. This class is used 

for handling the relationship between a primitive cell and supercells 

derived thereof. 

Initialize self 

 

Parameters 

---------- 

site/offset: 

An Atom object can be initialized by providing either 

 

* four elements, which are interpreted as 

`site`, `offset_x`, `offset_y`, `offset_z` 

* a list with four elements, which is interpreted as 

[`site`, `offset_x`, `offset_y`, `offset_z`], or 

* a list with two elements, which is interpreted as 

[`site`, [`offset_x`, `offset_y`, `offset_z`]]. 

 

where all of the elements are integers. Here, `site` indicates the 

index of a site in the primitive basis and `offset` describes the 

translation of the atom position in the supercell relative to the 

origin in units of the primitive cell vectors. 

 

Examples 

-------- 

The three following commands yield identical results 

 

>>> atom = Atom(1, 0, 0, 3) 

>>> atom = Atom(1, [0, 0, 3]) 

>>> atom = Atom([1, 0, 0, 3]) 

""" 

def __init__(self, *args): 

# TODO: Decide how to docstring classes! 

# TODO: Explicit init rather than implicit init. 

# If only one argument it must be a list (#3 in examples) 

if len(args) == 1: 

site = args[0][0] 

offset = tuple(args[0][1:]) 

# If two arguments it must be a site and an offset array (#2) 

62 ↛ 66line 62 didn't jump to line 66, because the condition on line 62 was never false elif len(args) == 2: 

site = args[0] 

offset = tuple(args[1]) 

# If four arguments it must be (#3) 

elif len(args) == 4: 

site = args[0] 

offset = tuple(args[1:]) 

else: 

raise TypeError 

 

assert isinstance(site, numbers.Integral) 

assert all(isinstance(i, numbers.Integral) for i in offset) 

assert len(offset) == 3 

assert site >= 0 

 

self._site = site 

self._offset = offset 

 

@property 

def site(self): 

"""int : index of corresponding site in the primitive basis""" 

return self._site 

 

@property 

def offset(self): 

"""list of ints : translational offset of the supercell site relative 

to the origin of the primitive cell in units of primitive lattice 

vectors""" 

return self._offset 

 

def __repr__(self): 

return 'Atom({}, {})'.format(self.site, self.offset) 

 

def spos(atom, basis): 

return np.add(basis[atom.site], atom.offset) 

 

def pos(atom, basis, cell): 

spos = atom.spos(basis) 

return np.dot(spos, cell) 

 

@staticmethod 

def spos_to_atom(spos, basis, tol=None): 

104 ↛ 107line 104 didn't jump to line 107, because the condition on line 104 was never false if not tol: 

# TODO: Link to config file 

tol = 1e-4 

107 ↛ 117line 107 didn't jump to line 117, because the loop on line 107 didn't complete for site, base in enumerate(basis): 

offset = np.subtract(spos, base) 

diff = offset - np.round(offset, 0) 

if np.linalg.norm(diff) < tol: 

offset = np.round(offset, 0).astype(int) 

atom = Atom(site, offset) 

assert np.linalg.norm(spos - atom.spos(basis)) < tol, ( 

'{} with basis {} != {}'.format(atom, basis, spos)) 

return atom 

 

s = '{} not compatible with {} and tolerance {}' 

raise Exception(s.format(spos, basis, tol)) 

 

def __hash__(self): 

return hash((self._site, *self.offset)) 

 

def __eq__(self, other): 

124 ↛ 125line 124 didn't jump to line 125, because the condition on line 124 was never true if not isinstance(other, Atom): 

return False 

return self.site == other.site and self.offset == other.offset 

 

 

class Atoms(aseAtoms): 

"""Minimally augmented version of the ASE Atoms class suitable for handling 

primitive cell information. 

 

Saves and loads by pickle. 

 

""" 

@property 

def basis(self): 

"""NumPy array : scaled coordinates of the sites in the primitive basis 

""" 

return self.get_scaled_positions().round(12) % 1 

 

def write(self, f): 

""" Write the object to file. 

 

Note: Only the cell, basis and numbers are stored! 

 

Parameters 

---------- 

f : str or file object 

name of input file (str) or stream to write to (file object) 

""" 

data = {} 

data['cell'] = self.cell 

data['basis'] = self.basis 

data['numbers'] = self.numbers 

 

try: 

pickle.dump(data, f) 

except Exception: 

raise Exception('Failed writing to file.') 

 

@staticmethod 

def read(f): 

""" Load an hiPhive Atoms object from file. 

 

Parameters 

---------- 

f : str or file object 

name of input file (str) or stream to load from (file object) 

 

Returns 

------- 

hiPhive Atoms object 

""" 

try: 

data = pickle.load(f) 

except Exception: 

raise Exception('Failed loading from file.') 

atoms = aseAtoms(numbers=data['numbers'], 

scaled_positions=data['basis'], 

cell=data['cell'], 

pbc=True) 

return Atoms(atoms) 

 

 

def atom_to_spos(atom, basis): 

"""Helper function for obtaining the position of a supercell atom in scaled 

coordinates. 

 

Parameters 

---------- 

atom : hiPhive Atom object 

supercell atom 

basis : list 

positions of sites in the primitive basis 

 

Returns 

------- 

array : NumPy array 

scaled coordinates of an atom in a supercell 

""" 

return np.add(atom.offset, basis[atom.site]) 

 

 

def spos_to_atom(spos, basis, tol=1e-4): 

"""Helper function for transforming a supercell position to the primitive 

basis. 

 

Parameters 

---------- 

spos : list 

scaled coordinates of an atom in a supercell 

basis : list 

positions of sites in the primitive basis 

tol : float 

a general tolerance 

 

Returns 

------- 

hiPhive Atom object 

supercell atom 

""" 

# TODO: Fix tolerance 

# Loop over all sites in the basis 

225 ↛ 245line 225 didn't jump to line 245, because the loop on line 225 didn't complete for site, base in enumerate(basis): 

# If the scaled position belongs to this site, the offset is the 

# difference in scaled coordinates and should be integer 

offset = np.subtract(spos, base) 

# The diff is the difference between the offset vector and the nearest 

# integer vector. 

diff = offset - np.round(offset, 0) 

# It should be close to the null vector if this is the correct site. 

if np.linalg.norm(diff) < tol: 

# If the difference is less than the tol make the offset integers 

offset = np.round(offset, 0).astype(int) 

# This should be the correct atom 

atom = Atom(site, offset) 

# Just to be sure we check that the atom actually produces the 

# input spos given the input basis 

s = ('Atom=[{},{}] with basis {} != {}' 

.format(atom.site, atom.offset, basis, spos)) 

assert np.linalg.norm(spos - atom_to_spos(atom, basis)) < tol, s 

return atom 

# If no atom was found we throw an error 

s = '{} not compatible with {} and tolerance {}'.format(spos, basis, tol) 

# TODO: Should we throw more explicit error? 

raise Exception(s) 

 

 

def align_super_cell(sc, prim, symprec=None): 

"""Rotate and translate a supercell configuration such that it is aligned 

with the target primitive cell. 

 

Parameters 

---------- 

sc : ASE Atoms object 

supercell configuration 

prim : ASE Atoms object 

target primitive configuration 

symprec : float 

precision parameter forwarded to spglib 

 

Returns 

------- 

tuple of ASE Atoms object, NumPy (3,3) array, 3-dimensional vector 

The method returns the aligned supercell configuration as well as the 

rotation matrix and the translation vector that related the input to 

the aligned supercell configuration. 

""" 

 

# TODO: Make sure the input is what we expect 

 

R, T = relate_structures(sc, prim) 

 

# Create the aligned system 

aligned_sc = Atoms(sc.copy()) 

aligned_sc.pbc = False 

aligned_sc.positions = np.dot(R, aligned_sc.positions.T).T + T 

aligned_sc.cell = np.dot(R, sc.cell.T).T 

aligned_sc.pbc = True 

aligned_sc.wrap() 

 

return aligned_sc, R, T