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
4from ase.ga.offspring_creator import OffspringCreator
5from ase.ga.utilities import get_distance_matrix, get_nndist
6from ase import Atoms
9class Mutation(OffspringCreator):
10 """Base class for all particle mutation type operators.
11 Do not call this class directly."""
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
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.
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)
30 Parameters:
32 elements: Only take into account the elements specified in this
33 list. Default is to take all elements into account.
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
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
79class RandomMutation(Mutation):
80 """Moves a random atom the supplied length in a random direction."""
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
87 def mutate(self, atoms):
88 """ Does the actual mutation. """
89 tbm = self.rng.choice(range(len(atoms)))
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
98 def get_new_individual(self, parents):
99 f = parents[0]
101 indi = self.initialize_individual(f)
102 indi.info['data']['parents'] = [f.info['confid']]
104 to_mut = f.copy()
105 for _ in range(self.num_muts):
106 to_mut = self.mutate(to_mut)
108 for atom in to_mut:
109 indi.append(atom)
111 return (self.finalize_individual(indi),
112 self.descriptor + ':Parent {0}'.format(f.info['confid']))
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])
122class RandomPermutation(Mutation):
123 """Permutes two random atoms.
125 Parameters:
127 num_muts: the number of times to perform this operation.
129 rng: Random number generator
130 By default numpy.random.
131 """
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
138 def get_new_individual(self, parents):
139 f = parents[0].copy()
141 diffatoms = len(set(f.numbers))
142 assert diffatoms > 1, 'Permutations with one atomic type is not valid'
144 indi = self.initialize_individual(f)
145 indi.info['data']['parents'] = [f.info['confid']]
147 for _ in range(self.num_muts):
148 RandomPermutation.mutate(f, self.elements, rng=self.rng)
150 for atom in f:
151 indi.append(atom)
153 return (self.finalize_individual(indi),
154 self.descriptor + ':Parent {0}'.format(f.info['confid']))
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]]
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
175 Parameters:
177 elements: which elements should be included in this permutation,
178 for example: include all metals and exclude all adsorbates
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.
188 num_muts: the number of times to perform this operation.
190 rng: Random number generator
191 By default numpy.random.
192 """
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
201 def get_new_individual(self, parents):
202 f = parents[0].copy()
204 diffatoms = len(set(f.numbers))
205 assert diffatoms > 1, 'Permutations with one atomic type is not valid'
207 indi = self.initialize_individual(f)
208 indi.info['data']['parents'] = [f.info['confid']]
210 for _ in range(self.num_muts):
211 elems = self.elements
212 COM2surfPermutation.mutate(f, elems, self.min_ratio, rng=self.rng)
214 for atom in f:
215 indi.append(atom)
217 return (self.finalize_individual(indi),
218 self.descriptor + ':Parent {0}'.format(f.info['confid']))
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
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]]
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]))
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]
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
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]))
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]
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
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 = {}
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)
325 if len(permuts) == 0:
326 _NP = _NeighborhoodPermutation
327 return _NP.get_possible_poor2rich_permutations(atoms, inverse,
328 recurs + 1, dm)
329 return permuts
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
336 Permutes two atoms from regions short of the same elements, to
337 regions rich in the same elements.
338 (Inverse of Rich2poorPermutation)
340 Parameters:
342 elements: Which elements to take into account in this permutation
344 rng: Random number generator
345 By default numpy.random.
346 """
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
353 def get_new_individual(self, parents):
354 f = parents[0].copy()
356 diffatoms = len(set(f.numbers))
357 assert diffatoms > 1, 'Permutations with one atomic type is not valid'
359 indi = self.initialize_individual(f)
360 indi.info['data']['parents'] = [f.info['confid']]
362 for _ in range(self.num_muts):
363 Poor2richPermutation.mutate(f, self.elements, rng=self.rng)
365 for atom in f:
366 indi.append(atom)
368 return (self.finalize_individual(indi),
369 self.descriptor + ':Parent {0}'.format(f.info['confid']))
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]]
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
388 Permutes two atoms from regions rich in the same elements, to
389 regions short of the same elements.
390 (Inverse of Poor2richPermutation)
392 Parameters:
394 elements: Which elements to take into account in this permutation
396 rng: Random number generator
397 By default numpy.random.
398 """
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
405 def get_new_individual(self, parents):
406 f = parents[0].copy()
408 diffatoms = len(set(f.numbers))
409 assert diffatoms > 1, 'Permutations with one atomic type is not valid'
411 indi = self.initialize_individual(f)
412 indi.info['data']['parents'] = [f.info['confid']]
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)
421 for atom in f:
422 indi.append(atom)
424 return (self.finalize_individual(indi),
425 self.descriptor + ':Parent {0}'.format(f.info['confid']))
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]]
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.
444 """
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
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)
459 for i in aconf[itbm]:
460 atoms[i].symbol = to_element
462 return atoms
464 def get_new_individual(self, parents):
465 f = parents[0]
467 indi = self.substitute(f)
468 indi = self.initialize_individual(f, indi)
469 indi.info['data']['parents'] = [f.info['confid']]
471 return (self.finalize_individual(indi),
472 self.descriptor + ':Parent {0}'.format(f.info['confid']))
475class RandomSubstitute(Mutation):
476 """Substitutes one atom with another atom type. The possible atom types
477 are supplied in the parameter elements"""
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
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
498 return atoms
500 def get_new_individual(self, parents):
501 f = parents[0]
503 indi = self.substitute(f)
504 indi = self.initialize_individual(f, indi)
505 indi.info['data']['parents'] = [f.info['confid']]
507 return (self.finalize_individual(indi),
508 self.descriptor + ':Parent {0}'.format(f.info['confid']))