Coverage for /builds/debichem-team/python-ase/ase/build/root.py: 100.00%

71 statements  

« prev     ^ index     » next       coverage.py v7.5.3, created at 2025-03-06 04:00 +0000

1from math import atan2, cos, log10, sin 

2 

3import numpy as np 

4 

5 

6def hcp0001_root(symbol, root, size, a=None, c=None, 

7 vacuum=None, orthogonal=False): 

8 """HCP(0001) surface maniupulated to have a x unit side length 

9 of *root* before repeating. This also results in *root* number 

10 of repetitions of the cell. 

11 

12 

13 The first 20 valid roots for nonorthogonal are... 

14 1, 3, 4, 7, 9, 12, 13, 16, 19, 21, 25, 

15 27, 28, 31, 36, 37, 39, 43, 48, 49""" 

16 from ase.build import hcp0001 

17 atoms = hcp0001(symbol=symbol, size=(1, 1, size[2]), 

18 a=a, c=c, vacuum=vacuum, orthogonal=orthogonal) 

19 atoms = root_surface(atoms, root) 

20 atoms *= (size[0], size[1], 1) 

21 return atoms 

22 

23 

24def fcc111_root(symbol, root, size, a=None, 

25 vacuum=None, orthogonal=False): 

26 """FCC(111) surface maniupulated to have a x unit side length 

27 of *root* before repeating. This also results in *root* number 

28 of repetitions of the cell. 

29 

30 The first 20 valid roots for nonorthogonal are... 

31 1, 3, 4, 7, 9, 12, 13, 16, 19, 21, 25, 27, 

32 28, 31, 36, 37, 39, 43, 48, 49""" 

33 from ase.build import fcc111 

34 atoms = fcc111(symbol=symbol, size=(1, 1, size[2]), 

35 a=a, vacuum=vacuum, orthogonal=orthogonal) 

36 atoms = root_surface(atoms, root) 

37 atoms *= (size[0], size[1], 1) 

38 return atoms 

39 

40 

41def bcc111_root(symbol, root, size, a=None, 

42 vacuum=None, orthogonal=False): 

43 """BCC(111) surface maniupulated to have a x unit side length 

44 of *root* before repeating. This also results in *root* number 

45 of repetitions of the cell. 

46 

47 

48 The first 20 valid roots for nonorthogonal are... 

49 1, 3, 4, 7, 9, 12, 13, 16, 19, 21, 25, 

50 27, 28, 31, 36, 37, 39, 43, 48, 49""" 

51 from ase.build import bcc111 

52 atoms = bcc111(symbol=symbol, size=(1, 1, size[2]), 

53 a=a, vacuum=vacuum, orthogonal=orthogonal) 

54 atoms = root_surface(atoms, root) 

55 atoms *= (size[0], size[1], 1) 

56 return atoms 

57 

58 

59def point_in_cell_2d(point, cell, eps=1e-8): 

60 """This function takes a 2D slice of the cell in the XY plane and calculates 

61 if a point should lie in it. This is used as a more accurate method of 

62 ensuring we find all of the correct cell repetitions in the root surface 

63 code. The Z axis is totally ignored but for most uses this should be fine. 

64 """ 

65 # Define area of a triangle 

66 def tri_area(t1, t2, t3): 

67 t1x, t1y = t1[0:2] 

68 t2x, t2y = t2[0:2] 

69 t3x, t3y = t3[0:2] 

70 return abs(t1x * (t2y - t3y) + t2x * 

71 (t3y - t1y) + t3x * (t1y - t2y)) / 2 

72 

73 # c0, c1, c2, c3 define a parallelogram 

74 c0 = (0, 0) 

75 c1 = cell[0, 0:2] 

76 c2 = cell[1, 0:2] 

77 c3 = c1 + c2 

78 

79 # Get area of parallelogram 

80 cA = tri_area(c0, c1, c2) + tri_area(c1, c2, c3) 

81 

82 # Get area of triangles formed from adjacent vertices of parallelogram and 

83 # point in question. 

84 pA = tri_area(point, c0, c1) + tri_area(point, c1, c2) + \ 

85 tri_area(point, c2, c3) + tri_area(point, c3, c0) 

86 

87 # If combined area of triangles from point is larger than area of 

88 # parallelogram, point is not inside parallelogram. 

89 return pA <= cA + eps 

90 

91 

92def _root_cell_normalization(primitive_slab): 

93 """Returns the scaling factor for x axis and cell normalized by that 

94 factor""" 

95 

96 xscale = np.linalg.norm(primitive_slab.cell[0, 0:2]) 

97 cell_vectors = primitive_slab.cell[0:2, 0:2] / xscale 

98 return xscale, cell_vectors 

99 

100 

101def _root_surface_analysis(primitive_slab, root, eps=1e-8): 

102 """A tool to analyze a slab and look for valid roots that exist, up to 

103 the given root. This is useful for generating all possible cells 

104 without prior knowledge. 

105 

106 *primitive slab* is the primitive cell to analyze. 

107 

108 *root* is the desired root to find, and all below. 

109 

110 This is the internal function which gives extra data to root_surface. 

111 """ 

112 

113 # Setup parameters for cell searching 

114 logeps = int(-log10(eps)) 

115 _xscale, cell_vectors = _root_cell_normalization(primitive_slab) 

116 

117 # Allocate grid for cell search search 

118 points = np.indices((root + 1, root + 1)).T.reshape(-1, 2) 

119 

120 # Find points corresponding to full cells 

121 cell_points = [cell_vectors[0] * x + cell_vectors[1] * y for x, y in points] 

122 

123 # Find point close to the desired cell (floating point error possible) 

124 roots = np.around(np.linalg.norm(cell_points, axis=1)**2, logeps) 

125 

126 valid_roots = np.nonzero(roots == root)[0] 

127 if len(valid_roots) == 0: 

128 raise ValueError( 

129 "Invalid root {} for cell {}".format( 

130 root, cell_vectors)) 

131 int_roots = np.array([int(this_root) for this_root in roots 

132 if this_root.is_integer() and this_root <= root]) 

133 return cell_points, cell_points[np.nonzero( 

134 roots == root)[0][0]], set(int_roots[1:]) 

135 

136 

137def root_surface_analysis(primitive_slab, root, eps=1e-8): 

138 """A tool to analyze a slab and look for valid roots that exist, up to 

139 the given root. This is useful for generating all possible cells 

140 without prior knowledge. 

141 

142 *primitive slab* is the primitive cell to analyze. 

143 

144 *root* is the desired root to find, and all below.""" 

145 return _root_surface_analysis( 

146 primitive_slab=primitive_slab, root=root, eps=eps)[2] 

147 

148 

149def root_surface(primitive_slab, root, eps=1e-8): 

150 """Creates a cell from a primitive cell that repeats along the x and y 

151 axis in a way consisent with the primitive cell, that has been cut 

152 to have a side length of *root*. 

153 

154 *primitive cell* should be a primitive 2d cell of your slab, repeated 

155 as needed in the z direction. 

156 

157 *root* should be determined using an analysis tool such as the 

158 root_surface_analysis function, or prior knowledge. It should always 

159 be a whole number as it represents the number of repetitions.""" 

160 

161 atoms = primitive_slab.copy() 

162 

163 xscale, cell_vectors = _root_cell_normalization(primitive_slab) 

164 

165 # Do root surface analysis 

166 cell_points, root_point, _roots = _root_surface_analysis( 

167 primitive_slab, root, eps=eps) 

168 

169 # Find new cell 

170 root_angle = -atan2(root_point[1], root_point[0]) 

171 root_rotation = [[cos(root_angle), -sin(root_angle)], 

172 [sin(root_angle), cos(root_angle)]] 

173 root_scale = np.linalg.norm(root_point) 

174 

175 cell = np.array([np.dot(x, root_rotation) * 

176 root_scale for x in cell_vectors]) 

177 

178 # Find all cell centers within the cell 

179 shift = cell_vectors.sum(axis=0) / 2 

180 cell_points = [ 

181 point for point in cell_points if point_in_cell_2d( 

182 point + shift, cell, eps=eps)] 

183 

184 # Setup new cell 

185 atoms.rotate(root_angle, v="z") 

186 atoms *= (root, root, 1) 

187 atoms.cell[0:2, 0:2] = cell * xscale 

188 atoms.center() 

189 

190 # Remove all extra atoms 

191 del atoms[[atom.index for atom in atoms if not point_in_cell_2d( 

192 atom.position, atoms.cell, eps=eps)]] 

193 

194 # Rotate cell back to original orientation 

195 standard_rotation = [[cos(-root_angle), -sin(-root_angle), 0], 

196 [sin(-root_angle), cos(-root_angle), 0], 

197 [0, 0, 1]] 

198 

199 new_cell = np.array([np.dot(x, standard_rotation) for x in atoms.cell]) 

200 new_positions = np.array([np.dot(x, standard_rotation) 

201 for x in atoms.positions]) 

202 

203 atoms.cell = new_cell 

204 atoms.positions = new_positions 

205 return atoms