Coverage for /builds/ase/ase/ase/ga/particle_mutations.py: 76.17%

277 statements  

« prev     ^ index     » next       coverage.py v7.5.3, created at 2025-08-02 00:12 +0000

1# fmt: off 

2 

3from operator import itemgetter 

4 

5import numpy as np 

6 

7from ase import Atoms 

8from ase.ga.offspring_creator import OffspringCreator 

9from ase.ga.utilities import get_distance_matrix, get_nndist 

10 

11 

12class Mutation(OffspringCreator): 

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

14 Do not call this class directly.""" 

15 

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

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

18 self.descriptor = 'Mutation' 

19 self.min_inputs = 1 

20 

21 @classmethod 

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

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

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

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

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

27 

28 For further elaboration see: 

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

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

31 5, 397-409 (1992) 

32 

33 Parameters: 

34 

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

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

37 

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

39 atoms = atoms.copy() 

40 if elements is None: 

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

42 else: 

43 e = elements 

44 atoms.set_constraint() 

45 atoms.center() 

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

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

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

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

50 atomic_conf = [] 

51 old_dist = -10. 

52 for dist, i in dists: 

53 if abs(dist - old_dist) > eps: 

54 atomic_conf.append([i]) 

55 else: 

56 atomic_conf[-1].append(i) 

57 old_dist = dist 

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

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

60 for shell in atomic_conf: 

61 torem = [] 

62 for i in shell: 

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

64 torem.append(i) 

65 for i in torem: 

66 shell.remove(i) 

67 return atomic_conf 

68 

69 @classmethod 

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

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

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

73 are not permuted.""" 

74 possible_permutations = [] 

75 for i in l1: 

76 for j in l2: 

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

78 possible_permutations.append((i, j)) 

79 return possible_permutations 

80 

81 

82class RandomMutation(Mutation): 

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

84 

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

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

87 self.descriptor = 'RandomMutation' 

88 self.length = length 

89 

90 def mutate(self, atoms): 

91 """ Does the actual mutation. """ 

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

93 

94 indi = Atoms() 

95 for a in atoms: 

96 if a.index == tbm: 

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

98 indi.append(a) 

99 return indi 

100 

101 def get_new_individual(self, parents): 

102 f = parents[0] 

103 

104 indi = self.initialize_individual(f) 

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

106 

107 to_mut = f.copy() 

108 for _ in range(self.num_muts): 

109 to_mut = self.mutate(to_mut) 

110 

111 for atom in to_mut: 

112 indi.append(atom) 

113 

114 return (self.finalize_individual(indi), 

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

116 

117 @classmethod 

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

119 """return random vector of certain length""" 

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

121 vl = np.linalg.norm(vec) 

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

123 

124 

125class RandomPermutation(Mutation): 

126 """Permutes two random atoms. 

127 

128 Parameters: 

129 

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

131 

132 rng: Random number generator 

133 By default numpy.random. 

134 """ 

135 

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

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

138 self.descriptor = 'RandomPermutation' 

139 self.elements = elements 

140 

141 def get_new_individual(self, parents): 

142 f = parents[0].copy() 

143 

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

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

146 

147 indi = self.initialize_individual(f) 

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

149 

150 for _ in range(self.num_muts): 

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

152 

153 for atom in f: 

154 indi.append(atom) 

155 

156 return (self.finalize_individual(indi), 

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

158 

159 @classmethod 

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

161 """Do the actual permutation.""" 

162 if elements is None: 

163 indices = range(len(atoms)) 

164 else: 

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

166 i1 = rng.choice(indices) 

167 i2 = rng.choice(indices) 

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

169 i2 = rng.choice(indices) 

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

171 

172 

173class COM2surfPermutation(Mutation): 

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

175 described in 

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

177 

178 Parameters: 

179 

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

181 for example: include all metals and exclude all adsorbates 

182 

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

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

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

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

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

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

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

190 

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

192 

193 rng: Random number generator 

194 By default numpy.random. 

195 """ 

196 

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

198 rng=np.random): 

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

200 self.descriptor = 'COM2surfPermutation' 

201 self.min_ratio = min_ratio 

202 self.elements = elements 

203 

204 def get_new_individual(self, parents): 

205 f = parents[0].copy() 

206 

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

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

209 

210 indi = self.initialize_individual(f) 

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

212 

213 for _ in range(self.num_muts): 

214 elems = self.elements 

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

216 

217 for atom in f: 

218 indi.append(atom) 

219 

220 return (self.finalize_individual(indi), 

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

222 

223 @classmethod 

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

225 """Performs the COM2surf permutation.""" 

226 ac = atoms.copy() 

227 if elements is not None: 

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

229 syms = ac.get_chemical_symbols() 

230 for el in set(syms): 

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

232 

233 atomic_conf = Mutation.get_atomic_configuration(atoms, 

234 elements=elements) 

235 core = COM2surfPermutation.get_core_indices(atoms, 

236 atomic_conf, 

237 min_ratio) 

238 shell = COM2surfPermutation.get_shell_indices(atoms, 

239 atomic_conf, 

240 min_ratio) 

241 permuts = Mutation.get_list_of_possible_permutations(atoms, 

242 core, 

243 shell) 

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

245 swap = list(permuts[chosen]) 

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

247 

248 @classmethod 

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

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

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

252 atomic configuration.""" 

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

254 for subl in atomic_conf for i in subl}) 

255 

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

257 while len(core) < 1: 

258 recurs += 1 

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

260 

261 for elem in elements: 

262 ratio = len([i for i in core 

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

264 if ratio < min_ratio: 

265 return COM2surfPermutation.get_core_indices(atoms, 

266 atomic_conf, 

267 min_ratio, 

268 recurs + 1) 

269 return core 

270 

271 @classmethod 

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

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

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

275 the supplied atomic configuration.""" 

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

277 for subl in atomic_conf for i in subl}) 

278 

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

280 while len(shell) < 1: 

281 recurs += 1 

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

283 

284 for elem in elements: 

285 ratio = len([i for i in shell 

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

287 if ratio < min_ratio: 

288 return COM2surfPermutation.get_shell_indices(atoms, 

289 atomic_conf, 

290 min_ratio, 

291 recurs + 1) 

292 return shell 

293 

294 

295class _NeighborhoodPermutation(Mutation): 

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

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

298 @classmethod 

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

300 recurs=0, distance_matrix=None): 

301 dm = distance_matrix 

302 if dm is None: 

303 dm = get_distance_matrix(atoms) 

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

305 # in the average bond length 

306 nndist = get_nndist(atoms, dm) + 0.2 

307 same_neighbors = {} 

308 

309 def f(x): 

310 return x[1] 

311 for i, atom in enumerate(atoms): 

312 same_neighbors[i] = 0 

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

314 for n in neighbors: 

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

316 same_neighbors[i] += 1 

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

318 if inverse: 

319 sorted_same.reverse() 

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

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

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

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

324 permuts = Mutation.get_list_of_possible_permutations(atoms, 

325 poor_indices, 

326 rich_indices) 

327 

328 if len(permuts) == 0: 

329 _NP = _NeighborhoodPermutation 

330 return _NP.get_possible_poor2rich_permutations(atoms, inverse, 

331 recurs + 1, dm) 

332 return permuts 

333 

334 

335class Poor2richPermutation(_NeighborhoodPermutation): 

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

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

338 

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

340 regions rich in the same elements. 

341 (Inverse of Rich2poorPermutation) 

342 

343 Parameters: 

344 

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

346 

347 rng: Random number generator 

348 By default numpy.random. 

349 """ 

350 

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

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

353 self.descriptor = 'Poor2richPermutation' 

354 self.elements = elements 

355 

356 def get_new_individual(self, parents): 

357 f = parents[0].copy() 

358 

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

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

361 

362 indi = self.initialize_individual(f) 

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

364 

365 for _ in range(self.num_muts): 

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

367 

368 for atom in f: 

369 indi.append(atom) 

370 

371 return (self.finalize_individual(indi), 

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

373 

374 @classmethod 

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

376 _NP = _NeighborhoodPermutation 

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

378 ac = atoms.copy() 

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

380 if atom.symbol not in elements]] 

381 permuts = _NP.get_possible_poor2rich_permutations(ac) 

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

383 swap = list(permuts[chosen]) 

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

385 

386 

387class Rich2poorPermutation(_NeighborhoodPermutation): 

388 """ 

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

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

391 

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

393 regions short of the same elements. 

394 (Inverse of Poor2richPermutation) 

395 

396 Parameters: 

397 

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

399 

400 rng: Random number generator 

401 By default numpy.random. 

402 """ 

403 

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

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

406 self.descriptor = 'Rich2poorPermutation' 

407 self.elements = elements 

408 

409 def get_new_individual(self, parents): 

410 f = parents[0].copy() 

411 

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

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

414 

415 indi = self.initialize_individual(f) 

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

417 

418 if self.elements is None: 

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

420 else: 

421 elems = self.elements 

422 for _ in range(self.num_muts): 

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

424 

425 for atom in f: 

426 indi.append(atom) 

427 

428 return (self.finalize_individual(indi), 

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

430 

431 @classmethod 

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

433 _NP = _NeighborhoodPermutation 

434 ac = atoms.copy() 

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

436 if atom.symbol not in elements]] 

437 permuts = _NP.get_possible_poor2rich_permutations(ac, 

438 inverse=True) 

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

440 swap = list(permuts[chosen]) 

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

442 

443 

444class SymmetricSubstitute(Mutation): 

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

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

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

448 

449 """ 

450 

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

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

453 self.descriptor = 'SymmetricSubstitute' 

454 self.elements = elements 

455 

456 def substitute(self, atoms): 

457 """Does the actual substitution""" 

458 atoms = atoms.copy() 

459 aconf = self.get_atomic_configuration(atoms, 

460 elements=self.elements) 

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

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

463 

464 for i in aconf[itbm]: 

465 atoms[i].symbol = to_element 

466 

467 return atoms 

468 

469 def get_new_individual(self, parents): 

470 f = parents[0] 

471 

472 indi = self.substitute(f) 

473 indi = self.initialize_individual(f, indi) 

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

475 

476 return (self.finalize_individual(indi), 

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

478 

479 

480class RandomSubstitute(Mutation): 

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

482 are supplied in the parameter elements""" 

483 

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

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

486 self.descriptor = 'RandomSubstitute' 

487 self.elements = elements 

488 

489 def substitute(self, atoms): 

490 """Does the actual substitution""" 

491 atoms = atoms.copy() 

492 if self.elements is None: 

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

494 else: 

495 elems = self.elements[:] 

496 possible_indices = [a.index for a in atoms 

497 if a.symbol in elems] 

498 itbm = self.rng.choice(possible_indices) 

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

500 new_symbol = self.rng.choice(elems) 

501 atoms[itbm].symbol = new_symbol 

502 

503 return atoms 

504 

505 def get_new_individual(self, parents): 

506 f = parents[0] 

507 

508 indi = self.substitute(f) 

509 indi = self.initialize_individual(f, indi) 

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

511 

512 return (self.finalize_individual(indi), 

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