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

1from collections import defaultdict 

2 

3import numpy as np 

4import kimpy 

5from kimpy import neighlist 

6from ase.neighborlist import neighbor_list 

7from ase import Atom 

8 

9from .kimpy_wrappers import check_call_wrapper 

10 

11 

12class NeighborList: 

13 

14 kimpy_arrays = { 

15 "num_particles": np.intc, 

16 "coords": np.double, 

17 "particle_contributing": np.intc, 

18 "species_code": np.intc, 

19 "cutoffs": np.double, 

20 "padding_image_of": np.intc, 

21 "need_neigh": np.intc, 

22 } 

23 

24 def __setattr__(self, name, value): 

25 """ 

26 Override assignment to any of the attributes listed in 

27 kimpy_arrays to automatically cast the object to a numpy array. 

28 This is done to avoid a ton of explicit numpy.array() calls (and 

29 the possibility that we forget to do the cast). It is important 

30 to use np.asarray() here instead of np.array() because using the 

31 latter will mean that incrementation (+=) will create a new 

32 object that the reference is bound to, which becomes a problem 

33 if update_compute_args isn't called to reregister the 

34 corresponding address with the KIM API. 

35 """ 

36 if name in self.kimpy_arrays and value is not None: 

37 value = np.asarray(value, dtype=self.kimpy_arrays[name]) 

38 self.__dict__[name] = value 

39 

40 def __init__( 

41 self, 

42 neigh_skin_ratio, 

43 model_influence_dist, 

44 model_cutoffs, 

45 padding_not_require_neigh, 

46 debug, 

47 ): 

48 

49 self.skin = neigh_skin_ratio * model_influence_dist 

50 self.influence_dist = model_influence_dist + self.skin 

51 self.cutoffs = model_cutoffs + self.skin 

52 self.padding_need_neigh = not padding_not_require_neigh.all() 

53 self.debug = debug 

54 

55 if self.debug: 

56 print() 

57 print("Calculator skin: {}".format(self.skin)) 

58 print(f"Model influence distance: {model_influence_dist}") 

59 print( 

60 "Calculator influence distance (including skin distance): {}" 

61 "".format(self.influence_dist) 

62 ) 

63 print("Number of cutoffs: {}".format(model_cutoffs.size)) 

64 print("Model cutoffs: {}".format(model_cutoffs)) 

65 print( 

66 "Calculator cutoffs (including skin distance): {}" 

67 "".format(self.cutoffs) 

68 ) 

69 print( 

70 "Model needs neighbors of padding atoms: {}" 

71 "".format(self.padding_need_neigh) 

72 ) 

73 print() 

74 

75 # Attributes to be set by subclasses 

76 self.neigh = None 

77 self.num_contributing_particles = None 

78 self.padding_image_of = None 

79 self.num_particles = None 

80 self.coords = None 

81 self.particle_contributing = None 

82 self.species_code = None 

83 self.need_neigh = None 

84 self.last_update_positions = None 

85 

86 def update_kim_coords(self, atoms): 

87 """Update atomic positions in self.coords, which is where the KIM 

88 API will look to find them in order to pass them to the model. 

89 """ 

90 if self.padding_image_of.size != 0: 

91 disp_contrib = atoms.positions - self.coords[: len(atoms)] 

92 disp_pad = disp_contrib[self.padding_image_of] 

93 self.coords += np.concatenate((disp_contrib, disp_pad)) 

94 else: 

95 np.copyto(self.coords, atoms.positions) 

96 

97 if self.debug: 

98 print("Debug: called update_kim_coords") 

99 print() 

100 

101 def need_neigh_update(self, atoms, system_changes): 

102 need_neigh_update = True 

103 if len(system_changes) == 1 and "positions" in system_changes: 

104 # Only position changes 

105 if self.last_update_positions is not None: 

106 a = self.last_update_positions 

107 b = atoms.positions 

108 if a.shape == b.shape: 

109 delta = np.linalg.norm(a - b, axis=1) 

110 # Indices of the two largest elements 

111 ind = np.argpartition(delta, -2)[-2:] 

112 if sum(delta[ind]) <= self.skin: 

113 need_neigh_update = False 

114 

115 return need_neigh_update 

116 

117 

118class ASENeighborList(NeighborList): 

119 def __init__( 

120 self, 

121 compute_args, 

122 neigh_skin_ratio, 

123 model_influence_dist, 

124 model_cutoffs, 

125 padding_not_require_neigh, 

126 debug, 

127 ): 

128 super().__init__( 

129 neigh_skin_ratio, 

130 model_influence_dist, 

131 model_cutoffs, 

132 padding_not_require_neigh, 

133 debug, 

134 ) 

135 

136 self.neigh = {} 

137 compute_args.set_callback( 

138 kimpy.compute_callback_name.GetNeighborList, 

139 self.get_neigh, 

140 self.neigh) 

141 

142 @staticmethod 

143 def get_neigh(data, cutoffs, neighbor_list_index, particle_number): 

144 """Retrieves the neighbors of each atom using ASE's native neighbor 

145 list library 

146 """ 

147 # We can only return neighbors of particles that were stored 

148 number_of_particles = data["num_particles"] 

149 if particle_number >= number_of_particles or particle_number < 0: 

150 return (np.array([]), 1) 

151 

152 neighbors = data["neighbors"][neighbor_list_index][particle_number] 

153 return (neighbors, 0) 

154 

155 def build(self, orig_atoms): 

156 """Build the ASE neighbor list and return an Atoms object with 

157 all of the neighbors added. First a neighbor list is created 

158 from ase.neighbor_list, having only information about the 

159 neighbors of the original atoms. If neighbors of padding atoms 

160 are required, they are calculated using information from the 

161 first neighbor list. 

162 """ 

163 syms = orig_atoms.get_chemical_symbols() 

164 orig_num_atoms = len(orig_atoms) 

165 orig_pos = orig_atoms.get_positions() 

166 

167 # New atoms object that will contain the contributing atoms plus 

168 # the padding atoms 

169 new_atoms = orig_atoms.copy() 

170 

171 neigh_list = defaultdict(list) 

172 neigh_dists = defaultdict(list) 

173 

174 # Information for padding atoms 

175 padding_image_of = [] 

176 padding_shifts = [] 

177 

178 # Ask ASE to build the neighbor list using the influence distance. 

179 # This is not a neighbor list for each atom, but rather a listing 

180 # of all neighbor pairs that exist. Atoms with no neighbors will 

181 # not show up. 

182 (neigh_indices_i, 

183 neigh_indices_j, 

184 relative_pos, 

185 neigh_cell_offsets, 

186 dists) = neighbor_list( 

187 "ijDSd", orig_atoms, self.influence_dist 

188 ) 

189 

190 # Loop over all neighbor pairs. Because this loop will generally 

191 # include image atoms (for periodic systems), we keep track of 

192 # which atoms/images we've accounted for in the `used` dictionary. 

193 used = dict() 

194 for neigh_i, neigh_j, rel_pos, offset, dist in zip(neigh_indices_i, 

195 neigh_indices_j, 

196 relative_pos, 

197 neigh_cell_offsets, 

198 dists): 

199 # Get neighbor position of neighbor 

200 # (mapped back into unit cell, so this may overlap with other atoms) 

201 wrapped_pos = orig_pos[neigh_i] + rel_pos 

202 

203 shift = tuple(offset) 

204 uniq_index = (neigh_j,) + shift 

205 if shift == (0, 0, 0): 

206 # This atom is in the unit cell, i.e. it is contributing 

207 neigh_list[neigh_i].append(neigh_j) 

208 neigh_dists[neigh_i].append(dist) 

209 if uniq_index not in used: 

210 used[uniq_index] = neigh_j 

211 else: 

212 # This atom is not in the unit cell, i.e. it is padding 

213 if uniq_index not in used: 

214 # Add the neighbor as a padding atom 

215 used[uniq_index] = len(new_atoms) 

216 new_atoms.append(Atom(syms[neigh_j], position=wrapped_pos)) 

217 padding_image_of.append(neigh_j) 

218 padding_shifts.append(offset) 

219 neigh_list[neigh_i].append(used[uniq_index]) 

220 neigh_dists[neigh_i].append(dist) 

221 neighbor_list_size = orig_num_atoms 

222 

223 # Add neighbors of padding atoms if the potential requires them 

224 if self.padding_need_neigh: 

225 neighbor_list_size = len(new_atoms) 

226 inv_used = dict((v, k) for k, v in used.items()) 

227 # Loop over all the neighbors (k) and the image of that neighbor 

228 # in the cell (neigh) 

229 for k, neigh in enumerate(padding_image_of): 

230 # Shift from original atom in cell to neighbor 

231 shift = padding_shifts[k] 

232 for orig_neigh, orig_dist in zip(neigh_list[neigh], 

233 neigh_dists[neigh]): 

234 # Get the shift of the neighbor of the original atom 

235 orig_shift = inv_used[orig_neigh][1:] 

236 

237 # Apply sum of original shift and current shift to 

238 # neighbors of original atom 

239 total_shift = orig_shift + shift 

240 

241 # Get the image in the cell of the original neighbor 

242 if orig_neigh <= orig_num_atoms - 1: 

243 orig_neigh_image = orig_neigh 

244 else: 

245 orig_neigh_image = padding_image_of[orig_neigh - 

246 orig_num_atoms] 

247 

248 # If the original image with the total shift has been 

249 # used before then it is also a neighbor of this atom 

250 uniq_index = (orig_neigh_image,) + tuple(total_shift) 

251 if uniq_index in used: 

252 neigh_list[k + orig_num_atoms].append(used[uniq_index]) 

253 neigh_dists[k + orig_num_atoms].append(orig_dist) 

254 

255 # If the model has multiple cutoffs, we need to return neighbor lists 

256 # corresponding to each of them 

257 neigh_lists = [] 

258 for cut in self.cutoffs: 

259 neigh_list = [ 

260 np.array(neigh_list[k], dtype=np.intc)[neigh_dists[k] <= cut] 

261 for k in range(neighbor_list_size) 

262 ] 

263 neigh_lists.append(neigh_list) 

264 

265 self.padding_image_of = padding_image_of 

266 

267 self.neigh["neighbors"] = neigh_lists 

268 self.neigh["num_particles"] = neighbor_list_size 

269 

270 return new_atoms 

271 

272 def update(self, orig_atoms, species_map): 

273 """Create the neighbor list along with the other required 

274 parameters (which are stored as instance attributes). The 

275 required parameters are: 

276 

277 - num_particles 

278 - coords 

279 - particle_contributing 

280 - species_code 

281 

282 Note that the KIM API requires a neighbor list that has indices 

283 corresponding to each atom. 

284 """ 

285 

286 # Information of original atoms 

287 self.num_contributing_particles = len(orig_atoms) 

288 

289 new_atoms = self.build(orig_atoms) 

290 

291 # Save the number of atoms and all their neighbors and positions 

292 num_atoms = len(new_atoms) 

293 num_padding = num_atoms - self.num_contributing_particles 

294 self.num_particles = [num_atoms] 

295 self.coords = new_atoms.get_positions() 

296 

297 # Save which coordinates are from original atoms and which are from 

298 # neighbors using a mask 

299 indices_mask = [1] * self.num_contributing_particles + [0] * num_padding 

300 self.particle_contributing = indices_mask 

301 

302 # Species support and code 

303 try: 

304 self.species_code = [ 

305 species_map[s] for s in new_atoms.get_chemical_symbols() 

306 ] 

307 except KeyError as e: 

308 raise RuntimeError("Species not supported by KIM model; {}" 

309 .format(str(e))) 

310 

311 self.last_update_positions = orig_atoms.get_positions() 

312 

313 if self.debug: 

314 print("Debug: called update_ase_neigh") 

315 print() 

316 

317 

318class KimpyNeighborList(NeighborList): 

319 def __init__( 

320 self, 

321 compute_args, 

322 neigh_skin_ratio, 

323 model_influence_dist, 

324 model_cutoffs, 

325 padding_not_require_neigh, 

326 debug, 

327 ): 

328 super().__init__( 

329 neigh_skin_ratio, 

330 model_influence_dist, 

331 model_cutoffs, 

332 padding_not_require_neigh, 

333 debug, 

334 ) 

335 

336 self.neigh = neighlist.NeighList() 

337 

338 compute_args.set_callback_pointer( 

339 kimpy.compute_callback_name.GetNeighborList, 

340 neighlist.get_neigh_kim(), 

341 self.neigh, 

342 ) 

343 

344 @check_call_wrapper 

345 def build(self): 

346 return self.neigh.build( 

347 self.coords, self.influence_dist, self.cutoffs, self.need_neigh 

348 ) 

349 

350 @check_call_wrapper 

351 def create_paddings( 

352 self, cell, pbc, contributing_coords, contributing_species_code 

353 ): 

354 # Cast things passed through kimpy to numpy arrays 

355 cell = np.asarray(cell, dtype=np.double) 

356 pbc = np.asarray(pbc, dtype=np.intc) 

357 contributing_coords = np.asarray(contributing_coords, dtype=np.double) 

358 

359 return neighlist.create_paddings( 

360 self.influence_dist, 

361 cell, 

362 pbc, 

363 contributing_coords, 

364 contributing_species_code, 

365 ) 

366 

367 def update(self, atoms, species_map): 

368 """Create the neighbor list along with the other required 

369 parameters (which are stored as instance attributes). The 

370 required parameters are: 

371 

372 - num_particles 

373 - coords 

374 - particle_contributing 

375 - species_code 

376 

377 Note that the KIM API requires a neighbor list that has indices 

378 corresponding to each atom. 

379 """ 

380 

381 # Get info from Atoms object 

382 cell = np.asarray(atoms.get_cell(), dtype=np.double) 

383 pbc = np.asarray(atoms.get_pbc(), dtype=np.intc) 

384 contributing_coords = np.asarray(atoms.get_positions(), dtype=np.double) 

385 self.num_contributing_particles = atoms.get_global_number_of_atoms() 

386 num_contributing = self.num_contributing_particles 

387 

388 # Species support and code 

389 try: 

390 contributing_species_code = np.array( 

391 [species_map[s] for s in atoms.get_chemical_symbols()], 

392 dtype=np.intc) 

393 except KeyError as e: 

394 raise RuntimeError("Species not supported by KIM model; {}" 

395 .format(str(e))) 

396 

397 if pbc.any(): # Need padding atoms 

398 # Create padding atoms 

399 

400 padding_coords, padding_species_code, self.padding_image_of = \ 

401 self.create_paddings( 

402 cell, pbc, contributing_coords, contributing_species_code) 

403 num_padding = padding_species_code.size 

404 

405 self.num_particles = [num_contributing + num_padding] 

406 self.coords = np.concatenate((contributing_coords, padding_coords)) 

407 self.species_code = np.concatenate( 

408 (contributing_species_code, padding_species_code) 

409 ) 

410 self.particle_contributing = ([1] * num_contributing + 

411 [0] * num_padding) 

412 self.need_neigh = [1] * self.num_particles[0] 

413 if not self.padding_need_neigh: 

414 self.need_neigh[num_contributing:] = 0 

415 

416 else: # Do not need padding atoms 

417 self.padding_image_of = [] 

418 self.num_particles = [num_contributing] 

419 self.coords = contributing_coords 

420 self.species_code = contributing_species_code 

421 self.particle_contributing = [1] * num_contributing 

422 self.need_neigh = self.particle_contributing 

423 

424 # Create neighborlist 

425 self.build() 

426 

427 self.last_update_positions = atoms.get_positions() 

428 

429 if self.debug: 

430 print("Debug: called update_kimpy_neigh") 

431 print()