Coverage for /builds/kinetik161/ase/ase/ga/particle_mutations.py: 53.82%

275 statements  

« prev     ^ index     » next       coverage.py v7.2.7, created at 2023-12-10 11:04 +0000

1from operator import itemgetter 

2 

3import numpy as np 

4 

5from ase import Atoms 

6from ase.ga.offspring_creator import OffspringCreator 

7from ase.ga.utilities import get_distance_matrix, get_nndist 

8 

9 

10class Mutation(OffspringCreator): 

11 """Base class for all particle mutation type operators. 

12 Do not call this class directly.""" 

13 

14 def __init__(self, num_muts=1, rng=np.random): 

15 OffspringCreator.__init__(self, num_muts=num_muts, rng=rng) 

16 self.descriptor = 'Mutation' 

17 self.min_inputs = 1 

18 

19 @classmethod 

20 def get_atomic_configuration(cls, atoms, elements=None, eps=4e-2): 

21 """Returns the atomic configuration of the particle as a list of 

22 lists. Each list contain the indices of the atoms sitting at the 

23 same distance from the geometrical center of the particle. Highly 

24 symmetrical particles will often have many atoms in each shell. 

25 

26 For further elaboration see: 

27 J. Montejano-Carrizales and J. Moran-Lopez, Geometrical 

28 characteristics of compact nanoclusters, Nanostruct. Mater., 1, 

29 5, 397-409 (1992) 

30 

31 Parameters: 

32 

33 elements: Only take into account the elements specified in this 

34 list. Default is to take all elements into account. 

35 

36 eps: The distance allowed to separate elements within each shell.""" 

37 atoms = atoms.copy() 

38 if elements is None: 

39 e = list(set(atoms.get_chemical_symbols())) 

40 else: 

41 e = elements 

42 atoms.set_constraint() 

43 atoms.center() 

44 geo_mid = np.array([(atoms.get_cell() / 2.)[i][i] for i in range(3)]) 

45 dists = [(np.linalg.norm(geo_mid - atoms[i].position), i) 

46 for i in range(len(atoms))] 

47 dists.sort(key=itemgetter(0)) 

48 atomic_conf = [] 

49 old_dist = -10. 

50 for dist, i in dists: 

51 if abs(dist - old_dist) > eps: 

52 atomic_conf.append([i]) 

53 else: 

54 atomic_conf[-1].append(i) 

55 old_dist = dist 

56 sorted_elems = sorted(set(atoms.get_chemical_symbols())) 

57 if e is not None and sorted(e) != sorted_elems: 

58 for shell in atomic_conf: 

59 torem = [] 

60 for i in shell: 

61 if atoms[i].symbol not in e: 

62 torem.append(i) 

63 for i in torem: 

64 shell.remove(i) 

65 return atomic_conf 

66 

67 @classmethod 

68 def get_list_of_possible_permutations(cls, atoms, l1, l2): 

69 """Returns a list of available permutations from the two 

70 lists of indices, l1 and l2. Checking that identical elements 

71 are not permuted.""" 

72 possible_permutations = [] 

73 for i in l1: 

74 for j in l2: 

75 if atoms[int(i)].symbol != atoms[int(j)].symbol: 

76 possible_permutations.append((i, j)) 

77 return possible_permutations 

78 

79 

80class RandomMutation(Mutation): 

81 """Moves a random atom the supplied length in a random direction.""" 

82 

83 def __init__(self, length=2., num_muts=1, rng=np.random): 

84 Mutation.__init__(self, num_muts=num_muts, rng=rng) 

85 self.descriptor = 'RandomMutation' 

86 self.length = length 

87 

88 def mutate(self, atoms): 

89 """ Does the actual mutation. """ 

90 tbm = self.rng.choice(range(len(atoms))) 

91 

92 indi = Atoms() 

93 for a in atoms: 

94 if a.index == tbm: 

95 a.position += self.random_vector(self.length, rng=self.rng) 

96 indi.append(a) 

97 return indi 

98 

99 def get_new_individual(self, parents): 

100 f = parents[0] 

101 

102 indi = self.initialize_individual(f) 

103 indi.info['data']['parents'] = [f.info['confid']] 

104 

105 to_mut = f.copy() 

106 for _ in range(self.num_muts): 

107 to_mut = self.mutate(to_mut) 

108 

109 for atom in to_mut: 

110 indi.append(atom) 

111 

112 return (self.finalize_individual(indi), 

113 self.descriptor + ':Parent {}'.format(f.info['confid'])) 

114 

115 @classmethod 

116 def random_vector(cls, length, rng=np.random): 

117 """return random vector of certain length""" 

118 vec = np.array([rng.random() * 2 - 1 for i in range(3)]) 

119 vl = np.linalg.norm(vec) 

120 return np.array([v * length / vl for v in vec]) 

121 

122 

123class RandomPermutation(Mutation): 

124 """Permutes two random atoms. 

125 

126 Parameters: 

127 

128 num_muts: the number of times to perform this operation. 

129 

130 rng: Random number generator 

131 By default numpy.random. 

132 """ 

133 

134 def __init__(self, elements=None, num_muts=1, rng=np.random): 

135 Mutation.__init__(self, num_muts=num_muts, rng=rng) 

136 self.descriptor = 'RandomPermutation' 

137 self.elements = elements 

138 

139 def get_new_individual(self, parents): 

140 f = parents[0].copy() 

141 

142 diffatoms = len(set(f.numbers)) 

143 assert diffatoms > 1, 'Permutations with one atomic type is not valid' 

144 

145 indi = self.initialize_individual(f) 

146 indi.info['data']['parents'] = [f.info['confid']] 

147 

148 for _ in range(self.num_muts): 

149 RandomPermutation.mutate(f, self.elements, rng=self.rng) 

150 

151 for atom in f: 

152 indi.append(atom) 

153 

154 return (self.finalize_individual(indi), 

155 self.descriptor + ':Parent {}'.format(f.info['confid'])) 

156 

157 @classmethod 

158 def mutate(cls, atoms, elements=None, rng=np.random): 

159 """Do the actual permutation.""" 

160 if elements is None: 

161 indices = range(len(atoms)) 

162 else: 

163 indices = [a.index for a in atoms if a.symbol in elements] 

164 i1 = rng.choice(indices) 

165 i2 = rng.choice(indices) 

166 while atoms[i1].symbol == atoms[i2].symbol: 

167 i2 = rng.choice(indices) 

168 atoms.symbols[[i1, i2]] = atoms.symbols[[i2, i1]] 

169 

170 

171class COM2surfPermutation(Mutation): 

172 """The Center Of Mass to surface (COM2surf) permutation operator 

173 described in 

174 S. Lysgaard et al., Top. Catal., 2014, 57 (1-4), pp 33-39 

175 

176 Parameters: 

177 

178 elements: which elements should be included in this permutation, 

179 for example: include all metals and exclude all adsorbates 

180 

181 min_ratio: minimum ratio of each element in the core or surface region. 

182 If elements=[a, b] then ratio of a is Na / (Na + Nb) (N: Number of). 

183 If less than minimum ratio is present in the core, the region defining 

184 the core will be extended until the minimum ratio is met, and vice 

185 versa for the surface region. It has the potential reach the 

186 recursive limit if an element has a smaller total ratio in the 

187 complete particle. In that case remember to decrease this min_ratio. 

188 

189 num_muts: the number of times to perform this operation. 

190 

191 rng: Random number generator 

192 By default numpy.random. 

193 """ 

194 

195 def __init__(self, elements=None, min_ratio=0.25, num_muts=1, 

196 rng=np.random): 

197 Mutation.__init__(self, num_muts=num_muts, rng=rng) 

198 self.descriptor = 'COM2surfPermutation' 

199 self.min_ratio = min_ratio 

200 self.elements = elements 

201 

202 def get_new_individual(self, parents): 

203 f = parents[0].copy() 

204 

205 diffatoms = len(set(f.numbers)) 

206 assert diffatoms > 1, 'Permutations with one atomic type is not valid' 

207 

208 indi = self.initialize_individual(f) 

209 indi.info['data']['parents'] = [f.info['confid']] 

210 

211 for _ in range(self.num_muts): 

212 elems = self.elements 

213 COM2surfPermutation.mutate(f, elems, self.min_ratio, rng=self.rng) 

214 

215 for atom in f: 

216 indi.append(atom) 

217 

218 return (self.finalize_individual(indi), 

219 self.descriptor + ':Parent {}'.format(f.info['confid'])) 

220 

221 @classmethod 

222 def mutate(cls, atoms, elements, min_ratio, rng=np.random): 

223 """Performs the COM2surf permutation.""" 

224 ac = atoms.copy() 

225 if elements is not None: 

226 del ac[[a.index for a in ac if a.symbol not in elements]] 

227 syms = ac.get_chemical_symbols() 

228 for el in set(syms): 

229 assert syms.count(el) / float(len(syms)) > min_ratio 

230 

231 atomic_conf = Mutation.get_atomic_configuration(atoms, 

232 elements=elements) 

233 core = COM2surfPermutation.get_core_indices(atoms, 

234 atomic_conf, 

235 min_ratio) 

236 shell = COM2surfPermutation.get_shell_indices(atoms, 

237 atomic_conf, 

238 min_ratio) 

239 permuts = Mutation.get_list_of_possible_permutations(atoms, 

240 core, 

241 shell) 

242 chosen = rng.randint(len(permuts)) 

243 swap = list(permuts[chosen]) 

244 atoms.symbols[swap] = atoms.symbols[swap[::-1]] 

245 

246 @classmethod 

247 def get_core_indices(cls, atoms, atomic_conf, min_ratio, recurs=0): 

248 """Recursive function that returns the indices in the core subject to 

249 the min_ratio constraint. The indices are found from the supplied 

250 atomic configuration.""" 

251 elements = list({atoms[i].symbol 

252 for subl in atomic_conf for i in subl}) 

253 

254 core = [i for subl in atomic_conf[:1 + recurs] for i in subl] 

255 while len(core) < 1: 

256 recurs += 1 

257 core = [i for subl in atomic_conf[:1 + recurs] for i in subl] 

258 

259 for elem in elements: 

260 ratio = len([i for i in core 

261 if atoms[i].symbol == elem]) / float(len(core)) 

262 if ratio < min_ratio: 

263 return COM2surfPermutation.get_core_indices(atoms, 

264 atomic_conf, 

265 min_ratio, 

266 recurs + 1) 

267 return core 

268 

269 @classmethod 

270 def get_shell_indices(cls, atoms, atomic_conf, min_ratio, recurs=0): 

271 """Recursive function that returns the indices in the surface 

272 subject to the min_ratio constraint. The indices are found from 

273 the supplied atomic configuration.""" 

274 elements = list({atoms[i].symbol 

275 for subl in atomic_conf for i in subl}) 

276 

277 shell = [i for subl in atomic_conf[-1 - recurs:] for i in subl] 

278 while len(shell) < 1: 

279 recurs += 1 

280 shell = [i for subl in atomic_conf[-1 - recurs:] for i in subl] 

281 

282 for elem in elements: 

283 ratio = len([i for i in shell 

284 if atoms[i].symbol == elem]) / float(len(shell)) 

285 if ratio < min_ratio: 

286 return COM2surfPermutation.get_shell_indices(atoms, 

287 atomic_conf, 

288 min_ratio, 

289 recurs + 1) 

290 return shell 

291 

292 

293class _NeighborhoodPermutation(Mutation): 

294 """Helper class that holds common functions to all permutations 

295 that look at the neighborhoods of each atoms.""" 

296 @classmethod 

297 def get_possible_poor2rich_permutations(cls, atoms, inverse=False, 

298 recurs=0, distance_matrix=None): 

299 dm = distance_matrix 

300 if dm is None: 

301 dm = get_distance_matrix(atoms) 

302 # Adding a small value (0.2) to overcome slight variations 

303 # in the average bond length 

304 nndist = get_nndist(atoms, dm) + 0.2 

305 same_neighbors = {} 

306 

307 def f(x): 

308 return x[1] 

309 for i, atom in enumerate(atoms): 

310 same_neighbors[i] = 0 

311 neighbors = [j for j in range(len(dm[i])) if dm[i][j] < nndist] 

312 for n in neighbors: 

313 if atoms[n].symbol == atom.symbol: 

314 same_neighbors[i] += 1 

315 sorted_same = sorted(same_neighbors.items(), key=f) 

316 if inverse: 

317 sorted_same.reverse() 

318 poor_indices = [j[0] for j in sorted_same 

319 if abs(j[1] - sorted_same[0][1]) <= recurs] 

320 rich_indices = [j[0] for j in sorted_same 

321 if abs(j[1] - sorted_same[-1][1]) <= recurs] 

322 permuts = Mutation.get_list_of_possible_permutations(atoms, 

323 poor_indices, 

324 rich_indices) 

325 

326 if len(permuts) == 0: 

327 _NP = _NeighborhoodPermutation 

328 return _NP.get_possible_poor2rich_permutations(atoms, inverse, 

329 recurs + 1, dm) 

330 return permuts 

331 

332 

333class Poor2richPermutation(_NeighborhoodPermutation): 

334 """The poor to rich (Poor2rich) permutation operator described in 

335 S. Lysgaard et al., Top. Catal., 2014, 57 (1-4), pp 33-39 

336 

337 Permutes two atoms from regions short of the same elements, to 

338 regions rich in the same elements. 

339 (Inverse of Rich2poorPermutation) 

340 

341 Parameters: 

342 

343 elements: Which elements to take into account in this permutation 

344 

345 rng: Random number generator 

346 By default numpy.random. 

347 """ 

348 

349 def __init__(self, elements=[], num_muts=1, rng=np.random): 

350 _NeighborhoodPermutation.__init__(self, num_muts=num_muts, rng=rng) 

351 self.descriptor = 'Poor2richPermutation' 

352 self.elements = elements 

353 

354 def get_new_individual(self, parents): 

355 f = parents[0].copy() 

356 

357 diffatoms = len(set(f.numbers)) 

358 assert diffatoms > 1, 'Permutations with one atomic type is not valid' 

359 

360 indi = self.initialize_individual(f) 

361 indi.info['data']['parents'] = [f.info['confid']] 

362 

363 for _ in range(self.num_muts): 

364 Poor2richPermutation.mutate(f, self.elements, rng=self.rng) 

365 

366 for atom in f: 

367 indi.append(atom) 

368 

369 return (self.finalize_individual(indi), 

370 self.descriptor + ':Parent {}'.format(f.info['confid'])) 

371 

372 @classmethod 

373 def mutate(cls, atoms, elements, rng=np.random): 

374 _NP = _NeighborhoodPermutation 

375 # indices = [a.index for a in atoms if a.symbol in elements] 

376 ac = atoms.copy() 

377 del ac[[atom.index for atom in ac 

378 if atom.symbol not in elements]] 

379 permuts = _NP.get_possible_poor2rich_permutations(ac) 

380 swap = list(rng.choice(permuts)) 

381 atoms.symbols[swap] = atoms.symbols[swap[::-1]] 

382 

383 

384class Rich2poorPermutation(_NeighborhoodPermutation): 

385 """ 

386 The rich to poor (Rich2poor) permutation operator described in 

387 S. Lysgaard et al., Top. Catal., 2014, 57 (1-4), pp 33-39 

388 

389 Permutes two atoms from regions rich in the same elements, to 

390 regions short of the same elements. 

391 (Inverse of Poor2richPermutation) 

392 

393 Parameters: 

394 

395 elements: Which elements to take into account in this permutation 

396 

397 rng: Random number generator 

398 By default numpy.random. 

399 """ 

400 

401 def __init__(self, elements=None, num_muts=1, rng=np.random): 

402 _NeighborhoodPermutation.__init__(self, num_muts=num_muts, rng=rng) 

403 self.descriptor = 'Rich2poorPermutation' 

404 self.elements = elements 

405 

406 def get_new_individual(self, parents): 

407 f = parents[0].copy() 

408 

409 diffatoms = len(set(f.numbers)) 

410 assert diffatoms > 1, 'Permutations with one atomic type is not valid' 

411 

412 indi = self.initialize_individual(f) 

413 indi.info['data']['parents'] = [f.info['confid']] 

414 

415 if self.elements is None: 

416 elems = list(set(f.get_chemical_symbols())) 

417 else: 

418 elems = self.elements 

419 for _ in range(self.num_muts): 

420 Rich2poorPermutation.mutate(f, elems, rng=self.rng) 

421 

422 for atom in f: 

423 indi.append(atom) 

424 

425 return (self.finalize_individual(indi), 

426 self.descriptor + ':Parent {}'.format(f.info['confid'])) 

427 

428 @classmethod 

429 def mutate(cls, atoms, elements, rng=np.random): 

430 _NP = _NeighborhoodPermutation 

431 ac = atoms.copy() 

432 del ac[[atom.index for atom in ac 

433 if atom.symbol not in elements]] 

434 permuts = _NP.get_possible_poor2rich_permutations(ac, 

435 inverse=True) 

436 swap = list(rng.choice(permuts)) 

437 atoms.symbols[swap] = atoms.symbols[swap[::-1]] 

438 

439 

440class SymmetricSubstitute(Mutation): 

441 """Permute all atoms within a subshell of the symmetric particle. 

442 The atoms within a subshell all have the same distance to the center, 

443 these are all equivalent under the particle point group symmetry. 

444 

445 """ 

446 

447 def __init__(self, elements=None, num_muts=1, rng=np.random): 

448 Mutation.__init__(self, num_muts=num_muts, rng=rng) 

449 self.descriptor = 'SymmetricSubstitute' 

450 self.elements = elements 

451 

452 def substitute(self, atoms): 

453 """Does the actual substitution""" 

454 atoms = atoms.copy() 

455 aconf = self.get_atomic_configuration(atoms, 

456 elements=self.elements) 

457 itbm = self.rng.randint(0, len(aconf) - 1) 

458 to_element = self.rng.choice(self.elements) 

459 

460 for i in aconf[itbm]: 

461 atoms[i].symbol = to_element 

462 

463 return atoms 

464 

465 def get_new_individual(self, parents): 

466 f = parents[0] 

467 

468 indi = self.substitute(f) 

469 indi = self.initialize_individual(f, indi) 

470 indi.info['data']['parents'] = [f.info['confid']] 

471 

472 return (self.finalize_individual(indi), 

473 self.descriptor + ':Parent {}'.format(f.info['confid'])) 

474 

475 

476class RandomSubstitute(Mutation): 

477 """Substitutes one atom with another atom type. The possible atom types 

478 are supplied in the parameter elements""" 

479 

480 def __init__(self, elements=None, num_muts=1, rng=np.random): 

481 Mutation.__init__(self, num_muts=num_muts, rng=rng) 

482 self.descriptor = 'RandomSubstitute' 

483 self.elements = elements 

484 

485 def substitute(self, atoms): 

486 """Does the actual substitution""" 

487 atoms = atoms.copy() 

488 if self.elements is None: 

489 elems = list(set(atoms.get_chemical_symbols())) 

490 else: 

491 elems = self.elements[:] 

492 possible_indices = [a.index for a in atoms 

493 if a.symbol in elems] 

494 itbm = self.rng.choice(possible_indices) 

495 elems.remove(atoms[itbm].symbol) 

496 new_symbol = self.rng.choice(elems) 

497 atoms[itbm].symbol = new_symbol 

498 

499 return atoms 

500 

501 def get_new_individual(self, parents): 

502 f = parents[0] 

503 

504 indi = self.substitute(f) 

505 indi = self.initialize_individual(f, indi) 

506 indi.info['data']['parents'] = [f.info['confid']] 

507 

508 return (self.finalize_individual(indi), 

509 self.descriptor + ':Parent {}'.format(f.info['confid']))