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

1import numpy as np 

2from operator import itemgetter 

3 

4from ase.ga.offspring_creator import OffspringCreator 

5from ase.ga.utilities import get_distance_matrix, get_nndist 

6from ase import Atoms 

7 

8 

9class Mutation(OffspringCreator): 

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

11 Do not call this class directly.""" 

12 

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

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

15 self.descriptor = 'Mutation' 

16 self.min_inputs = 1 

17 

18 @classmethod 

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

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

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

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

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

24 

25 For further elaboration see: 

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

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

28 5, 397-409 (1992) 

29 

30 Parameters: 

31 

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

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

34 

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

36 atoms = atoms.copy() 

37 if elements is None: 

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

39 else: 

40 e = elements 

41 atoms.set_constraint() 

42 atoms.center() 

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

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

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

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

47 atomic_conf = [] 

48 old_dist = -10. 

49 for dist, i in dists: 

50 if abs(dist - old_dist) > eps: 

51 atomic_conf.append([i]) 

52 else: 

53 atomic_conf[-1].append(i) 

54 old_dist = dist 

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

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

57 for shell in atomic_conf: 

58 torem = [] 

59 for i in shell: 

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

61 torem.append(i) 

62 for i in torem: 

63 shell.remove(i) 

64 return atomic_conf 

65 

66 @classmethod 

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

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

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

70 are not permuted.""" 

71 possible_permutations = [] 

72 for i in l1: 

73 for j in l2: 

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

75 possible_permutations.append((i, j)) 

76 return possible_permutations 

77 

78 

79class RandomMutation(Mutation): 

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

81 

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

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

84 self.descriptor = 'RandomMutation' 

85 self.length = length 

86 

87 def mutate(self, atoms): 

88 """ Does the actual mutation. """ 

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

90 

91 indi = Atoms() 

92 for a in atoms: 

93 if a.index == tbm: 

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

95 indi.append(a) 

96 return indi 

97 

98 def get_new_individual(self, parents): 

99 f = parents[0] 

100 

101 indi = self.initialize_individual(f) 

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

103 

104 to_mut = f.copy() 

105 for _ in range(self.num_muts): 

106 to_mut = self.mutate(to_mut) 

107 

108 for atom in to_mut: 

109 indi.append(atom) 

110 

111 return (self.finalize_individual(indi), 

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

113 

114 @classmethod 

115 def random_vector(cls, l, rng=np.random): 

116 """return random vector of length l""" 

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

118 vl = np.linalg.norm(vec) 

119 return np.array([v * l / vl for v in vec]) 

120 

121 

122class RandomPermutation(Mutation): 

123 """Permutes two random atoms. 

124 

125 Parameters: 

126 

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

128 

129 rng: Random number generator 

130 By default numpy.random. 

131 """ 

132 

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

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

135 self.descriptor = 'RandomPermutation' 

136 self.elements = elements 

137 

138 def get_new_individual(self, parents): 

139 f = parents[0].copy() 

140 

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

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

143 

144 indi = self.initialize_individual(f) 

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

146 

147 for _ in range(self.num_muts): 

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

149 

150 for atom in f: 

151 indi.append(atom) 

152 

153 return (self.finalize_individual(indi), 

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

155 

156 @classmethod 

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

158 """Do the actual permutation.""" 

159 if elements is None: 

160 indices = range(len(atoms)) 

161 else: 

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

163 i1 = rng.choice(indices) 

164 i2 = rng.choice(indices) 

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

166 i2 = rng.choice(indices) 

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

168 

169 

170class COM2surfPermutation(Mutation): 

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

172 described in 

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

174 

175 Parameters: 

176 

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

178 for example: include all metals and exclude all adsorbates 

179 

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

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

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

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

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

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

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

187 

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

189 

190 rng: Random number generator 

191 By default numpy.random. 

192 """ 

193 

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

195 rng=np.random): 

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

197 self.descriptor = 'COM2surfPermutation' 

198 self.min_ratio = min_ratio 

199 self.elements = elements 

200 

201 def get_new_individual(self, parents): 

202 f = parents[0].copy() 

203 

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

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

206 

207 indi = self.initialize_individual(f) 

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

209 

210 for _ in range(self.num_muts): 

211 elems = self.elements 

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

213 

214 for atom in f: 

215 indi.append(atom) 

216 

217 return (self.finalize_individual(indi), 

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

219 

220 @classmethod 

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

222 """Performs the COM2surf permutation.""" 

223 ac = atoms.copy() 

224 if elements is not None: 

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

226 syms = ac.get_chemical_symbols() 

227 for el in set(syms): 

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

229 

230 atomic_conf = Mutation.get_atomic_configuration(atoms, 

231 elements=elements) 

232 core = COM2surfPermutation.get_core_indices(atoms, 

233 atomic_conf, 

234 min_ratio) 

235 shell = COM2surfPermutation.get_shell_indices(atoms, 

236 atomic_conf, 

237 min_ratio) 

238 permuts = Mutation.get_list_of_possible_permutations(atoms, 

239 core, 

240 shell) 

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

242 swap = list(permuts[chosen]) 

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

244 

245 @classmethod 

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

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

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

249 atomic configuration.""" 

250 elements = list(set([atoms[i].symbol 

251 for subl in atomic_conf for i in subl])) 

252 

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

254 while len(core) < 1: 

255 recurs += 1 

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

257 

258 for elem in elements: 

259 ratio = len([i for i in core 

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

261 if ratio < min_ratio: 

262 return COM2surfPermutation.get_core_indices(atoms, 

263 atomic_conf, 

264 min_ratio, 

265 recurs + 1) 

266 return core 

267 

268 @classmethod 

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

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

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

272 the supplied atomic configuration.""" 

273 elements = list(set([atoms[i].symbol 

274 for subl in atomic_conf for i in subl])) 

275 

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

277 while len(shell) < 1: 

278 recurs += 1 

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

280 

281 for elem in elements: 

282 ratio = len([i for i in shell 

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

284 if ratio < min_ratio: 

285 return COM2surfPermutation.get_shell_indices(atoms, 

286 atomic_conf, 

287 min_ratio, 

288 recurs + 1) 

289 return shell 

290 

291 

292class _NeighborhoodPermutation(Mutation): 

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

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

295 @classmethod 

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

297 recurs=0, distance_matrix=None): 

298 dm = distance_matrix 

299 if dm is None: 

300 dm = get_distance_matrix(atoms) 

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

302 # in the average bond length 

303 nndist = get_nndist(atoms, dm) + 0.2 

304 same_neighbors = {} 

305 

306 def f(x): 

307 return x[1] 

308 for i, atom in enumerate(atoms): 

309 same_neighbors[i] = 0 

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

311 for n in neighbors: 

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

313 same_neighbors[i] += 1 

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

315 if inverse: 

316 sorted_same.reverse() 

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

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

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

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

321 permuts = Mutation.get_list_of_possible_permutations(atoms, 

322 poor_indices, 

323 rich_indices) 

324 

325 if len(permuts) == 0: 

326 _NP = _NeighborhoodPermutation 

327 return _NP.get_possible_poor2rich_permutations(atoms, inverse, 

328 recurs + 1, dm) 

329 return permuts 

330 

331 

332class Poor2richPermutation(_NeighborhoodPermutation): 

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

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

335 

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

337 regions rich in the same elements. 

338 (Inverse of Rich2poorPermutation) 

339 

340 Parameters: 

341 

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

343 

344 rng: Random number generator 

345 By default numpy.random. 

346 """ 

347 

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

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

350 self.descriptor = 'Poor2richPermutation' 

351 self.elements = elements 

352 

353 def get_new_individual(self, parents): 

354 f = parents[0].copy() 

355 

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

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

358 

359 indi = self.initialize_individual(f) 

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

361 

362 for _ in range(self.num_muts): 

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

364 

365 for atom in f: 

366 indi.append(atom) 

367 

368 return (self.finalize_individual(indi), 

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

370 

371 @classmethod 

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

373 _NP = _NeighborhoodPermutation 

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

375 ac = atoms.copy() 

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

377 if atom.symbol not in elements]] 

378 permuts = _NP.get_possible_poor2rich_permutations(ac) 

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

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

381 

382 

383class Rich2poorPermutation(_NeighborhoodPermutation): 

384 """ 

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

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

387 

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

389 regions short of the same elements. 

390 (Inverse of Poor2richPermutation) 

391 

392 Parameters: 

393 

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

395 

396 rng: Random number generator 

397 By default numpy.random. 

398 """ 

399 

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

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

402 self.descriptor = 'Rich2poorPermutation' 

403 self.elements = elements 

404 

405 def get_new_individual(self, parents): 

406 f = parents[0].copy() 

407 

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

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

410 

411 indi = self.initialize_individual(f) 

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

413 

414 if self.elements is None: 

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

416 else: 

417 elems = self.elements 

418 for _ in range(self.num_muts): 

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

420 

421 for atom in f: 

422 indi.append(atom) 

423 

424 return (self.finalize_individual(indi), 

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

426 

427 @classmethod 

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

429 _NP = _NeighborhoodPermutation 

430 ac = atoms.copy() 

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

432 if atom.symbol not in elements]] 

433 permuts = _NP.get_possible_poor2rich_permutations(ac, 

434 inverse=True) 

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

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

437 

438 

439class SymmetricSubstitute(Mutation): 

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

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

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

443 

444 """ 

445 

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

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

448 self.descriptor = 'SymmetricSubstitute' 

449 self.elements = elements 

450 

451 def substitute(self, atoms): 

452 """Does the actual substitution""" 

453 atoms = atoms.copy() 

454 aconf = self.get_atomic_configuration(atoms, 

455 elements=self.elements) 

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

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

458 

459 for i in aconf[itbm]: 

460 atoms[i].symbol = to_element 

461 

462 return atoms 

463 

464 def get_new_individual(self, parents): 

465 f = parents[0] 

466 

467 indi = self.substitute(f) 

468 indi = self.initialize_individual(f, indi) 

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

470 

471 return (self.finalize_individual(indi), 

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

473 

474 

475class RandomSubstitute(Mutation): 

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

477 are supplied in the parameter elements""" 

478 

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

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

481 self.descriptor = 'RandomSubstitute' 

482 self.elements = elements 

483 

484 def substitute(self, atoms): 

485 """Does the actual substitution""" 

486 atoms = atoms.copy() 

487 if self.elements is None: 

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

489 else: 

490 elems = self.elements[:] 

491 possible_indices = [a.index for a in atoms 

492 if a.symbol in elems] 

493 itbm = self.rng.choice(possible_indices) 

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

495 new_symbol = self.rng.choice(elems) 

496 atoms[itbm].symbol = new_symbol 

497 

498 return atoms 

499 

500 def get_new_individual(self, parents): 

501 f = parents[0] 

502 

503 indi = self.substitute(f) 

504 indi = self.initialize_individual(f, indi) 

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

506 

507 return (self.finalize_individual(indi), 

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