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
« prev ^ index » next coverage.py v7.5.3, created at 2025-08-02 00:12 +0000
1# fmt: off
3from operator import itemgetter
5import numpy as np
7from ase import Atoms
8from ase.ga.offspring_creator import OffspringCreator
9from ase.ga.utilities import get_distance_matrix, get_nndist
12class Mutation(OffspringCreator):
13 """Base class for all particle mutation type operators.
14 Do not call this class directly."""
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
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.
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)
33 Parameters:
35 elements: Only take into account the elements specified in this
36 list. Default is to take all elements into account.
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
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
82class RandomMutation(Mutation):
83 """Moves a random atom the supplied length in a random direction."""
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
90 def mutate(self, atoms):
91 """ Does the actual mutation. """
92 tbm = self.rng.choice(range(len(atoms)))
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
101 def get_new_individual(self, parents):
102 f = parents[0]
104 indi = self.initialize_individual(f)
105 indi.info['data']['parents'] = [f.info['confid']]
107 to_mut = f.copy()
108 for _ in range(self.num_muts):
109 to_mut = self.mutate(to_mut)
111 for atom in to_mut:
112 indi.append(atom)
114 return (self.finalize_individual(indi),
115 self.descriptor + ':Parent {}'.format(f.info['confid']))
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])
125class RandomPermutation(Mutation):
126 """Permutes two random atoms.
128 Parameters:
130 num_muts: the number of times to perform this operation.
132 rng: Random number generator
133 By default numpy.random.
134 """
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
141 def get_new_individual(self, parents):
142 f = parents[0].copy()
144 diffatoms = len(set(f.numbers))
145 assert diffatoms > 1, 'Permutations with one atomic type is not valid'
147 indi = self.initialize_individual(f)
148 indi.info['data']['parents'] = [f.info['confid']]
150 for _ in range(self.num_muts):
151 RandomPermutation.mutate(f, self.elements, rng=self.rng)
153 for atom in f:
154 indi.append(atom)
156 return (self.finalize_individual(indi),
157 self.descriptor + ':Parent {}'.format(f.info['confid']))
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]]
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
178 Parameters:
180 elements: which elements should be included in this permutation,
181 for example: include all metals and exclude all adsorbates
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.
191 num_muts: the number of times to perform this operation.
193 rng: Random number generator
194 By default numpy.random.
195 """
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
204 def get_new_individual(self, parents):
205 f = parents[0].copy()
207 diffatoms = len(set(f.numbers))
208 assert diffatoms > 1, 'Permutations with one atomic type is not valid'
210 indi = self.initialize_individual(f)
211 indi.info['data']['parents'] = [f.info['confid']]
213 for _ in range(self.num_muts):
214 elems = self.elements
215 COM2surfPermutation.mutate(f, elems, self.min_ratio, rng=self.rng)
217 for atom in f:
218 indi.append(atom)
220 return (self.finalize_individual(indi),
221 self.descriptor + ':Parent {}'.format(f.info['confid']))
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
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]]
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})
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]
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
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})
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]
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
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 = {}
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)
328 if len(permuts) == 0:
329 _NP = _NeighborhoodPermutation
330 return _NP.get_possible_poor2rich_permutations(atoms, inverse,
331 recurs + 1, dm)
332 return permuts
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
339 Permutes two atoms from regions short of the same elements, to
340 regions rich in the same elements.
341 (Inverse of Rich2poorPermutation)
343 Parameters:
345 elements: Which elements to take into account in this permutation
347 rng: Random number generator
348 By default numpy.random.
349 """
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
356 def get_new_individual(self, parents):
357 f = parents[0].copy()
359 diffatoms = len(set(f.numbers))
360 assert diffatoms > 1, 'Permutations with one atomic type is not valid'
362 indi = self.initialize_individual(f)
363 indi.info['data']['parents'] = [f.info['confid']]
365 for _ in range(self.num_muts):
366 Poor2richPermutation.mutate(f, self.elements, rng=self.rng)
368 for atom in f:
369 indi.append(atom)
371 return (self.finalize_individual(indi),
372 self.descriptor + ':Parent {}'.format(f.info['confid']))
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]]
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
392 Permutes two atoms from regions rich in the same elements, to
393 regions short of the same elements.
394 (Inverse of Poor2richPermutation)
396 Parameters:
398 elements: Which elements to take into account in this permutation
400 rng: Random number generator
401 By default numpy.random.
402 """
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
409 def get_new_individual(self, parents):
410 f = parents[0].copy()
412 diffatoms = len(set(f.numbers))
413 assert diffatoms > 1, 'Permutations with one atomic type is not valid'
415 indi = self.initialize_individual(f)
416 indi.info['data']['parents'] = [f.info['confid']]
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)
425 for atom in f:
426 indi.append(atom)
428 return (self.finalize_individual(indi),
429 self.descriptor + ':Parent {}'.format(f.info['confid']))
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]]
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.
449 """
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
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)
464 for i in aconf[itbm]:
465 atoms[i].symbol = to_element
467 return atoms
469 def get_new_individual(self, parents):
470 f = parents[0]
472 indi = self.substitute(f)
473 indi = self.initialize_individual(f, indi)
474 indi.info['data']['parents'] = [f.info['confid']]
476 return (self.finalize_individual(indi),
477 self.descriptor + ':Parent {}'.format(f.info['confid']))
480class RandomSubstitute(Mutation):
481 """Substitutes one atom with another atom type. The possible atom types
482 are supplied in the parameter elements"""
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
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
503 return atoms
505 def get_new_individual(self, parents):
506 f = parents[0]
508 indi = self.substitute(f)
509 indi = self.initialize_individual(f, indi)
510 indi.info['data']['parents'] = [f.info['confid']]
512 return (self.finalize_individual(indi),
513 self.descriptor + ':Parent {}'.format(f.info['confid']))