Coverage for hiphive/cutoffs.py: 90%

122 statements  

« prev     ^ index     » next       coverage.py v7.6.8, created at 2024-11-28 11:20 +0000

1import pickle 

2import numpy as np 

3from ase.neighborlist import NeighborList 

4from hiphive.input_output.pretty_table_prints import table_array_to_string 

5 

6 

7class Cutoffs: 

8 """ This class maintains information about the cutoff configuration, 

9 i.e. which clusters will be included (="inside cutoff"). It also 

10 encapsulates functionality that is used e.g., during cluster space 

11 construction. 

12 

13 Here, `n-body` refers to number of atoms in a cluster. For example 

14 the cluster (0011) is a two-body cluster of fourth order and the 

15 cluster (123) is a three-body cluster of third order. 

16 

17 Parameters 

18 ---------- 

19 cutoff_matrix : numpy.ndarray 

20 the matrix element `ij` provides to the cutoff for order `j+2` 

21 and nbody `i+2`; elements with `i>j` will be ignored 

22 """ 

23 

24 def __init__(self, cutoff_matrix): 

25 

26 self._cutoff_matrix = np.array(cutoff_matrix, dtype=float) 

27 

28 if len(self._cutoff_matrix.shape) != 2: 28 ↛ 29line 28 didn't jump to line 29 because the condition on line 28 was never true

29 raise ValueError('Please specify cutoff matrix as a 2D array') 

30 for i, row in enumerate(self._cutoff_matrix): 

31 if np.any(row[i:] < 0): 31 ↛ 32line 31 didn't jump to line 32 because the condition on line 31 was never true

32 raise ValueError('Negative number as cutoff') 

33 row[:i] = np.nan 

34 self._cutoff_matrix = self._cutoff_matrix[:(self.max_nbody-1), :(self.max_order-1)] 

35 

36 @property 

37 def cutoff_matrix(self): 

38 """ numpy.ndarray : copy of cutoff matrix """ 

39 return self._cutoff_matrix.copy() 

40 

41 @property 

42 def orders(self): 

43 """ list(int) : allowed orders """ 

44 return list(range(2, self.max_order + 1)) 

45 

46 @property 

47 def nbodies(self): 

48 """ list(int) : allowed bodies """ 

49 return list(range(2, self.max_nbody + 1)) 

50 

51 def get_cutoff(self, order, nbody): 

52 """ 

53 Returns cutoff for a given body and order. 

54 

55 Parameters 

56 ---------- 

57 order : int 

58 nbody : int 

59 

60 Raises 

61 ------ 

62 ValueError 

63 if order is not in orders 

64 ValueError 

65 if nbody is not in nbodies 

66 ValueError 

67 if nbody is larger than order 

68 

69 Returns 

70 ------- 

71 float 

72 """ 

73 if order not in self.orders: 73 ↛ 74line 73 didn't jump to line 74 because the condition on line 73 was never true

74 raise ValueError('order not in orders') 

75 if nbody not in self.nbodies: 75 ↛ 76line 75 didn't jump to line 76 because the condition on line 75 was never true

76 raise ValueError('nbody not in nbodies') 

77 if nbody > order: 77 ↛ 78line 77 didn't jump to line 78 because the condition on line 77 was never true

78 raise ValueError('nbody can not be larger than order') 

79 return self._cutoff_matrix[nbody - 2, order - 2] 

80 

81 @property 

82 def max_cutoff(self): 

83 """ float : maximum cutoff """ 

84 max_cutoff = 0 

85 for i, row in enumerate(self._cutoff_matrix): 

86 max_cutoff = max(max_cutoff, np.max(row[i:])) 

87 return max_cutoff 

88 

89 @property 

90 def max_nbody(self): 

91 """ int : maximum body """ 

92 nbody = 1 

93 for i, row in enumerate(self._cutoff_matrix): 

94 if np.any(row[i:]): 94 ↛ 93line 94 didn't jump to line 93 because the condition on line 94 was always true

95 nbody = i + 2 

96 return nbody 

97 

98 @property 

99 def max_order(self): 

100 """ int : maximum order """ 

101 order = None 

102 for col in range(self._cutoff_matrix.shape[1]): 

103 if np.any(self._cutoff_matrix[:col + 1, col]): 103 ↛ 102line 103 didn't jump to line 102 because the condition on line 103 was always true

104 order = col + 2 

105 return order 

106 

107 def max_nbody_cutoff(self, nbody): 

108 """ Return maximum cutoff for a given body. """ 

109 if nbody not in self.nbodies: 109 ↛ 110line 109 didn't jump to line 110 because the condition on line 109 was never true

110 raise ValueError('nbody not in nbodies') 

111 return np.max(self._cutoff_matrix[nbody - 2, max(0, nbody - 2):]) 

112 

113 def max_nbody_order(self, nbody): 

114 """ Returns maximum order for a given body """ 

115 if nbody not in self.nbodies: 115 ↛ 116line 115 didn't jump to line 116 because the condition on line 115 was never true

116 raise ValueError('nbody not in nbodies') 

117 row = self._cutoff_matrix[nbody - 2] 

118 max_order = None 

119 for order, cutoff in enumerate(row[nbody-2:], start=nbody): 

120 if cutoff: 120 ↛ 119line 120 didn't jump to line 119 because the condition on line 120 was always true

121 max_order = order 

122 return max_order 

123 

124 def write(self, fileobj): 

125 """ Writes instance to file. 

126 

127 Parameters 

128 ---------- 

129 fileobj : file-like object 

130 file-like object to which the cutoffs will be written to 

131 """ 

132 pickle.dump(self._cutoff_matrix, fileobj) 

133 

134 def read(fileobj): 

135 """ Reads an instance from file. 

136 

137 Parameters 

138 ---------- 

139 fileobj : file-like object 

140 input file to read from 

141 """ 

142 data = pickle.load(fileobj) 

143 return Cutoffs(data) 

144 

145 def to_filename_tag(self): 

146 """ Simple function turning cutoffs into a string to be used in e.g. 

147 filenames. """ 

148 s = [] 

149 for i, c in enumerate(self._cutoff_matrix.tolist(), start=2): 

150 s.append('{}body-{}'.format(i, '_'.join(map(str, c)))) 

151 return '_'.join(s) 

152 

153 def __str__(self): 

154 cutoff_matrix = self._cutoff_matrix.copy() 

155 cutoff_matrix = np.vstack(([[None] * len(self.orders)], cutoff_matrix)) 

156 s = table_array_to_string(cutoff_matrix) 

157 

158 width = max(len(c) for c in s.split('\n')) 

159 header = ' Cutoffs '.center(width, '=') + '\n' 

160 bottom = '\n' + ''.center(width, '=') 

161 s = header + s + bottom 

162 return s 

163 

164 def __repr__(self): 

165 return 'Cutoffs({!r})'.format(self._cutoff_matrix) 

166 

167 

168class CutoffMaximumBody(Cutoffs): 

169 """ Specify cutoff-list plus maximum body 

170 

171 Usefull when creating e.g. 6th order expansions but with only 3-body 

172 interactions. 

173 

174 Parameters 

175 ---------- 

176 cutoff_list : list 

177 list of cutoffs for order 2, 3, etc. Must be in decresing order 

178 max_nbody : int 

179 No clusters containing more than max_nbody atoms will be generated 

180 """ 

181 

182 def __init__(self, cutoff_list, max_nbody): 

183 cutoff_matrix = np.zeros((max_nbody - 1, len(cutoff_list))) 

184 for order, cutoff in enumerate(cutoff_list, start=2): 

185 cutoff_matrix[:, order - 2] = cutoff 

186 super().__init__(cutoff_matrix) 

187 

188 

189def is_cutoff_allowed(atoms, cutoff): 

190 """ Checks if atoms is compatible with cutoff 

191 

192 Parameters 

193 ---------- 

194 atoms : ase.Atoms 

195 structure used for checking compatibility with cutoff 

196 cutoff : float 

197 cutoff to be tested 

198 

199 Returns 

200 ------- 

201 bool 

202 True if cutoff compatible with atoms object, else False 

203 """ 

204 nbrlist = NeighborList(cutoffs=[cutoff / 2] * len(atoms), skin=0, 

205 self_interaction=False, bothways=True) 

206 nbrlist.update(atoms) 

207 

208 for i in range(len(atoms)): 

209 neighbors, _ = nbrlist.get_neighbors(i) 

210 if i in neighbors: 

211 return False 

212 if len(neighbors) != len(set(neighbors)): 

213 return False 

214 return True 

215 

216 

217def estimate_maximum_cutoff(atoms, max_iter=11): 

218 """ Estimates the maximum possible cutoff given the atoms object 

219 

220 Parameters 

221 ---------- 

222 atoms : ase.Atoms 

223 structure used for checking compatibility with cutoff 

224 max_iter : int 

225 number of iterations in binary search 

226 """ 

227 

228 # First upper boundary of cutoff 

229 upper_cutoff = min(np.linalg.norm(atoms.cell, axis=1)) 

230 

231 # generate all possible offsets given upper_cutoff 

232 nbrlist = NeighborList(cutoffs=[upper_cutoff / 2] * len(atoms), skin=0, 

233 self_interaction=False, bothways=True) 

234 nbrlist.update(atoms) 

235 all_offsets = [] 

236 for i in range(len(atoms)): 

237 _, offsets = nbrlist.get_neighbors(i) 

238 all_offsets.extend([tuple(offset) for offset in offsets]) 

239 

240 # find lower boundary and new upper boundary 

241 unique_offsets = set(all_offsets) 

242 unique_offsets.discard((0, 0, 0)) 

243 upper = min(np.linalg.norm(np.dot(offset, atoms.cell)) 

244 for offset in unique_offsets) 

245 lower = upper / 2.0 

246 

247 # run binary search between the upper and lower bounds 

248 for _ in range(max_iter): 

249 cutoff = (upper + lower) / 2 

250 if is_cutoff_allowed(atoms, cutoff): 

251 lower = cutoff 

252 else: 

253 upper = cutoff 

254 return lower 

255 

256 

257class BaseClusterFilter: 

258 """Base cluster filter class. 

259 

260 This filter simply accepts all proposed clusters. A proper 

261 subclass must implement the same methods. 

262 """ 

263 def setup(self, atoms): 

264 """ The filter is passed the environment of the primitive cell. 

265 

266 Parameters 

267 ---------- 

268 atoms : ase.Atoms 

269 non-pbc primitive cell plus neighboring atoms 

270 """ 

271 self._atoms = atoms 

272 

273 def __call__(self, cluster): 

274 """ Returns True or False when a cluster is proposed. 

275 

276 Parameters 

277 ---------- 

278 cluster : tuple(int) 

279 indices of proposed cluster referenced to the internal 

280 atoms object 

281 """ 

282 return True