Coverage for wulffpack/core/form.py: 98%

82 statements  

« prev     ^ index     » next       coverage.py v7.4.4, created at 2024-04-02 08:51 +0000

1from typing import List, Dict, Union 

2import numpy as np 

3from .facet import Facet 

4from .geometry import (is_array_in_arrays, 

5 where_is_array_in_arrays) 

6 

7 

8class Form(): 

9 """ 

10 A `Form` object contains all facets that are equivalent by the 

11 symmetry of the system under consideration. For a cubic system, 

12 this means that the form with Miller indices {100} will contain 

13 the facets with normals [100], [-100], [010] etc. 

14 

15 Parameters 

16 ---------- 

17 miller_indices 

18 Miller indices for a representative member of the form. 

19 energy 

20 The surface energy per area for a facet in this form. 

21 cell 

22 The cell, with the basis vectors as columns. 

23 symmetries 

24 Symmetry elements of the system. If symmetry is broken 

25 by, e.g., the presence of an interface, this list 

26 should only contain the symmetries surviving. 

27 parent_miller_indices 

28 If symmetry is broken, it may still be of interest to 

29 know what its form would be had not the symmetry been 

30 broken. This attribute contains the Miller indices for 

31 a representative of such a form. 

32 """ 

33 

34 def __init__(self, miller_indices: tuple, 

35 energy: float, 

36 cell: np.ndarray, 

37 symmetries: List[np.ndarray], 

38 parent_miller_indices: tuple): 

39 self.miller_indices = miller_indices 

40 self.energy = energy 

41 self.parent_miller_indices = parent_miller_indices 

42 

43 # Create all facets belonging to this form 

44 self.facets = [] 

45 used_normals = [] 

46 reciprocal_cell = np.linalg.inv(cell).T 

47 normal = np.dot(reciprocal_cell, self.miller_indices) 

48 normal_scaled = np.linalg.solve(cell, normal) 

49 for R in symmetries: 

50 new_normal_scaled = np.dot(R, normal_scaled) 

51 new_normal = np.dot(cell, new_normal_scaled) 

52 array_index = where_is_array_in_arrays(new_normal, used_normals) 

53 if array_index == -1: 

54 # Then we found a new facet 

55 self.facets.append(Facet(normal=tuple(new_normal), 

56 energy=energy, 

57 symmetry=R)) 

58 used_normals.append(new_normal) 

59 else: 

60 # If the facet was already there, save instead the 

61 # symmetry operation that took us there 

62 facet = self.facets[array_index] 

63 assert np.allclose(facet.normal, new_normal / np.linalg.norm(new_normal)) 

64 facet.symmetries.append(R) 

65 

66 @property 

67 def area(self) -> float: 

68 """Returns the total area of the facets in this form.""" 

69 return len(self.facets) * self.facets[0].area 

70 

71 @property 

72 def surface_energy(self) -> float: 

73 """Returns the total surface energy of the facets in this form.""" 

74 return len(self.facets) * self.facets[0].surface_energy 

75 

76 @property 

77 def volume(self) -> float: 

78 """ 

79 Returns the total volume formed by pyramids formed by each 

80 facet and the origin. 

81 """ 

82 return len(self.facets) * self.facets[0].get_volume() 

83 

84 @property 

85 def edge_length(self) -> float: 

86 """Returns the length of all facets in the form.""" 

87 return len(self.facets) * self.facets[0].perimeter_length 

88 

89 

90def setup_forms(surface_energies: Dict, 

91 cell: np.ndarray, 

92 symmetries_restricted: List[np.ndarray], 

93 symmetries_full: List[np.ndarray], 

94 twin_boundary: Union[List, tuple] = None, 

95 interface: Union[List, tuple] = None, 

96 tol: float = 1e-6) -> Dict: 

97 """ 

98 Create an adapted dictionary of surface energies based on 

99 the symmetries specified. 

100 

101 This function is relevant for polycrystalline particles, the crystal 

102 structure of which possess higher symmetry than the grains they are made 

103 from. A decahedron, for example, is made of five FCC grains. FCC, being a 

104 cubic crystal lattice, has 48 symmetry elements, but the grain is one of 

105 five and has much fewer symmetry elements. This means that there are 

106 multiple facets that would belong to the {111} form had not the symmetry 

107 been broken. In the decahedral case, there are thus three inequivalent 

108 families of facets that in the cubic case would all have been equivalent 

109 facets belonging to the {111} form. These three families are, respectively, 

110 the twin facets, the usually large facets "on top and underneath" the 

111 particle as well as the re-entrance facets(the "notches"). All of these 

112 will get their own key in the dictionary, describing a representative 

113 member of the form. 

114 

115 Parameters 

116 ---------- 

117 surface_energies 

118 keys are either tuples with three integers (describing a form in the 

119 cubic case) or the string `'twin'`/`'interface'`, values are 

120 corresponding surface energies 

121 cell 

122 the basis vectors as columns 

123 symmetries_restricted 

124 the matrices for the symmetry elements in the broken symmetry 

125 symmetry_full 

126 the matrices for the symmetry elements in the case of full symmetry 

127 (for example the 48 symmetry elements of the m-3m point group) 

128 twin_boundary 

129 Miller index for a twin boundary if there is one 

130 interface 

131 Miller index for an interface(to a substrate for example) 

132 tol 

133 Numerical tolerance when checking if energies are identical 

134 

135 Returns 

136 ------- 

137 dictionary 

138 keys are tuples describing the form(for each inequivalent form in the 

139 broken symmetry case) with three integer, values are corresponding 

140 surface energies 

141 """ 

142 reciprocal_cell = np.linalg.inv(cell).T 

143 forms = [] 

144 scaled_normals = [] # To be able to identify over-specified facets 

145 ordered_facets = [] # --"-- 

146 if min(surface_energies.values()) < 0: 

147 raise ValueError('Please use only positive ' 

148 'surface/twin/interface energies') 

149 

150 for form, energy in surface_energies.items(): 

151 inequivalent_normals_scaled = [] 

152 if form == 'twin': 

153 if twin_boundary: 

154 forms.append(Form(miller_indices=twin_boundary, 

155 energy=energy / 2, 

156 cell=cell, 

157 symmetries=symmetries_restricted, 

158 parent_miller_indices=form)) 

159 elif form == 'interface': 

160 if interface: 

161 forms.append(Form(miller_indices=interface, 

162 energy=energy, 

163 cell=cell, 

164 symmetries=symmetries_restricted, 

165 parent_miller_indices=form)) 

166 else: 

167 if len(form) == 4: 

168 miller_indices = convert_bravais_miller_to_miller(form) 

169 else: 

170 miller_indices = form 

171 normal = np.dot(reciprocal_cell, miller_indices) # Cartesian coords 

172 normal_scaled = np.linalg.solve(cell, normal) # scaled coords 

173 

174 # Check that this facet is not symmetrically equivalent to another 

175 scaled_normals.append(normal_scaled) 

176 ordered_facets.append((form, energy)) 

177 for R_r in symmetries_full: 

178 transformed_normal_scaled = np.dot(R_r, normal_scaled) 

179 

180 previous_index = where_is_array_in_arrays(transformed_normal_scaled, 

181 scaled_normals[:-1]) 

182 if previous_index != -1: 

183 # Then this facet is redundant. 

184 # If it has the same energy, we just skip it, 

185 # otherwise we need to throw an exception. 

186 if abs(energy - ordered_facets[previous_index][1]) > tol: 

187 raise ValueError( 

188 f'The Miller indices {form} ' 

189 f'are equivalent to {ordered_facets[previous_index][0]} ' 

190 'by symmetry. If your system is such that they should be ' 

191 'inequivalent, please specify a primitive structure with ' 

192 'the appropriate point group symmetry or provide a ' 

193 'custom set of symmetries.') 

194 else: 

195 break 

196 else: 

197 # Then this facet is not symmetrically equivalent to another. 

198 # Now we need to look at all symmetrically equivalent versions 

199 # and check whether broken symmetry makes them inequivalent. 

200 for R_r in symmetries_full: 

201 transformed_normal_scaled = np.dot(R_r, normal_scaled) 

202 for R_f in symmetries_restricted: 

203 if is_array_in_arrays(np.dot(R_f, transformed_normal_scaled), 

204 inequivalent_normals_scaled): 

205 break 

206 else: 

207 transformed_normal = np.dot(cell, transformed_normal_scaled) 

208 miller = np.linalg.solve(reciprocal_cell, transformed_normal) 

209 rounded_miller = np.round(miller) 

210 assert np.allclose(miller, rounded_miller) 

211 rounded_miller = tuple(int(i) for i in rounded_miller) 

212 forms.append(Form(miller_indices=rounded_miller, 

213 energy=energy, 

214 cell=cell, 

215 symmetries=symmetries_restricted, 

216 parent_miller_indices=form)) 

217 inequivalent_normals_scaled.append(transformed_normal_scaled) 

218 return forms 

219 

220 

221def convert_bravais_miller_to_miller(bravais_miller_indices: tuple) -> tuple: 

222 """ 

223 Returns the Miller indices(three integer tuple) 

224 corresponding to Bravais-Miller indices(for integer 

225 tuple). 

226 

227 Paramters 

228 --------- 

229 bravais_miller_indices 

230 Four integer tuple 

231 """ 

232 if sum(bravais_miller_indices[:3]) != 0: 232 ↛ 233line 232 didn't jump to line 233, because the condition on line 232 was never true

233 raise ValueError('Invalid Bravais-Miller indices (h, k, i, l), ' 

234 'h + k + i != 0') 

235 return tuple(bravais_miller_indices[i] for i in [0, 1, 3])