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

1""" 

2ASE Calculator for interatomic models compatible with the Knowledgebase 

3of Interatomic Models (KIM) application programming interface (API). 

4Written by: 

5 

6Mingjian Wen 

7Daniel S. Karls 

8University of Minnesota 

9""" 

10import numpy as np 

11 

12from ase.calculators.calculator import Calculator 

13from ase.calculators.calculator import compare_atoms 

14 

15from . import kimpy_wrappers 

16from . import neighborlist 

17 

18 

19class KIMModelData: 

20 """Initializes and subsequently stores the KIM API Portable Model 

21 object, KIM API ComputeArguments object, and the neighbor list 

22 object used by instances of KIMModelCalculator. Also stores the 

23 arrays which are registered in the KIM API and which are used to 

24 communicate with the model. 

25 """ 

26 

27 def __init__(self, model_name, ase_neigh, neigh_skin_ratio, debug=False): 

28 self.model_name = model_name 

29 self.ase_neigh = ase_neigh 

30 self.debug = debug 

31 

32 # Initialize KIM API Portable Model object and ComputeArguments object 

33 self.kim_model, self.compute_args = self._init_kim() 

34 

35 self.species_map = self._create_species_map() 

36 

37 # Ask model to provide information relevant for neighbor list 

38 # construction 

39 ( 

40 model_influence_dist, 

41 model_cutoffs, 

42 padding_not_require_neigh, 

43 ) = self.get_model_neighbor_list_parameters() 

44 

45 # Initialize neighbor list object 

46 self.neigh = self._init_neigh( 

47 neigh_skin_ratio, 

48 model_influence_dist, 

49 model_cutoffs, 

50 padding_not_require_neigh, 

51 ) 

52 

53 def _init_kim(self): 

54 """Create the KIM API Portable Model object and KIM API ComputeArguments 

55 object 

56 """ 

57 if self.kim_initialized: 

58 return 

59 

60 kim_model = kimpy_wrappers.PortableModel(self.model_name, self.debug) 

61 

62 # KIM API model object is what actually creates/destroys the 

63 # ComputeArguments object, so we must pass it as a parameter 

64 compute_args = kim_model.compute_arguments_create() 

65 

66 return kim_model, compute_args 

67 

68 def _init_neigh( 

69 self, 

70 neigh_skin_ratio, 

71 model_influence_dist, 

72 model_cutoffs, 

73 padding_not_require_neigh, 

74 ): 

75 """Initialize neighbor list, either an ASE-native neighborlist 

76 or one created using the neighlist module in kimpy 

77 """ 

78 neigh_list_object_type = ( 

79 neighborlist.ASENeighborList 

80 if self.ase_neigh 

81 else neighborlist.KimpyNeighborList 

82 ) 

83 return neigh_list_object_type( 

84 self.compute_args, 

85 neigh_skin_ratio, 

86 model_influence_dist, 

87 model_cutoffs, 

88 padding_not_require_neigh, 

89 self.debug, 

90 ) 

91 

92 def get_model_neighbor_list_parameters(self): 

93 model_influence_dist = self.kim_model.get_influence_distance() 

94 ( 

95 model_cutoffs, 

96 padding_not_require_neigh, 

97 ) = self.kim_model.get_neighbor_list_cutoffs_and_hints() 

98 

99 return model_influence_dist, model_cutoffs, padding_not_require_neigh 

100 

101 def update_compute_args_pointers(self, energy, forces): 

102 self.compute_args.update( 

103 self.num_particles, 

104 self.species_code, 

105 self._particle_contributing, 

106 self.coords, 

107 energy, 

108 forces, 

109 ) 

110 

111 def _create_species_map(self): 

112 """Get all the supported species of the KIM model and the 

113 corresponding integer codes used by the model 

114 

115 Returns 

116 ------- 

117 species_map : dict 

118 key : str 

119 chemical symbols (e.g. "Ar") 

120 value : int 

121 species integer code (e.g. 1) 

122 """ 

123 supported_species, codes = self._get_model_supported_species_and_codes() 

124 species_map = dict() 

125 for i, spec in enumerate(supported_species): 

126 species_map[spec] = codes[i] 

127 if self.debug: 

128 print( 

129 "Species {} is supported and its code is: {}".format( 

130 spec, codes[i]) 

131 ) 

132 

133 return species_map 

134 

135 @property 

136 def padding_image_of(self): 

137 return self.neigh.padding_image_of 

138 

139 @property 

140 def num_particles(self): 

141 return self.neigh.num_particles 

142 

143 @property 

144 def coords(self): 

145 return self.neigh.coords 

146 

147 @property 

148 def _particle_contributing(self): 

149 return self.neigh.particle_contributing 

150 

151 @property 

152 def species_code(self): 

153 return self.neigh.species_code 

154 

155 @property 

156 def kim_initialized(self): 

157 return hasattr(self, "kim_model") 

158 

159 @property 

160 def _neigh_initialized(self): 

161 return hasattr(self, "neigh") 

162 

163 @property 

164 def _get_model_supported_species_and_codes(self): 

165 return self.kim_model.get_model_supported_species_and_codes 

166 

167 

168class KIMModelCalculator(Calculator): 

169 """Calculator that works with KIM Portable Models (PMs). 

170 

171 Calculator that carries out direct communication between ASE and a 

172 KIM Portable Model (PM) through the kimpy library (which provides a 

173 set of python bindings to the KIM API). 

174 

175 Parameters 

176 ---------- 

177 model_name : str 

178 The unique identifier assigned to the interatomic model (for 

179 details, see https://openkim.org/doc/schema/kim-ids) 

180 

181 ase_neigh : bool, optional 

182 False (default): Use kimpy's neighbor list library 

183 

184 True: Use ASE's internal neighbor list mechanism (usually slower 

185 than the kimpy neighlist library) 

186 

187 neigh_skin_ratio : float, optional 

188 Used to determine the neighbor list cutoff distance, r_neigh, 

189 through the relation r_neigh = (1 + neigh_skin_ratio) * rcut, 

190 where rcut is the model's influence distance. (Default: 0.2) 

191 

192 release_GIL : bool, optional 

193 Whether to release python GIL. Releasing the GIL allows a KIM 

194 model to run with multiple concurrent threads. (Default: False) 

195 

196 debug : bool, optional 

197 If True, detailed information is printed to stdout. (Default: 

198 False) 

199 """ 

200 

201 implemented_properties = ["energy", "free_energy", "forces", "stress"] 

202 

203 ignored_changes = {"initial_charges", "initial_magmoms"} 

204 

205 def __init__( 

206 self, 

207 model_name, 

208 ase_neigh=False, 

209 neigh_skin_ratio=0.2, 

210 release_GIL=False, 

211 debug=False, 

212 *args, 

213 **kwargs 

214 ): 

215 super().__init__(*args, **kwargs) 

216 

217 self.model_name = model_name 

218 self.release_GIL = release_GIL 

219 self.debug = debug 

220 

221 if neigh_skin_ratio < 0: 

222 raise ValueError('Argument "neigh_skin_ratio" must be non-negative') 

223 self.neigh_skin_ratio = neigh_skin_ratio 

224 

225 # Model output 

226 self.energy = None 

227 self.forces = None 

228 

229 # Create KIMModelData object. This will take care of creating 

230 # and storing the KIM API Portable Model object, KIM API 

231 # ComputeArguments object, and the neighbor list object that 

232 # our calculator needs 

233 self._kimmodeldata = KIMModelData( 

234 self.model_name, ase_neigh, self.neigh_skin_ratio, self.debug 

235 ) 

236 

237 self._parameters_changed = False 

238 

239 def __enter__(self): 

240 return self 

241 

242 def __exit__(self, exc_type, value, traceback): 

243 pass 

244 

245 def __repr__(self): 

246 return "KIMModelCalculator(model_name={})".format(self.model_name) 

247 

248 def calculate( 

249 self, 

250 atoms=None, 

251 properties=["energy", "forces", "stress"], 

252 system_changes=["positions", "numbers", "cell", "pbc"], 

253 ): 

254 """ 

255 Inherited method from the ase Calculator class that is called by 

256 get_property() 

257 

258 Parameters 

259 ---------- 

260 atoms : Atoms 

261 Atoms object whose properties are desired 

262 

263 properties : list of str 

264 List of what needs to be calculated. Can be any combination 

265 of 'energy', 'forces' and 'stress'. 

266 

267 system_changes : list of str 

268 List of what has changed since last calculation. Can be any 

269 combination of these six: 'positions', 'numbers', 'cell', 

270 and 'pbc'. 

271 """ 

272 

273 super().calculate(atoms, properties, system_changes) 

274 

275 if self._parameters_changed: 

276 self._parameters_changed = False 

277 

278 if system_changes: 

279 

280 # Ask model to update all of its parameters and the parameters 

281 # related to the neighbor list(s). This update is necessary to do 

282 # here since the user will generally have made changes the model 

283 # parameters since the last time an update was performed and we 

284 # need to ensure that any properties calculated here are made using 

285 # the up-to-date model and neighbor list parameters. 

286 self._model_refresh_and_update_neighbor_list_parameters() 

287 

288 if self._need_neigh_update(atoms, system_changes): 

289 self._update_neigh(atoms, self._species_map) 

290 self.energy = np.array([0.0], dtype=kimpy_wrappers.c_double) 

291 self.forces = np.zeros( 

292 [self._num_particles[0], 3], dtype=kimpy_wrappers.c_double 

293 ) 

294 self._update_compute_args_pointers(self.energy, self.forces) 

295 else: 

296 self._update_kim_coords(atoms) 

297 

298 self._kim_model.compute(self._compute_args, self.release_GIL) 

299 

300 energy = self.energy[0] 

301 forces = self._assemble_padding_forces() 

302 

303 try: 

304 volume = atoms.get_volume() 

305 stress = self._compute_virial_stress( 

306 self.forces, self._coords, volume) 

307 except ValueError: # Volume cannot be computed 

308 stress = None 

309 

310 # Quantities passed back to ASE 

311 self.results["energy"] = energy 

312 self.results["free_energy"] = energy 

313 self.results["forces"] = forces 

314 self.results["stress"] = stress 

315 

316 def check_state(self, atoms, tol=1e-15): 

317 # Check for change in atomic configuration (positions or pbc) 

318 system_changes = compare_atoms( 

319 self.atoms, atoms, excluded_properties=self.ignored_changes 

320 ) 

321 

322 # Check if model parameters were changed 

323 if self._parameters_changed: 

324 system_changes.append("calculator") 

325 

326 return system_changes 

327 

328 def _assemble_padding_forces(self): 

329 """ 

330 Assemble forces on padding atoms back to contributing atoms. 

331 

332 Parameters 

333 ---------- 

334 forces : 2D array of doubles 

335 Forces on both contributing and padding atoms 

336 

337 num_contrib: int 

338 Number of contributing atoms 

339 

340 padding_image_of : 1D array of int 

341 Atom number, of which the padding atom is an image 

342 

343 

344 Returns 

345 ------- 

346 Total forces on contributing atoms. 

347 """ 

348 

349 total_forces = np.array(self.forces[:self._num_contributing_particles]) 

350 

351 if self._padding_image_of.size != 0: 

352 pad_forces = self.forces[self._num_contributing_particles:] 

353 for f, org_index in zip(pad_forces, self._padding_image_of): 

354 total_forces[org_index] += f 

355 

356 return total_forces 

357 

358 @staticmethod 

359 def _compute_virial_stress(forces, coords, volume): 

360 """Compute the virial stress in Voigt notation. 

361 

362 Parameters 

363 ---------- 

364 forces : 2D array 

365 Partial forces on all atoms (padding included) 

366 

367 coords : 2D array 

368 Coordinates of all atoms (padding included) 

369 

370 volume : float 

371 Volume of cell 

372 

373 Returns 

374 ------- 

375 stress : 1D array 

376 stress in Voigt order (xx, yy, zz, yz, xz, xy) 

377 """ 

378 stress = np.zeros(6) 

379 stress[0] = -np.dot(forces[:, 0], coords[:, 0]) / volume 

380 stress[1] = -np.dot(forces[:, 1], coords[:, 1]) / volume 

381 stress[2] = -np.dot(forces[:, 2], coords[:, 2]) / volume 

382 stress[3] = -np.dot(forces[:, 1], coords[:, 2]) / volume 

383 stress[4] = -np.dot(forces[:, 0], coords[:, 2]) / volume 

384 stress[5] = -np.dot(forces[:, 0], coords[:, 1]) / volume 

385 

386 return stress 

387 

388 @property 

389 def _update_compute_args_pointers(self): 

390 return self._kimmodeldata.update_compute_args_pointers 

391 

392 @property 

393 def _kim_model(self): 

394 return self._kimmodeldata.kim_model 

395 

396 @property 

397 def _compute_args(self): 

398 return self._kimmodeldata.compute_args 

399 

400 @property 

401 def _num_particles(self): 

402 return self._kimmodeldata.num_particles 

403 

404 @property 

405 def _coords(self): 

406 return self._kimmodeldata.coords 

407 

408 @property 

409 def _padding_image_of(self): 

410 return self._kimmodeldata.padding_image_of 

411 

412 @property 

413 def _species_map(self): 

414 return self._kimmodeldata.species_map 

415 

416 @property 

417 def _neigh(self): 

418 # WARNING: This property is underscored for a reason! The 

419 # neighborlist(s) itself (themselves) may not be up to date with 

420 # respect to changes that have been made to the model's parameters, or 

421 # even since the positions in the Atoms object may have changed. 

422 # Neighbor lists are only potentially updated inside the ``calculate`` 

423 # method. 

424 return self._kimmodeldata.neigh 

425 

426 @property 

427 def _num_contributing_particles(self): 

428 return self._neigh.num_contributing_particles 

429 

430 @property 

431 def _update_kim_coords(self): 

432 return self._neigh.update_kim_coords 

433 

434 @property 

435 def _need_neigh_update(self): 

436 return self._neigh.need_neigh_update 

437 

438 @property 

439 def _update_neigh(self): 

440 return self._neigh.update 

441 

442 @property 

443 def parameters_metadata(self): 

444 return self._kim_model.parameters_metadata 

445 

446 @property 

447 def parameter_names(self): 

448 return self._kim_model.parameter_names 

449 

450 @property 

451 def get_parameters(self): 

452 # Ask model to update all of its parameters and the parameters related 

453 # to the neighbor list(s). This update is necessary to do here since 

454 # the user will generally have made changes the model parameters since 

455 # the last time an update was performed and we need to ensure the 

456 # parameters returned by this method are fully up to date. 

457 self._model_refresh_and_update_neighbor_list_parameters() 

458 

459 return self._kim_model.get_parameters 

460 

461 def set_parameters(self, **kwargs): 

462 parameters = self._kim_model.set_parameters(**kwargs) 

463 self._parameters_changed = True 

464 

465 return parameters 

466 

467 def _model_refresh_and_update_neighbor_list_parameters(self): 

468 """ 

469 Call the model's refresh routine and update the neighbor list object 

470 for any necessary changes arising from changes to the model parameters, 

471 e.g. a change in one of its cutoffs. After a model's parameters have 

472 been changed, this method *must* be called before calling the model's 

473 compute routine. 

474 """ 

475 self._kim_model.clear_then_refresh() 

476 

477 # Update neighbor list parameters 

478 ( 

479 model_influence_dist, 

480 model_cutoffs, 

481 padding_not_require_neigh, 

482 ) = self._kimmodeldata.get_model_neighbor_list_parameters() 

483 

484 self._neigh.set_neigh_parameters( 

485 self.neigh_skin_ratio, 

486 model_influence_dist, 

487 model_cutoffs, 

488 padding_not_require_neigh, 

489 )