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
« prev ^ index » next coverage.py v7.2.7, created at 2023-12-10 11:04 +0000
1from operator import itemgetter
3import numpy as np
5from ase import Atoms
6from ase.ga.offspring_creator import OffspringCreator
7from ase.ga.utilities import get_distance_matrix, get_nndist
10class Mutation(OffspringCreator):
11 """Base class for all particle mutation type operators.
12 Do not call this class directly."""
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
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.
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)
31 Parameters:
33 elements: Only take into account the elements specified in this
34 list. Default is to take all elements into account.
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
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
80class RandomMutation(Mutation):
81 """Moves a random atom the supplied length in a random direction."""
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
88 def mutate(self, atoms):
89 """ Does the actual mutation. """
90 tbm = self.rng.choice(range(len(atoms)))
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
99 def get_new_individual(self, parents):
100 f = parents[0]
102 indi = self.initialize_individual(f)
103 indi.info['data']['parents'] = [f.info['confid']]
105 to_mut = f.copy()
106 for _ in range(self.num_muts):
107 to_mut = self.mutate(to_mut)
109 for atom in to_mut:
110 indi.append(atom)
112 return (self.finalize_individual(indi),
113 self.descriptor + ':Parent {}'.format(f.info['confid']))
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])
123class RandomPermutation(Mutation):
124 """Permutes two random atoms.
126 Parameters:
128 num_muts: the number of times to perform this operation.
130 rng: Random number generator
131 By default numpy.random.
132 """
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
139 def get_new_individual(self, parents):
140 f = parents[0].copy()
142 diffatoms = len(set(f.numbers))
143 assert diffatoms > 1, 'Permutations with one atomic type is not valid'
145 indi = self.initialize_individual(f)
146 indi.info['data']['parents'] = [f.info['confid']]
148 for _ in range(self.num_muts):
149 RandomPermutation.mutate(f, self.elements, rng=self.rng)
151 for atom in f:
152 indi.append(atom)
154 return (self.finalize_individual(indi),
155 self.descriptor + ':Parent {}'.format(f.info['confid']))
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]]
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
176 Parameters:
178 elements: which elements should be included in this permutation,
179 for example: include all metals and exclude all adsorbates
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.
189 num_muts: the number of times to perform this operation.
191 rng: Random number generator
192 By default numpy.random.
193 """
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
202 def get_new_individual(self, parents):
203 f = parents[0].copy()
205 diffatoms = len(set(f.numbers))
206 assert diffatoms > 1, 'Permutations with one atomic type is not valid'
208 indi = self.initialize_individual(f)
209 indi.info['data']['parents'] = [f.info['confid']]
211 for _ in range(self.num_muts):
212 elems = self.elements
213 COM2surfPermutation.mutate(f, elems, self.min_ratio, rng=self.rng)
215 for atom in f:
216 indi.append(atom)
218 return (self.finalize_individual(indi),
219 self.descriptor + ':Parent {}'.format(f.info['confid']))
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
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]]
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})
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]
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
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})
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]
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
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 = {}
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)
326 if len(permuts) == 0:
327 _NP = _NeighborhoodPermutation
328 return _NP.get_possible_poor2rich_permutations(atoms, inverse,
329 recurs + 1, dm)
330 return permuts
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
337 Permutes two atoms from regions short of the same elements, to
338 regions rich in the same elements.
339 (Inverse of Rich2poorPermutation)
341 Parameters:
343 elements: Which elements to take into account in this permutation
345 rng: Random number generator
346 By default numpy.random.
347 """
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
354 def get_new_individual(self, parents):
355 f = parents[0].copy()
357 diffatoms = len(set(f.numbers))
358 assert diffatoms > 1, 'Permutations with one atomic type is not valid'
360 indi = self.initialize_individual(f)
361 indi.info['data']['parents'] = [f.info['confid']]
363 for _ in range(self.num_muts):
364 Poor2richPermutation.mutate(f, self.elements, rng=self.rng)
366 for atom in f:
367 indi.append(atom)
369 return (self.finalize_individual(indi),
370 self.descriptor + ':Parent {}'.format(f.info['confid']))
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]]
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
389 Permutes two atoms from regions rich in the same elements, to
390 regions short of the same elements.
391 (Inverse of Poor2richPermutation)
393 Parameters:
395 elements: Which elements to take into account in this permutation
397 rng: Random number generator
398 By default numpy.random.
399 """
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
406 def get_new_individual(self, parents):
407 f = parents[0].copy()
409 diffatoms = len(set(f.numbers))
410 assert diffatoms > 1, 'Permutations with one atomic type is not valid'
412 indi = self.initialize_individual(f)
413 indi.info['data']['parents'] = [f.info['confid']]
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)
422 for atom in f:
423 indi.append(atom)
425 return (self.finalize_individual(indi),
426 self.descriptor + ':Parent {}'.format(f.info['confid']))
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]]
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.
445 """
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
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)
460 for i in aconf[itbm]:
461 atoms[i].symbol = to_element
463 return atoms
465 def get_new_individual(self, parents):
466 f = parents[0]
468 indi = self.substitute(f)
469 indi = self.initialize_individual(f, indi)
470 indi.info['data']['parents'] = [f.info['confid']]
472 return (self.finalize_individual(indi),
473 self.descriptor + ':Parent {}'.format(f.info['confid']))
476class RandomSubstitute(Mutation):
477 """Substitutes one atom with another atom type. The possible atom types
478 are supplied in the parameter elements"""
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
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
499 return atoms
501 def get_new_individual(self, parents):
502 f = parents[0]
504 indi = self.substitute(f)
505 indi = self.initialize_individual(f, indi)
506 indi.info['data']['parents'] = [f.info['confid']]
508 return (self.finalize_individual(indi),
509 self.descriptor + ':Parent {}'.format(f.info['confid']))