Coverage for /builds/ase/ase/ase/cluster/wulff.py: 51.52%

99 statements  

« prev     ^ index     » next       coverage.py v7.5.3, created at 2025-08-02 00:12 +0000

1# fmt: off 

2 

3import numpy as np 

4 

5delta = 1e-10 

6 

7 

8def wulff_construction(symbol, surfaces, energies, size, structure, 

9 rounding='closest', latticeconstant=None, 

10 debug=False, maxiter=100): 

11 """Create a cluster using the Wulff construction. 

12 

13 A cluster is created with approximately the number of atoms 

14 specified, following the Wulff construction, i.e. minimizing the 

15 surface energy of the cluster. 

16 

17 Parameters 

18 ---------- 

19 symbol : str or int 

20 The chemical symbol (or atomic number) of the desired element. 

21 

22 surfaces : list 

23 A list of surfaces. Each surface is an (h, k, l) tuple or list of 

24 integers. 

25 

26 energies : list 

27 A list of surface energies for the surfaces. 

28 

29 size : int 

30 The desired number of atoms. 

31 

32 structure : {'fcc', bcc', 'sc'} 

33 The desired crystal structure. 

34 

35 rounding : {'closest', 'above', 'below'} 

36 Specifies what should be done if no Wulff construction corresponds 

37 to exactly the requested number of atoms. 'above', 'below', and 

38 'closest' mean that the nearest cluster above or below - or the 

39 closest one - is created instead. 

40 

41 latticeconstant : float (optional) 

42 The lattice constant. If not given, extracted from `ase.data`. 

43 

44 debug : bool, default False 

45 If True, information about the iteration towards the right cluster 

46 size is printed. 

47 """ 

48 

49 if debug: 

50 print('Wulff: Aiming for cluster with %i atoms (%s)' % 

51 (size, rounding)) 

52 

53 if rounding not in ['above', 'below', 'closest']: 

54 raise ValueError(f'Invalid rounding: {rounding}') 

55 

56 # Interpret structure, if it is a string. 

57 if isinstance(structure, str): 

58 if structure == 'fcc': 

59 from ase.cluster.cubic import FaceCenteredCubic as structure 

60 elif structure == 'bcc': 

61 from ase.cluster.cubic import BodyCenteredCubic as structure 

62 elif structure == 'sc': 

63 from ase.cluster.cubic import SimpleCubic as structure 

64 elif structure == 'hcp': 

65 from ase.cluster.hexagonal import HexagonalClosedPacked as structure 

66 elif structure == 'graphite': 

67 from ase.cluster.hexagonal import Graphite as structure 

68 else: 

69 error = f'Crystal structure {structure} is not supported.' 

70 raise NotImplementedError(error) 

71 

72 # Check number of surfaces 

73 nsurf = len(surfaces) 

74 if len(energies) != nsurf: 

75 raise ValueError('The energies array should contain %d values.' 

76 % (nsurf,)) 

77 

78 # Copy energies array so it is safe to modify it 

79 energies = np.array(energies) 

80 

81 # We should check that for each direction, the surface energy plus 

82 # the energy in the opposite direction is positive. But this is 

83 # very difficult in the general case! 

84 

85 # Before starting, make a fake cluster just to extract the 

86 # interlayer distances in the relevant directions, and use these 

87 # to "renormalize" the surface energies such that they can be used 

88 # to convert to number of layers instead of to distances. 

89 atoms = structure(symbol, surfaces, 5 * np.ones(len(surfaces), int), 

90 latticeconstant=latticeconstant) 

91 for i, s in enumerate(surfaces): 

92 d = atoms.get_layer_distance(s) 

93 energies[i] /= d 

94 

95 # First guess a size that is not too large. 

96 wanted_size = size ** (1.0 / 3.0) 

97 max_e = max(energies) 

98 factor = wanted_size / max_e 

99 atoms, layers = make_atoms(symbol, surfaces, energies, factor, structure, 

100 latticeconstant) 

101 if len(atoms) == 0: 

102 # Probably the cluster is very flat 

103 if debug: 

104 print('First try made an empty cluster, trying again.') 

105 factor = 1 / energies.min() 

106 atoms, layers = make_atoms(symbol, surfaces, energies, factor, 

107 structure, latticeconstant) 

108 if len(atoms) == 0: 

109 raise RuntimeError('Failed to create a finite cluster.') 

110 

111 # Second guess: scale to get closer. 

112 old_factor = factor 

113 old_layers = layers 

114 old_atoms = atoms 

115 factor *= (size / len(atoms))**(1.0 / 3.0) 

116 atoms, layers = make_atoms(symbol, surfaces, energies, factor, 

117 structure, latticeconstant) 

118 if len(atoms) == 0: 

119 print('Second guess gave an empty cluster, discarding it.') 

120 atoms = old_atoms 

121 factor = old_factor 

122 layers = old_layers 

123 else: 

124 del old_atoms 

125 

126 # Find if the cluster is too small or too large (both means perfect!) 

127 below = above = None 

128 if len(atoms) <= size: 

129 below = atoms 

130 if len(atoms) >= size: 

131 above = atoms 

132 

133 # Now iterate towards the right cluster 

134 iter = 0 

135 while (below is None or above is None): 

136 if len(atoms) < size: 

137 # Find a larger cluster 

138 if debug: 

139 print('Making a larger cluster.') 

140 factor = ((layers + 0.5 + delta) / energies).min() 

141 atoms, new_layers = make_atoms(symbol, surfaces, energies, factor, 

142 structure, latticeconstant) 

143 assert (new_layers - layers).max() == 1 

144 assert (new_layers - layers).min() >= 0 

145 layers = new_layers 

146 else: 

147 # Find a smaller cluster 

148 if debug: 

149 print('Making a smaller cluster.') 

150 factor = ((layers - 0.5 - delta) / energies).max() 

151 atoms, new_layers = make_atoms(symbol, surfaces, energies, factor, 

152 structure, latticeconstant) 

153 assert (new_layers - layers).max() <= 0 

154 assert (new_layers - layers).min() == -1 

155 layers = new_layers 

156 if len(atoms) <= size: 

157 below = atoms 

158 if len(atoms) >= size: 

159 above = atoms 

160 iter += 1 

161 if iter == maxiter: 

162 raise RuntimeError('Runaway iteration.') 

163 if rounding == 'below': 

164 if debug: 

165 print('Choosing smaller cluster with %i atoms' % len(below)) 

166 return below 

167 elif rounding == 'above': 

168 if debug: 

169 print('Choosing larger cluster with %i atoms' % len(above)) 

170 return above 

171 else: 

172 assert rounding == 'closest' 

173 if (len(above) - size) < (size - len(below)): 

174 atoms = above 

175 else: 

176 atoms = below 

177 if debug: 

178 print('Choosing closest cluster with %i atoms' % len(atoms)) 

179 return atoms 

180 

181 

182def make_atoms(symbol, surfaces, energies, factor, structure, latticeconstant): 

183 layers1 = factor * np.array(energies) 

184 layers = np.round(layers1).astype(int) 

185 atoms = structure(symbol, surfaces, layers, 

186 latticeconstant=latticeconstant) 

187 return (atoms, layers)